Merge branch 'v8/dev' into v8/feature/5170-IPublishedContent

# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
This commit is contained in:
Shannon
2019-06-21 15:06:43 +10:00
137 changed files with 2017 additions and 1814 deletions
@@ -26,26 +26,26 @@ namespace Umbraco.Core.Logging.Viewer
/// A count of number of errors
/// By counting Warnings with Exceptions, Errors & Fatal messages
/// </summary>
int GetNumberOfErrors(DateTimeOffset startDate, DateTimeOffset endDate);
int GetNumberOfErrors(LogTimePeriod logTimePeriod);
/// <summary>
/// Returns a number of the different log level entries
/// </summary>
LogLevelCounts GetLogLevelCounts(DateTimeOffset startDate, DateTimeOffset endDate);
LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod);
/// <summary>
/// Returns a list of all unique message templates and their counts
/// </summary>
IEnumerable<LogTemplate> GetMessageTemplates(DateTimeOffset startDate, DateTimeOffset endDate);
IEnumerable<LogTemplate> GetMessageTemplates(LogTimePeriod logTimePeriod);
bool CanHandleLargeLogs { get; }
bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate);
bool CheckCanOpenLogs(LogTimePeriod logTimePeriod);
/// <summary>
/// Returns the collection of logs
/// </summary>
PagedResult<LogMessage> GetLogs(DateTimeOffset startDate, DateTimeOffset endDate,
PagedResult<LogMessage> GetLogs(LogTimePeriod logTimePeriod,
int pageNumber = 1,
int pageSize = 100,
Direction orderDirection = Direction.Descending,
@@ -26,7 +26,7 @@ namespace Umbraco.Core.Logging.Viewer
public override bool CanHandleLargeLogs => false;
public override bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate)
public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod)
{
//Log Directory
var logDirectory = _logsPath;
@@ -36,7 +36,7 @@ namespace Umbraco.Core.Logging.Viewer
//foreach full day in the range - see if we can find one or more filenames that end with
//yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing
for (var day = startDate.Date; day.Date <= endDate.Date; day = day.AddDays(1))
for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1))
{
//Filename ending to search for (As could be multiple)
var filesToFind = GetSearchPattern(day);
@@ -57,7 +57,7 @@ namespace Umbraco.Core.Logging.Viewer
return $"*{day:yyyyMMdd}*.json";
}
protected override IReadOnlyList<LogEvent> GetLogs(DateTimeOffset startDate, DateTimeOffset endDate, ILogFilter filter, int skip, int take)
protected override IReadOnlyList<LogEvent> GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take)
{
var logs = new List<LogEvent>();
@@ -68,7 +68,7 @@ namespace Umbraco.Core.Logging.Viewer
//foreach full day in the range - see if we can find one or more filenames that end with
//yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing
for (var day = startDate.Date; day.Date <= endDate.Date; day = day.AddDays(1))
for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1))
{
//Filename ending to search for (As could be multiple)
var filesToFind = GetSearchPattern(day);
@@ -0,0 +1,16 @@
using System;
namespace Umbraco.Core.Logging.Viewer
{
public class LogTimePeriod
{
public LogTimePeriod(DateTime startTime, DateTime endTime)
{
StartTime = startTime;
EndTime = endTime;
}
public DateTime StartTime { get; }
public DateTime EndTime { get; }
}
}
@@ -27,9 +27,9 @@ namespace Umbraco.Core.Logging.Viewer
/// <summary>
/// Get all logs from your chosen data source back as Serilog LogEvents
/// </summary>
protected abstract IReadOnlyList<LogEvent> GetLogs(DateTimeOffset startDate, DateTimeOffset endDate, ILogFilter filter, int skip, int take);
protected abstract IReadOnlyList<LogEvent> GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take);
public abstract bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate);
public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod);
public virtual IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
@@ -82,24 +82,24 @@ namespace Umbraco.Core.Logging.Viewer
return searches;
}
public int GetNumberOfErrors(DateTimeOffset startDate, DateTimeOffset endDate)
public int GetNumberOfErrors(LogTimePeriod logTimePeriod)
{
var errorCounter = new ErrorCounterFilter();
GetLogs(startDate, endDate, errorCounter, 0, int.MaxValue);
GetLogs(logTimePeriod, errorCounter, 0, int.MaxValue);
return errorCounter.Count;
}
public LogLevelCounts GetLogLevelCounts(DateTimeOffset startDate, DateTimeOffset endDate)
public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod)
{
var counter = new CountingFilter();
GetLogs(startDate, endDate, counter, 0, int.MaxValue);
GetLogs(logTimePeriod, counter, 0, int.MaxValue);
return counter.Counts;
}
public IEnumerable<LogTemplate> GetMessageTemplates(DateTimeOffset startDate, DateTimeOffset endDate)
public IEnumerable<LogTemplate> GetMessageTemplates(LogTimePeriod logTimePeriod)
{
var messageTemplates = new MessageTemplateFilter();
GetLogs(startDate, endDate, messageTemplates, 0, int.MaxValue);
GetLogs(logTimePeriod, messageTemplates, 0, int.MaxValue);
var templates = messageTemplates.Counts.
Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value })
@@ -108,14 +108,14 @@ namespace Umbraco.Core.Logging.Viewer
return templates;
}
public PagedResult<LogMessage> GetLogs(DateTimeOffset startDate, DateTimeOffset endDate,
public PagedResult<LogMessage> GetLogs(LogTimePeriod logTimePeriod,
int pageNumber = 1, int pageSize = 100,
Direction orderDirection = Direction.Descending,
string filterExpression = null,
string[] logLevels = null)
{
var expression = new ExpressionFilter(filterExpression);
var filteredLogs = GetLogs(startDate, endDate, expression, 0, int.MaxValue);
var filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue);
//This is user used the checkbox UI to toggle which log levels they wish to see
//If an empty array or null - its implied all levels to be viewed
@@ -389,7 +389,7 @@ namespace Umbraco.Core.Migrations.Install
private DatabaseSchemaResult ValidateSchema(IScope scope)
{
if (_databaseFactory.Configured == false)
if (_databaseFactory.Initialized == false)
return new DatabaseSchemaResult(_databaseFactory.SqlContext.SqlSyntax);
if (_databaseSchemaValidationResult != null)
@@ -513,7 +513,7 @@ namespace Umbraco.Core.Migrations.Install
private Attempt<Result> CheckReadyForInstall()
{
if (_databaseFactory.Configured == false)
if (_databaseFactory.CanConnect == false)
{
return Attempt.Fail(new Result
{
@@ -3,6 +3,7 @@ using System.Linq;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Implements <see cref="IDocumentEntitySlim"/>.
/// </summary>
@@ -111,52 +111,6 @@ namespace Umbraco.Core.Models.Entities
public virtual bool IsContainer { get; set; }
/// <summary>
/// Represents a lightweight property.
/// </summary>
public class PropertySlim
{
/// <summary>
/// Initializes a new instance of the <see cref="PropertySlim"/> class.
/// </summary>
public PropertySlim(string editorAlias, object value)
{
PropertyEditorAlias = editorAlias;
Value = value;
}
/// <summary>
/// Gets the property editor alias.
/// </summary>
public string PropertyEditorAlias { get; }
/// <summary>
/// Gets the property value.
/// </summary>
public object Value { get; }
protected bool Equals(PropertySlim other)
{
return PropertyEditorAlias.Equals(other.PropertyEditorAlias) && Equals(Value, other.Value);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((PropertySlim) obj);
}
public override int GetHashCode()
{
unchecked
{
return (PropertyEditorAlias.GetHashCode() * 397) ^ (Value != null ? Value.GetHashCode() : 0);
}
}
}
#region IDeepCloneable
/// <inheritdoc />
@@ -2,6 +2,7 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Represents a lightweight document entity, managed by the entity service.
/// </summary>
@@ -0,0 +1,14 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Represents a lightweight media entity, managed by the entity service.
/// </summary>
public interface IMediaEntitySlim : IContentEntitySlim
{
/// <summary>
/// The media file's path/url
/// </summary>
string MediaPath { get; }
}
}
@@ -0,0 +1,10 @@
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Implements <see cref="IMediaEntitySlim"/>.
/// </summary>
public class MediaEntitySlim : ContentEntitySlim, IMediaEntitySlim
{
public string MediaPath { get; set; }
}
}
@@ -10,22 +10,35 @@ namespace Umbraco.Core.Persistence
/// <summary>
/// Creates a new database.
/// </summary>
/// <remarks>The new database must be disposed after being used.</remarks>
/// <remarks>
/// <para>The new database must be disposed after being used.</para>
/// <para>Creating a database causes the factory to initialize if it is not already initialized.</para>
/// </remarks>
IUmbracoDatabase CreateDatabase();
/// <summary>
/// Gets a value indicating whether the database factory is configured.
/// Gets a value indicating whether the database factory is configured, i.e. whether
/// its connection string and provider name have been set. The factory may however not
/// be initialized (see <see cref="Initialized"/>).
/// </summary>
bool Configured { get; }
/// <summary>
/// Gets a value indicating whether the database factory is initialized, i.e. whether
/// its internal state is ready and it has been possible to connect to the database.
/// </summary>
bool Initialized { get; }
/// <summary>
/// Gets the connection string.
/// </summary>
/// <remarks>Throws if the factory is not configured.</remarks>
/// <remarks>May return <c>null</c> if the database factory is not configured.</remarks>
string ConnectionString { get; }
/// <summary>
/// Gets a value indicating whether the database can connect.
/// Gets a value indicating whether the database factory is configured (see <see cref="Configured"/>),
/// and it is possible to connect to the database. The factory may however not be initialized (see
/// <see cref="Initialized"/>).
/// </summary>
bool CanConnect { get; }
@@ -37,6 +50,9 @@ namespace Umbraco.Core.Persistence
/// <summary>
/// Gets the Sql context.
/// </summary>
/// <remarks>
/// <para>Getting the Sql context causes the factory to initialize if it is not already initialized.</para>
/// </remarks>
ISqlContext SqlContext { get; }
/// <summary>
@@ -232,6 +232,14 @@ namespace Umbraco.Core.Persistence
using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName })
using (var bulkReader = new PocoDataDataReader<T, SqlServerSyntaxProvider>(records, pocoData, syntax))
{
//we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared
//to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses
//the names instead of their ordering.
foreach(var col in bulkReader.ColumnMappings)
{
copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn);
}
copy.WriteToServer(bulkReader);
return bulkReader.RecordsAffected;
}
@@ -140,10 +140,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
while (templateDtoIx < templateDtos.Count && templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id)
{
var allowedDto = templateDtos[templateDtoIx];
templateDtoIx++;
if (!templates.TryGetValue(allowedDto.TemplateNodeId, out var template)) continue;
allowedTemplates.Add(template);
templateDtoIx++;
if (allowedDto.IsDefault)
defaultTemplateId = template.Id;
}
@@ -75,6 +75,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
dtos = page.Items;
totalRecords = page.TotalItems;
}
else if (isMedia)
{
var page = Database.Page<MediaEntityDto>(pageIndexToFetch, pageSize, sql);
dtos = page.Items;
totalRecords = page.TotalItems;
}
else
{
var page = Database.Page<BaseDto>(pageIndexToFetch, pageSize, sql);
@@ -87,10 +93,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (isContent)
BuildVariants(entities.Cast<DocumentEntitySlim>());
// TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media
if (isMedia)
BuildProperties(entities, dtos.ToList());
return entities;
}
@@ -112,14 +114,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0]));
}
var dto = Database.FirstOrDefault<BaseDto>(sql);
var dto = isMedia
? Database.FirstOrDefault<MediaEntityDto>(sql)
: Database.FirstOrDefault<BaseDto>(sql);
if (dto == null) return null;
var entity = BuildEntity(false, isMedia, dto);
if (isMedia)
BuildProperties(entity, dto);
return entity;
}
@@ -162,7 +164,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
: PerformGetAll(objectType);
}
private IEnumerable<IEntitySlim> GetEntities(Sql<ISqlContext> sql, bool isContent, bool isMedia, bool loadMediaProperties)
private IEnumerable<IEntitySlim> GetEntities(Sql<ISqlContext> sql, bool isContent, bool isMedia)
{
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
@@ -174,15 +176,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
: BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList();
}
var dtos = Database.Fetch<BaseDto>(sql);
if (dtos.Count == 0) return Enumerable.Empty<IEntitySlim>();
var dtos = isMedia
? (IEnumerable<BaseDto>)Database.Fetch<MediaEntityDto>(sql)
: Database.Fetch<BaseDto>(sql);
var entities = dtos.Select(x => BuildEntity(false, isMedia, x)).ToArray();
// TODO: See https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media
if (isMedia && loadMediaProperties)
BuildProperties(entities, dtos);
return entities;
}
@@ -192,7 +191,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var isMedia = objectType == Constants.ObjectTypes.Media;
var sql = GetFullSqlForEntityType(isContent, isMedia, objectType, filter);
return GetEntities(sql, isContent, isMedia, true);
return GetEntities(sql, isContent, isMedia);
}
public IEnumerable<TreeEntityPath> GetAllPaths(Guid objectType, params int[] ids)
@@ -238,22 +237,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
sql = translator.Translate();
sql = AddGroupBy(isContent, isMedia, sql, true);
return GetEntities(sql, isContent, isMedia, true);
}
// TODO: See https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media
internal IEnumerable<IEntitySlim> GetMediaByQueryWithoutPropertyData(IQuery<IUmbracoEntity> query)
{
var isContent = false;
var isMedia = true;
var sql = GetBaseWhere(isContent, isMedia, false, null, Constants.ObjectTypes.Media);
var translator = new SqlTranslator<IUmbracoEntity>(sql, query);
sql = translator.Translate();
sql = AddGroupBy(isContent, isMedia, sql, true);
return GetEntities(sql, isContent, isMedia, false);
return GetEntities(sql, isContent, isMedia);
}
public UmbracoObjectTypes GetObjectType(int id)
@@ -280,41 +264,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return Database.ExecuteScalar<int>(sql) > 0;
}
// TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media
private void BuildProperties(EntitySlim entity, BaseDto dto)
{
var pdtos = Database.Fetch<PropertyDataDto>(GetPropertyData(dto.VersionId));
foreach (var pdto in pdtos)
BuildProperty(entity, pdto);
}
// TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media
private void BuildProperties(EntitySlim[] entities, List<BaseDto> dtos)
{
var versionIds = dtos.Select(x => x.VersionId).Distinct().ToList();
var pdtos = Database.FetchByGroups<PropertyDataDto, int>(versionIds, 2000, GetPropertyData);
var xentity = entities.ToDictionary(x => x.Id, x => x); // nodeId -> entity
var xdto = dtos.ToDictionary(x => x.VersionId, x => x.NodeId); // versionId -> nodeId
foreach (var pdto in pdtos)
{
var nodeId = xdto[pdto.VersionId];
var entity = xentity[nodeId];
BuildProperty(entity, pdto);
}
}
// TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media
private void BuildProperty(EntitySlim entity, PropertyDataDto pdto)
{
// explain ?!
var value = string.IsNullOrWhiteSpace(pdto.TextValue)
? pdto.VarcharValue
: pdto.TextValue.ConvertToJsonIfPossible();
entity.AdditionalData[pdto.PropertyTypeDto.Alias] = new EntitySlim.PropertySlim(pdto.PropertyTypeDto.DataTypeDto.EditorAlias, value);
}
private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity)
=> BuildVariants(new[] { entity }).First();
@@ -400,31 +349,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return AddGroupBy(isContent, isMedia, sql, true);
}
private Sql<ISqlContext> GetPropertyData(int versionId)
{
return Sql()
.Select<PropertyDataDto>(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto)))
.From<PropertyDataDto>()
.InnerJoin<PropertyTypeDto>().On<PropertyDataDto, PropertyTypeDto>((left, right) => left.PropertyTypeId == right.Id)
.InnerJoin<DataTypeDto>().On<PropertyTypeDto, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId)
.Where<PropertyDataDto>(x => x.VersionId == versionId);
}
private Sql<ISqlContext> GetPropertyData(IEnumerable<int> versionIds)
{
return Sql()
.Select<PropertyDataDto>(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto)))
.From<PropertyDataDto>()
.InnerJoin<PropertyTypeDto>().On<PropertyDataDto, PropertyTypeDto>((left, right) => left.PropertyTypeId == right.Id)
.InnerJoin<DataTypeDto>().On<PropertyTypeDto, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId)
.WhereIn<PropertyDataDto>(x => x.VersionId, versionIds)
.OrderBy<PropertyDataDto>(x => x.VersionId);
}
// gets the base SELECT + FROM [+ filter] sql
// always from the 'current' content version
protected Sql<ISqlContext> GetBase(bool isContent, bool isMedia, Action<Sql<ISqlContext>> filter, bool isCount = false)
{
{
var sql = Sql();
if (isCount)
@@ -448,6 +376,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
sql
.AndSelect<DocumentDto>(x => x.Published, x => x.Edited);
}
if (isMedia)
{
sql
.AndSelect<MediaVersionDto>(x => Alias(x.Path, "MediaPath"));
}
}
sql
@@ -467,6 +401,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.InnerJoin<DocumentDto>().On<NodeDto, DocumentDto>((left, right) => left.NodeId == right.NodeId);
}
if (isMedia)
{
sql
.InnerJoin<MediaVersionDto>().On<ContentVersionDto, MediaVersionDto>((left, right) => left.Id == right.Id);
}
//Any LeftJoin statements need to come last
if (isCount == false)
{
@@ -536,6 +476,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.AndBy<DocumentDto>(x => x.Published, x => x.Edited);
}
if (isMedia)
{
sql
.AndBy<MediaVersionDto>(x => Alias(x.Path, "MediaPath"));
}
if (isContent || isMedia)
sql
@@ -566,23 +512,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
#region Classes
[ExplicitColumns]
internal class UmbracoPropertyDto
{
[Column("propertyEditorAlias")]
public string PropertyEditorAlias { get; set; }
[Column("propertyTypeAlias")]
public string PropertyAlias { get; set; }
[Column("varcharValue")]
public string VarcharValue { get; set; }
[Column("textValue")]
public string TextValue { get; set; }
}
/// <summary>
/// The DTO used to fetch results for a content item with its variation info
/// </summary>
@@ -594,6 +523,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
public bool Edited { get; set; }
}
private class MediaEntityDto : BaseDto
{
public string MediaPath { get; set; }
}
public class VariantInfoDto
{
public int NodeId { get; set; }
@@ -645,7 +579,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (isContent)
return BuildDocumentEntity(dto);
if (isMedia)
return BuildContentEntity(dto);
return BuildMediaEntity(dto);
// EntitySlim does not track changes
var entity = new EntitySlim();
@@ -678,11 +612,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
entity.ContentTypeThumbnail = dto.Thumbnail;
}
private static EntitySlim BuildContentEntity(BaseDto dto)
private MediaEntitySlim BuildMediaEntity(BaseDto dto)
{
// EntitySlim does not track changes
var entity = new ContentEntitySlim();
var entity = new MediaEntitySlim();
BuildContentEntity(entity, dto);
if (dto is MediaEntityDto contentDto)
{
// fill in the media info
entity.MediaPath = contentDto.MediaPath;
}
return entity;
}
@@ -99,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (uniqueing)
{
if (name.NumPos > 0 && name.Name.StartsWith(nodeName) && name.NumVal == uniqueNumber)
if (name.NumPos > 0 && name.Name.InvariantStartsWith(nodeName) && name.NumVal == uniqueNumber)
uniqueNumber++;
else
break;
@@ -411,8 +411,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return;
//now the user association
RemoveAllUsersFromGroup(entity.UserGroup.Id);
AddUsersToGroup(entity.UserGroup.Id, entity.UserIds);
RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds);
}
protected override void PersistUpdatedItem(UserGroupWithUsers entity)
@@ -424,8 +423,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return;
//now the user association
RemoveAllUsersFromGroup(entity.UserGroup.Id);
AddUsersToGroup(entity.UserGroup.Id, entity.UserIds);
RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds);
}
/// <summary>
/// Adds a set of users to a group, first removing any that exist
/// </summary>
/// <param name="groupId">Id of group</param>
/// <param name="userIds">Ids of users</param>
private void RefreshUsersInGroup(int groupId, int[] userIds)
{
RemoveAllUsersFromGroup(groupId);
AddUsersToGroup(groupId, userIds);
}
/// <summary>
@@ -444,7 +453,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
/// <param name="userIds">Ids of users</param>
private void AddUsersToGroup(int groupId, int[] userIds)
{
// TODO: Check if the user exists?
foreach (var userId in userIds)
{
var dto = new User2UserGroupDto
@@ -28,7 +28,8 @@ namespace Umbraco.Core.Persistence
{
private readonly Lazy<IMapperCollection> _mappers;
private readonly ILogger _logger;
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private object _lock = new object();
private DatabaseFactory _npocoDatabaseFactory;
private IPocoDataFactory _pocoDataFactory;
@@ -36,12 +37,13 @@ namespace Umbraco.Core.Persistence
private string _providerName;
private DbProviderFactory _dbProviderFactory;
private DatabaseType _databaseType;
private bool _serverVersionDetected;
private ISqlSyntaxProvider _sqlSyntax;
private RetryPolicy _connectionRetryPolicy;
private RetryPolicy _commandRetryPolicy;
private NPoco.MapperCollection _pocoMappers;
private SqlContext _sqlContext;
private bool _upgrading;
private bool _initialized;
#region Constructors
@@ -106,36 +108,30 @@ namespace Umbraco.Core.Persistence
#endregion
/// <inheritdoc />
public bool Configured { get; private set; }
/// <inheritdoc />
public string ConnectionString
public bool Configured
{
get
{
EnsureConfigured();
return _connectionString;
lock (_lock)
{
return !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace();
}
}
}
/// <inheritdoc />
public bool CanConnect
{
get
{
if (!Configured || !DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName)) return false;
public bool Initialized => Volatile.Read(ref _initialized);
if (_serverVersionDetected) return true;
/// <inheritdoc />
public string ConnectionString => _connectionString;
if (_databaseType.IsSqlServer())
DetectSqlServerVersion();
_serverVersionDetected = true;
/// <inheritdoc />
public bool CanConnect =>
// actually tries to connect to the database (regardless of configured/initialized)
!_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() &&
DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName);
return true;
}
}
private void DetectSqlServerVersion()
private void UpdateSqlServerDatabaseType()
{
// replace NPoco database type by a more efficient one
@@ -171,7 +167,15 @@ namespace Umbraco.Core.Persistence
}
/// <inheritdoc />
public ISqlContext SqlContext { get; private set; }
public ISqlContext SqlContext
{
get
{
// must be initialized to have a context
EnsureInitialized();
return _sqlContext;
}
}
/// <inheritdoc />
public void ConfigureForUpgrade()
@@ -182,63 +186,79 @@ namespace Umbraco.Core.Persistence
/// <inheritdoc />
public void Configure(string connectionString, string providerName)
{
try
if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString));
if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName));
lock (_lock)
{
_lock.EnterWriteLock();
_logger.Debug<UmbracoDatabaseFactory>("Configuring.");
if (Configured) throw new InvalidOperationException("Already configured.");
if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString));
if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName));
if (Volatile.Read(ref _initialized))
throw new InvalidOperationException("Already initialized.");
_connectionString = connectionString;
_providerName = providerName;
_connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString);
_commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString);
_dbProviderFactory = DbProviderFactories.GetFactory(_providerName);
if (_dbProviderFactory == null)
throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\".");
_databaseType = DatabaseType.Resolve(_dbProviderFactory.GetType().Name, _providerName);
if (_databaseType == null)
throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\".");
_sqlSyntax = GetSqlSyntaxProvider(_providerName);
if (_sqlSyntax == null)
throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\".");
// ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database
// so that everything NPoco is properly cached for the lifetime of the application
_pocoMappers = new NPoco.MapperCollection { new PocoMapper() };
var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver);
_pocoDataFactory = factory;
var config = new FluentConfig(xmappers => factory);
// create the database factory
_npocoDatabaseFactory = DatabaseFactory.Config(x => x
.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances
.WithFluentConfig(config)); // with proper configuration
if (_npocoDatabaseFactory == null) throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance.");
SqlContext = new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers);
_logger.Debug<UmbracoDatabaseFactory>("Configured.");
Configured = true;
}
finally
{
if (_lock.IsWriteLockHeld)
_lock.ExitWriteLock();
}
// rest to be lazy-initialized
}
private void EnsureInitialized()
{
LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize);
}
private SqlContext Initialize()
{
_logger.Debug<UmbracoDatabaseFactory>("Initializing.");
if (_connectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string.");
if (_providerName.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper provider name.");
// cannot initialize without being able to talk to the database
if (!DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName))
throw new Exception("Cannot connect to the database.");
_connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString);
_commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString);
_dbProviderFactory = DbProviderFactories.GetFactory(_providerName);
if (_dbProviderFactory == null)
throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\".");
_databaseType = DatabaseType.Resolve(_dbProviderFactory.GetType().Name, _providerName);
if (_databaseType == null)
throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\".");
_sqlSyntax = GetSqlSyntaxProvider(_providerName);
if (_sqlSyntax == null)
throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\".");
if (_databaseType.IsSqlServer())
UpdateSqlServerDatabaseType();
// ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database
// so that everything NPoco is properly cached for the lifetime of the application
_pocoMappers = new NPoco.MapperCollection { new PocoMapper() };
var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver);
_pocoDataFactory = factory;
var config = new FluentConfig(xmappers => factory);
// create the database factory
_npocoDatabaseFactory = DatabaseFactory.Config(x => x
.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances
.WithFluentConfig(config)); // with proper configuration
if (_npocoDatabaseFactory == null)
throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance.");
_logger.Debug<UmbracoDatabaseFactory>("Initialized.");
return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers);
}
/// <inheritdoc />
public IUmbracoDatabase CreateDatabase()
{
// must be initialized to create a database
EnsureInitialized();
return (IUmbracoDatabase) _npocoDatabaseFactory.GetDatabase();
}
@@ -260,22 +280,6 @@ namespace Umbraco.Core.Persistence
}
}
// ensures that the database is configured, else throws
private void EnsureConfigured()
{
_lock.EnterReadLock();
try
{
if (Configured == false)
throw new InvalidOperationException("Not configured.");
}
finally
{
if (_lock.IsReadLockHeld)
_lock.ExitReadLock();
}
}
// method used by NPoco's UmbracoDatabaseFactory to actually create the database instance
private UmbracoDatabase CreateDatabaseInstance()
{
@@ -292,7 +296,7 @@ namespace Umbraco.Core.Persistence
//var db = _umbracoDatabaseAccessor.UmbracoDatabase;
//_umbracoDatabaseAccessor.UmbracoDatabase = null;
//db?.Dispose();
Configured = false;
Volatile.Write(ref _initialized, false);
}
// during tests, the thread static var can leak between tests
@@ -2,9 +2,9 @@
namespace Umbraco.Core.Services.Changes
{
internal static class TreeChangeExtensions
public static class TreeChangeExtensions
{
public static TreeChange<TItem>.EventArgs ToEventArgs<TItem>(this IEnumerable<TreeChange<TItem>> changes)
internal static TreeChange<TItem>.EventArgs ToEventArgs<TItem>(this IEnumerable<TreeChange<TItem>> changes)
{
return new TreeChange<TItem>.EventArgs(changes);
}
+4 -56
View File
@@ -15,92 +15,40 @@ namespace Umbraco.Core.Services
/// <param name="id">The identifier of the entity.</param>
IEntitySlim Get(int id);
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="id">The identifier of the entity.</param>
/// <param name="full">A value indicating whether to load a light entity, or the full entity.</param>
/// <remarks>Returns either a <see cref="IEntitySlim"/>, or an actual entity, depending on <paramref name="full"/>.</remarks>
IUmbracoEntity Get(int id, bool full);
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="key">The unique key of the entity.</param>
IEntitySlim Get(Guid key);
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="key">The unique key of the entity.</param>
/// <param name="full">A value indicating whether to load a light entity, or the full entity.</param>
/// <remarks>Returns either a <see cref="IEntitySlim"/>, or an actual entity, depending on <paramref name="full"/>.</remarks>
IUmbracoEntity Get(Guid key, bool full);
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="id">The identifier of the entity.</param>
/// <param name="objectType">The object type of the entity.</param>
IEntitySlim Get(int id, UmbracoObjectTypes objectType);
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="id">The identifier of the entity.</param>
/// <param name="objectType">The object type of the entity.</param>
/// <param name="full">A value indicating whether to load a light entity, or the full entity.</param>
/// <remarks>Returns either a <see cref="IEntitySlim"/>, or an actual entity, depending on <paramref name="full"/>.</remarks>
IUmbracoEntity Get(int id, UmbracoObjectTypes objectType, bool full);
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="key">The unique key of the entity.</param>
/// <param name="objectType">The object type of the entity.</param>
IEntitySlim Get(Guid key, UmbracoObjectTypes objectType);
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="key">The unique key of the entity.</param>
/// <param name="objectType">The object type of the entity.</param>
/// <param name="full">A value indicating whether to load a light entity, or the full entity.</param>
/// <remarks>Returns either a <see cref="IEntitySlim"/>, or an actual entity, depending on <paramref name="full"/>.</remarks>
IUmbracoEntity Get(Guid key, UmbracoObjectTypes objectType, bool full);
/// <summary>
/// Gets an entity.
/// </summary>
/// <typeparam name="T">The type used to determine the object type of the entity.</typeparam>
/// <param name="id">The identifier of the entity.</param>
IEntitySlim Get<T>(int id) where T : IUmbracoEntity;
/// <summary>
/// Gets an entity.
/// </summary>
/// <typeparam name="T">The type used to determine the object type of the entity.</typeparam>
/// <param name="id">The identifier of the entity.</param>
/// <param name="full">A value indicating whether to load a light entity, or the full entity.</param>
/// <remarks>Returns either a <see cref="IEntitySlim"/>, or an actual entity, depending on <paramref name="full"/>.</remarks>
IUmbracoEntity Get<T>(int id, bool full) where T : IUmbracoEntity;
/// <summary>
/// Gets an entity.
/// </summary>
/// <typeparam name="T">The type used to determine the object type of the entity.</typeparam>
/// <param name="key">The unique key of the entity.</param>
IEntitySlim Get<T>(Guid key) where T : IUmbracoEntity;
/// <summary>
/// Gets an entity.
/// </summary>
/// <typeparam name="T">The type used to determine the object type of the entity.</typeparam>
/// <param name="key">The unique key of the entity.</param>
/// <param name="full">A value indicating whether to load a light entity, or the full entity.</param>
/// <remarks>Returns either a <see cref="IEntitySlim"/>, or an actual entity, depending on <paramref name="full"/>.</remarks>
IUmbracoEntity Get<T>(Guid key, bool full) where T : IUmbracoEntity;
/// <summary>
/// Determines whether an entity exists.
/// </summary>
+6 -15
View File
@@ -142,52 +142,43 @@ namespace Umbraco.Core.Services
/// Gets the Child object from a Relation as an <see cref="IUmbracoEntity"/>
/// </summary>
/// <param name="relation">Relation to retrieve child object from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An <see cref="IUmbracoEntity"/></returns>
IUmbracoEntity GetChildEntityFromRelation(IRelation relation, bool loadBaseType = false);
IUmbracoEntity GetChildEntityFromRelation(IRelation relation);
/// <summary>
/// Gets the Parent object from a Relation as an <see cref="IUmbracoEntity"/>
/// </summary>
/// <param name="relation">Relation to retrieve parent object from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An <see cref="IUmbracoEntity"/></returns>
IUmbracoEntity GetParentEntityFromRelation(IRelation relation, bool loadBaseType = false);
IUmbracoEntity GetParentEntityFromRelation(IRelation relation);
/// <summary>
/// Gets the Parent and Child objects from a Relation as a <see cref="Tuple"/>"/> with <see cref="IUmbracoEntity"/>.
/// </summary>
/// <param name="relation">Relation to retrieve parent and child object from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>Returns a Tuple with Parent (item1) and Child (item2)</returns>
Tuple<IUmbracoEntity, IUmbracoEntity> GetEntitiesFromRelation(IRelation relation, bool loadBaseType = false);
Tuple<IUmbracoEntity, IUmbracoEntity> GetEntitiesFromRelation(IRelation relation);
/// <summary>
/// Gets the Child objects from a list of Relations as a list of <see cref="IUmbracoEntity"/> objects.
/// </summary>
/// <param name="relations">List of relations to retrieve child objects from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An enumerable list of <see cref="IUmbracoEntity"/></returns>
IEnumerable<IUmbracoEntity> GetChildEntitiesFromRelations(IEnumerable<IRelation> relations, bool loadBaseType = false);
IEnumerable<IUmbracoEntity> GetChildEntitiesFromRelations(IEnumerable<IRelation> relations);
/// <summary>
/// Gets the Parent objects from a list of Relations as a list of <see cref="IUmbracoEntity"/> objects.
/// </summary>
/// <param name="relations">List of relations to retrieve parent objects from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An enumerable list of <see cref="IUmbracoEntity"/></returns>
IEnumerable<IUmbracoEntity> GetParentEntitiesFromRelations(IEnumerable<IRelation> relations,
bool loadBaseType = false);
IEnumerable<IUmbracoEntity> GetParentEntitiesFromRelations(IEnumerable<IRelation> relations);
/// <summary>
/// Gets the Parent and Child objects from a list of Relations as a list of <see cref="IUmbracoEntity"/> objects.
/// </summary>
/// <param name="relations">List of relations to retrieve parent and child objects from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An enumerable list of <see cref="Tuple"/> with <see cref="IUmbracoEntity"/></returns>
IEnumerable<Tuple<IUmbracoEntity, IUmbracoEntity>> GetEntitiesFromRelations(
IEnumerable<IRelation> relations,
bool loadBaseType = false);
IEnumerable<Tuple<IUmbracoEntity, IUmbracoEntity>> GetEntitiesFromRelations(IEnumerable<IRelation> relations);
/// <summary>
/// Relates two objects by their entity Ids.
@@ -7,11 +7,9 @@ using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.Repositories.Implement;
using Umbraco.Core.Scoping;
namespace Umbraco.Core.Services.Implement
@@ -19,30 +17,25 @@ namespace Umbraco.Core.Services.Implement
public class EntityService : ScopeRepositoryService, IEntityService
{
private readonly IEntityRepository _entityRepository;
private readonly Dictionary<string, (UmbracoObjectTypes ObjectType, Func<int, IUmbracoEntity> GetById, Func<Guid, IUmbracoEntity> GetByKey)> _objectTypes;
private readonly Dictionary<string, UmbracoObjectTypes> _objectTypes;
private IQuery<IUmbracoEntity> _queryRootEntity;
private readonly IdkMap _idkMap;
public EntityService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory,
IContentService contentService, IContentTypeService contentTypeService,
IMediaService mediaService, IMediaTypeService mediaTypeService,
IDataTypeService dataTypeService,
IMemberService memberService, IMemberTypeService memberTypeService, IdkMap idkMap,
IEntityRepository entityRepository)
public EntityService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IdkMap idkMap, IEntityRepository entityRepository)
: base(provider, logger, eventMessagesFactory)
{
_idkMap = idkMap;
_entityRepository = entityRepository;
_objectTypes = new Dictionary<string, (UmbracoObjectTypes, Func<int, IUmbracoEntity>, Func<Guid, IUmbracoEntity>)>
_objectTypes = new Dictionary<string, UmbracoObjectTypes>
{
{ typeof (IDataType).FullName, (UmbracoObjectTypes.DataType, dataTypeService.GetDataType, dataTypeService.GetDataType) },
{ typeof (IContent).FullName, (UmbracoObjectTypes.Document, contentService.GetById, contentService.GetById) },
{ typeof (IContentType).FullName, (UmbracoObjectTypes.DocumentType, contentTypeService.Get, contentTypeService.Get) },
{ typeof (IMedia).FullName, (UmbracoObjectTypes.Media, mediaService.GetById, mediaService.GetById) },
{ typeof (IMediaType).FullName, (UmbracoObjectTypes.MediaType, mediaTypeService.Get, mediaTypeService.Get) },
{ typeof (IMember).FullName, (UmbracoObjectTypes.Member, memberService.GetById, memberService.GetByKey) },
{ typeof (IMemberType).FullName, (UmbracoObjectTypes.MemberType, memberTypeService.Get, memberTypeService.Get) },
{ typeof (IDataType).FullName, UmbracoObjectTypes.DataType },
{ typeof (IContent).FullName, UmbracoObjectTypes.Document },
{ typeof (IContentType).FullName, UmbracoObjectTypes.DocumentType },
{ typeof (IMedia).FullName, UmbracoObjectTypes.Media },
{ typeof (IMediaType).FullName, UmbracoObjectTypes.MediaType },
{ typeof (IMember).FullName, UmbracoObjectTypes.Member },
{ typeof (IMemberType).FullName, UmbracoObjectTypes.MemberType },
};
}
@@ -54,162 +47,68 @@ namespace Umbraco.Core.Services.Implement
#endregion
// gets the getters, throws if not supported
private (UmbracoObjectTypes ObjectType, Func<int, IUmbracoEntity> GetById, Func<Guid, IUmbracoEntity> GetByKey) GetGetters(Type type)
// gets the object type, throws if not supported
private UmbracoObjectTypes GetObjectType(Type type)
{
if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out var getters))
if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out var objType))
throw new NotSupportedException($"Type \"{type?.FullName ?? "<null>"}\" is not supported here.");
return getters;
return objType;
}
/// <inheritdoc />
public IEntitySlim Get(int id)
{
return (IEntitySlim) Get(id, false);
}
/// <inheritdoc />
public IUmbracoEntity Get(int id, bool full)
{
if (!full)
using (ScopeProvider.CreateScope(autoComplete: true))
{
// get the light entity
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _entityRepository.Get(id);
}
return _entityRepository.Get(id);
}
// get the full entity
var objectType = GetObjectType(id);
var entityType = objectType.GetClrType();
var getters = GetGetters(entityType);
return getters.GetById(id);
}
/// <inheritdoc />
public IEntitySlim Get(Guid key)
{
return (IEntitySlim) Get(key, false);
}
/// <inheritdoc />
public IUmbracoEntity Get(Guid key, bool full)
{
if (!full)
using (ScopeProvider.CreateScope(autoComplete: true))
{
// get the light entity
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _entityRepository.Get(key);
}
return _entityRepository.Get(key);
}
// get the full entity
var objectType = GetObjectType(key);
var entityType = objectType.GetClrType();
var getters = GetGetters(entityType);
return getters.GetByKey(key);
}
/// <inheritdoc />
public virtual IEntitySlim Get(int id, UmbracoObjectTypes objectType)
{
return (IEntitySlim) Get(id, objectType, false);
}
/// <inheritdoc />
public virtual IUmbracoEntity Get(int id, UmbracoObjectTypes objectType, bool full)
{
if (!full)
using (ScopeProvider.CreateScope(autoComplete: true))
{
// get the light entity
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _entityRepository.Get(id, objectType.GetGuid());
}
return _entityRepository.Get(id, objectType.GetGuid());
}
// get the full entity
var entityType = objectType.GetClrType();
var getters = GetGetters(entityType);
return getters.GetById(id);
}
/// <inheritdoc />
public IEntitySlim Get(Guid key, UmbracoObjectTypes objectType)
{
return (IEntitySlim) Get(key, objectType, false);
}
/// <inheritdoc />
public IUmbracoEntity Get(Guid key, UmbracoObjectTypes objectType, bool full)
{
if (!full)
using (ScopeProvider.CreateScope(autoComplete: true))
{
// get the light entity
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _entityRepository.Get(key, objectType.GetGuid());
}
return _entityRepository.Get(key, objectType.GetGuid());
}
// get the full entity
var entityType = objectType.GetClrType();
var getters = GetGetters(entityType);
return getters.GetByKey(key);
}
/// <inheritdoc />
public virtual IEntitySlim Get<T>(int id)
where T : IUmbracoEntity
{
return (IEntitySlim) Get<T>(id, false);
}
/// <inheritdoc />
public virtual IUmbracoEntity Get<T>(int id, bool full)
where T : IUmbracoEntity
{
if (!full)
using (ScopeProvider.CreateScope(autoComplete: true))
{
// get the light entity
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _entityRepository.Get(id);
}
return _entityRepository.Get(id);
}
// get the full entity
var entityType = typeof (T);
var getters = GetGetters(entityType);
return getters.GetById(id);
}
/// <inheritdoc />
public virtual IEntitySlim Get<T>(Guid key)
where T : IUmbracoEntity
{
return (IEntitySlim) Get<T>(key, false);
}
/// <inheritdoc />
public IUmbracoEntity Get<T>(Guid key, bool full)
where T : IUmbracoEntity
{
if (!full)
using (ScopeProvider.CreateScope(autoComplete: true))
{
// get the light entity
using (ScopeProvider.CreateScope(autoComplete: true))
{
return _entityRepository.Get(key);
}
return _entityRepository.Get(key);
}
// get the full entity
var entityType = typeof (T);
var getters = GetGetters(entityType);
return getters.GetByKey(key);
}
/// <inheritdoc />
@@ -240,8 +139,7 @@ namespace Umbraco.Core.Services.Implement
where T : IUmbracoEntity
{
var entityType = typeof (T);
var getters = GetGetters(entityType);
var objectType = getters.ObjectType;
var objectType = GetObjectType(entityType);
var objectTypeId = objectType.GetGuid();
using (ScopeProvider.CreateScope(autoComplete: true))
@@ -261,7 +159,7 @@ namespace Umbraco.Core.Services.Implement
if (entityType == null)
throw new NotSupportedException($"Type \"{objectType}\" is not supported here.");
GetGetters(entityType);
GetObjectType(entityType);
using (ScopeProvider.CreateScope(autoComplete: true))
{
@@ -277,7 +175,7 @@ namespace Umbraco.Core.Services.Implement
public virtual IEnumerable<IEntitySlim> GetAll(Guid objectType, params int[] ids)
{
var entityType = ObjectTypes.GetClrType(objectType);
GetGetters(entityType);
GetObjectType(entityType);
using (ScopeProvider.CreateScope(autoComplete: true))
{
@@ -290,8 +188,7 @@ namespace Umbraco.Core.Services.Implement
where T : IUmbracoEntity
{
var entityType = typeof (T);
var getters = GetGetters(entityType);
var objectType = getters.ObjectType;
var objectType = GetObjectType(entityType);
var objectTypeId = objectType.GetGuid();
using (ScopeProvider.CreateScope(autoComplete: true))
@@ -304,7 +201,7 @@ namespace Umbraco.Core.Services.Implement
public IEnumerable<IEntitySlim> GetAll(UmbracoObjectTypes objectType, Guid[] keys)
{
var entityType = objectType.GetClrType();
GetGetters(entityType);
GetObjectType(entityType);
using (ScopeProvider.CreateScope(autoComplete: true))
{
@@ -316,7 +213,7 @@ namespace Umbraco.Core.Services.Implement
public virtual IEnumerable<IEntitySlim> GetAll(Guid objectType, params Guid[] keys)
{
var entityType = ObjectTypes.GetClrType(objectType);
GetGetters(entityType);
GetObjectType(entityType);
using (ScopeProvider.CreateScope(autoComplete: true))
{
@@ -377,22 +274,6 @@ namespace Umbraco.Core.Services.Implement
}
}
/// <summary>
/// Gets a collection of children by the parent's Id and UmbracoObjectType without adding property data
/// </summary>
/// <param name="parentId">Id of the parent to retrieve children for</param>
/// <returns>An enumerable list of <see cref="IUmbracoEntity"/> objects</returns>
internal IEnumerable<IEntitySlim> GetMediaChildrenWithoutPropertyData(int parentId)
{
using (ScopeProvider.CreateScope(autoComplete: true))
{
var query = Query<IUmbracoEntity>().Where(x => x.ParentId == parentId);
// TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media
return ((EntityRepository)_entityRepository).GetMediaByQueryWithoutPropertyData(query);
}
}
/// <inheritdoc />
public virtual IEnumerable<IEntitySlim> GetDescendants(int id)
{
@@ -578,7 +459,7 @@ namespace Umbraco.Core.Services.Implement
public virtual IEnumerable<TreeEntityPath> GetAllPaths(UmbracoObjectTypes objectType, params int[] ids)
{
var entityType = objectType.GetClrType();
GetGetters(entityType);
GetObjectType(entityType);
using (ScopeProvider.CreateScope(autoComplete: true))
{
@@ -590,7 +471,7 @@ namespace Umbraco.Core.Services.Implement
public virtual IEnumerable<TreeEntityPath> GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys)
{
var entityType = objectType.GetClrType();
GetGetters(entityType);
GetObjectType(entityType);
using (ScopeProvider.CreateScope(autoComplete: true))
{
@@ -285,39 +285,36 @@ namespace Umbraco.Core.Services.Implement
/// Gets the Child object from a Relation as an <see cref="IUmbracoEntity"/>
/// </summary>
/// <param name="relation">Relation to retrieve child object from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An <see cref="IUmbracoEntity"/></returns>
public IUmbracoEntity GetChildEntityFromRelation(IRelation relation, bool loadBaseType = false)
public IUmbracoEntity GetChildEntityFromRelation(IRelation relation)
{
var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType);
return _entityService.Get(relation.ChildId, objectType, loadBaseType);
return _entityService.Get(relation.ChildId, objectType);
}
/// <summary>
/// Gets the Parent object from a Relation as an <see cref="IUmbracoEntity"/>
/// </summary>
/// <param name="relation">Relation to retrieve parent object from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An <see cref="IUmbracoEntity"/></returns>
public IUmbracoEntity GetParentEntityFromRelation(IRelation relation, bool loadBaseType = false)
public IUmbracoEntity GetParentEntityFromRelation(IRelation relation)
{
var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType);
return _entityService.Get(relation.ParentId, objectType, loadBaseType);
return _entityService.Get(relation.ParentId, objectType);
}
/// <summary>
/// Gets the Parent and Child objects from a Relation as a <see cref="Tuple"/>"/> with <see cref="IUmbracoEntity"/>.
/// </summary>
/// <param name="relation">Relation to retrieve parent and child object from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>Returns a Tuple with Parent (item1) and Child (item2)</returns>
public Tuple<IUmbracoEntity, IUmbracoEntity> GetEntitiesFromRelation(IRelation relation, bool loadBaseType = false)
public Tuple<IUmbracoEntity, IUmbracoEntity> GetEntitiesFromRelation(IRelation relation)
{
var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType);
var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType);
var child = _entityService.Get(relation.ChildId, childObjectType, loadBaseType);
var parent = _entityService.Get(relation.ParentId, parentObjectType, loadBaseType);
var child = _entityService.Get(relation.ChildId, childObjectType);
var parent = _entityService.Get(relation.ParentId, parentObjectType);
return new Tuple<IUmbracoEntity, IUmbracoEntity>(parent, child);
}
@@ -326,14 +323,13 @@ namespace Umbraco.Core.Services.Implement
/// Gets the Child objects from a list of Relations as a list of <see cref="IUmbracoEntity"/> objects.
/// </summary>
/// <param name="relations">List of relations to retrieve child objects from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An enumerable list of <see cref="IUmbracoEntity"/></returns>
public IEnumerable<IUmbracoEntity> GetChildEntitiesFromRelations(IEnumerable<IRelation> relations, bool loadBaseType = false)
public IEnumerable<IUmbracoEntity> GetChildEntitiesFromRelations(IEnumerable<IRelation> relations)
{
foreach (var relation in relations)
{
var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType);
yield return _entityService.Get(relation.ChildId, objectType, loadBaseType);
yield return _entityService.Get(relation.ChildId, objectType);
}
}
@@ -341,14 +337,13 @@ namespace Umbraco.Core.Services.Implement
/// Gets the Parent objects from a list of Relations as a list of <see cref="IUmbracoEntity"/> objects.
/// </summary>
/// <param name="relations">List of relations to retrieve parent objects from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An enumerable list of <see cref="IUmbracoEntity"/></returns>
public IEnumerable<IUmbracoEntity> GetParentEntitiesFromRelations(IEnumerable<IRelation> relations, bool loadBaseType = false)
public IEnumerable<IUmbracoEntity> GetParentEntitiesFromRelations(IEnumerable<IRelation> relations)
{
foreach (var relation in relations)
{
var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType);
yield return _entityService.Get(relation.ParentId, objectType, loadBaseType);
yield return _entityService.Get(relation.ParentId, objectType);
}
}
@@ -356,17 +351,16 @@ namespace Umbraco.Core.Services.Implement
/// Gets the Parent and Child objects from a list of Relations as a list of <see cref="IUmbracoEntity"/> objects.
/// </summary>
/// <param name="relations">List of relations to retrieve parent and child objects from</param>
/// <param name="loadBaseType">Optional bool to load the complete object graph when set to <c>False</c></param>
/// <returns>An enumerable list of <see cref="Tuple"/> with <see cref="IUmbracoEntity"/></returns>
public IEnumerable<Tuple<IUmbracoEntity, IUmbracoEntity>> GetEntitiesFromRelations(IEnumerable<IRelation> relations, bool loadBaseType = false)
public IEnumerable<Tuple<IUmbracoEntity, IUmbracoEntity>> GetEntitiesFromRelations(IEnumerable<IRelation> relations)
{
foreach (var relation in relations)
{
var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType);
var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType);
var child = _entityService.Get(relation.ChildId, childObjectType, loadBaseType);
var parent = _entityService.Get(relation.ParentId, parentObjectType, loadBaseType);
var child = _entityService.Get(relation.ChildId, childObjectType);
var parent = _entityService.Get(relation.ParentId, parentObjectType);
yield return new Tuple<IUmbracoEntity, IUmbracoEntity>(parent, child);
}
+3
View File
@@ -218,6 +218,7 @@
<Compile Include="Composing\TypeHelper.cs" />
<Compile Include="Composing\TypeLoader.cs" />
<Compile Include="IO\MediaPathSchemes\UniqueMediaPathScheme.cs" />
<Compile Include="Logging\Viewer\LogTimePeriod.cs" />
<Compile Include="Mapping\MapperContext.cs" />
<Compile Include="Migrations\Upgrade\Common\DeleteKeysAndIndexes.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\DataTypes\ContentPickerPreValueMigrator.cs" />
@@ -249,6 +250,8 @@
<Compile Include="Migrations\Upgrade\V_8_1_0\ConvertTinyMceAndGridMediaUrlsToLocalLink.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\FixLanguageIsoCodeLength.cs" />
<Compile Include="Models\CultureImpact.cs" />
<Compile Include="Models\Entities\IMediaEntitySlim.cs" />
<Compile Include="Models\Entities\MediaEntitySlim.cs" />
<Compile Include="Models\PublishedContent\ILivePublishedModelFactory.cs" />
<Compile Include="Models\PublishedContent\IPublishedContentType.cs" />
<Compile Include="Models\PublishedContent\IPublishedPropertyType.cs" />
+17 -16
View File
@@ -23,9 +23,10 @@ namespace Umbraco.Tests.Logging
private string _newSearchfilePath;
private string _newSearchfileDirPath;
private DateTimeOffset _startDate = new DateTime(year: 2018, month: 11, day: 12, hour:0, minute:0, second:0);
private DateTimeOffset _endDate = new DateTime(year: 2018, month: 11, day: 13, hour: 0, minute: 0, second: 0);
private LogTimePeriod _logTimePeriod = new LogTimePeriod(
new DateTime(year: 2018, month: 11, day: 12, hour:0, minute:0, second:0),
new DateTime(year: 2018, month: 11, day: 13, hour: 0, minute: 0, second: 0)
);
[OneTimeSetUp]
public void Setup()
{
@@ -67,7 +68,7 @@ namespace Umbraco.Tests.Logging
[Test]
public void Logs_Contain_Correct_Error_Count()
{
var numberOfErrors = _logViewer.GetNumberOfErrors(startDate: _startDate, endDate: _endDate);
var numberOfErrors = _logViewer.GetNumberOfErrors(_logTimePeriod);
//Our dummy log should contain 2 errors
Assert.AreEqual(2, numberOfErrors);
@@ -76,8 +77,8 @@ namespace Umbraco.Tests.Logging
[Test]
public void Logs_Contain_Correct_Log_Level_Counts()
{
var logCounts = _logViewer.GetLogLevelCounts(startDate: _startDate, endDate: _endDate);
var logCounts = _logViewer.GetLogLevelCounts(_logTimePeriod);
Assert.AreEqual(1954, logCounts.Debug);
Assert.AreEqual(2, logCounts.Error);
Assert.AreEqual(0, logCounts.Fatal);
@@ -88,7 +89,7 @@ namespace Umbraco.Tests.Logging
[Test]
public void Logs_Contains_Correct_Message_Templates()
{
var templates = _logViewer.GetMessageTemplates(startDate: _startDate, endDate: _endDate);
var templates = _logViewer.GetMessageTemplates(_logTimePeriod);
//Count no of templates
Assert.AreEqual(43, templates.Count());
@@ -112,7 +113,7 @@ namespace Umbraco.Tests.Logging
{
//We are just testing a return value (as we know the example file is less than 200MB)
//But this test method does not test/check that
var canOpenLogs = _logViewer.CheckCanOpenLogs(startDate: _startDate, endDate: _endDate);
var canOpenLogs = _logViewer.CheckCanOpenLogs(_logTimePeriod);
Assert.IsTrue(canOpenLogs);
}
@@ -120,7 +121,7 @@ namespace Umbraco.Tests.Logging
public void Logs_Can_Be_Queried()
{
//Should get me the most 100 recent log entries & using default overloads for remaining params
var allLogs = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1);
var allLogs = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1);
//Check we get 100 results back for a page & total items all good :)
Assert.AreEqual(100, allLogs.Items.Count());
@@ -138,7 +139,7 @@ namespace Umbraco.Tests.Logging
//Check we call method again with a smaller set of results & in ascending
var smallQuery = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, pageSize: 10, orderDirection: Direction.Ascending);
var smallQuery = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, pageSize: 10, orderDirection: Direction.Ascending);
Assert.AreEqual(10, smallQuery.Items.Count());
Assert.AreEqual(241, smallQuery.TotalPages);
@@ -152,17 +153,17 @@ namespace Umbraco.Tests.Logging
//Check invalid log levels
//Rather than expect 0 items - get all items back & ignore the invalid levels
string[] invalidLogLevels = { "Invalid", "NotALevel" };
var queryWithInvalidLevels = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, logLevels: invalidLogLevels);
var queryWithInvalidLevels = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, logLevels: invalidLogLevels);
Assert.AreEqual(2410, queryWithInvalidLevels.TotalItems);
//Check we can call method with an array of logLevel (error & warning)
string [] logLevels = { "Warning", "Error" };
var queryWithLevels = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, logLevels: logLevels);
var queryWithLevels = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, logLevels: logLevels);
Assert.AreEqual(9, queryWithLevels.TotalItems);
//Query @Level='Warning' BUT we pass in array of LogLevels for Debug & Info (Expect to get 0 results)
string[] logLevelMismatch = { "Debug", "Information" };
var filterLevelQuery = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, filterExpression: "@Level='Warning'", logLevels: logLevelMismatch); ;
var filterLevelQuery = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, filterExpression: "@Level='Warning'", logLevels: logLevelMismatch); ;
Assert.AreEqual(0, filterLevelQuery.TotalItems);
}
@@ -177,10 +178,10 @@ namespace Umbraco.Tests.Logging
[Test]
public void Logs_Can_Query_With_Expressions(string queryToVerify, int expectedCount)
{
var testQuery = _logViewer.GetLogs(startDate: _startDate, endDate: _endDate, pageNumber: 1, filterExpression: queryToVerify);
var testQuery = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, filterExpression: queryToVerify);
Assert.AreEqual(expectedCount, testQuery.TotalItems);
}
[Test]
public void Log_Search_Can_Persist()
{
+1 -3
View File
@@ -37,9 +37,7 @@ namespace Umbraco.Tests.Models
};
item.AdditionalData.Add("test1", 3);
item.AdditionalData.Add("test2", "valuie");
item.AdditionalData.Add("test3", new EntitySlim.PropertySlim("TestPropertyEditor", "test"));
item.AdditionalData.Add("test4", new EntitySlim.PropertySlim("TestPropertyEditor2", "test2"));
var result = ss.ToStream(item);
var json = result.ResultStream.ToJsonString();
Debug.Print(json); // FIXME: compare with v7
@@ -43,16 +43,6 @@ namespace Umbraco.Tests.Persistence
_databaseFactory = null;
}
[Test]
public void GetDatabaseType()
{
using (var database = _databaseFactory.CreateDatabase())
{
var databaseType = database.DatabaseType;
Assert.AreEqual(DatabaseType.SQLCe, databaseType);
}
}
[Test]
public void CreateDatabase() // FIXME: move to DatabaseBuilderTest!
{
@@ -79,6 +69,13 @@ namespace Umbraco.Tests.Persistence
// re-create the database factory and database context with proper connection string
_databaseFactory = new UmbracoDatabaseFactory(connString, Constants.DbProviderNames.SqlCe, _logger, new Lazy<IMapperCollection>(() => Mock.Of<IMapperCollection>()));
// test get database type (requires an actual database)
using (var database = _databaseFactory.CreateDatabase())
{
var databaseType = database.DatabaseType;
Assert.AreEqual(DatabaseType.SQLCe, databaseType);
}
// create application context
//var appCtx = new ApplicationContext(
// _databaseFactory,
@@ -36,6 +36,8 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.IsTrue(result > 0, "Expected >0 but was " + result);
}
[Test]
public void OrderByTest()
{
@@ -75,6 +77,7 @@ namespace Umbraco.Tests.Persistence.Repositories
[TestCase(0, "Alpha", "Alpha (3)")]
[TestCase(0, "Kilo (1)", "Kilo (1) (1)")] // though... we might consider "Kilo (2)"
[TestCase(6, "Kilo (1)", "Kilo (1)")] // because of the id
[TestCase(0, "alpha", "alpha (3)")]
[TestCase(0, "", " (1)")]
[TestCase(0, null, " (1)")]
public void Test(int nodeId, string nodeName, string expected)
@@ -95,5 +98,22 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.AreEqual(expected, SimilarNodeName.GetUniqueName(names, nodeId, nodeName));
}
[Test]
[Explicit("This test fails! We need to fix up the logic")]
public void TestMany()
{
var names = new[]
{
new SimilarNodeName { Id = 1, Name = "Alpha (2)" },
new SimilarNodeName { Id = 2, Name = "Test" },
new SimilarNodeName { Id = 3, Name = "Test (1)" },
new SimilarNodeName { Id = 4, Name = "Test (2)" },
new SimilarNodeName { Id = 22, Name = "Test (1) (1)" },
};
//fixme - this will yield "Test (2)" which is already in use
Assert.AreEqual("Test (3)", SimilarNodeName.GetUniqueName(names, 0, "Test"));
}
}
}
@@ -675,15 +675,10 @@ namespace Umbraco.Tests.Services
foreach (var entity in entities)
{
Console.WriteLine();
foreach (var data in entity.AdditionalData)
{
Console.WriteLine($"{entity.Id} {data.Key} {data.Value} {(data.Value is EntitySlim.PropertySlim p ? p.PropertyEditorAlias : "")}");
}
Assert.IsTrue(entity.GetType().Implements<IMediaEntitySlim>());
Console.WriteLine(((IMediaEntitySlim)entity).MediaPath);
Assert.IsNotEmpty(((IMediaEntitySlim)entity).MediaPath);
}
Assert.That(entities.Any(x =>
x.AdditionalData.Any(y => y.Value is EntitySlim.PropertySlim && ((EntitySlim.PropertySlim)y.Value).PropertyEditorAlias == Constants.PropertyEditors.Aliases.UploadField)), Is.True);
}
[Test]
+1 -5
View File
@@ -164,11 +164,7 @@ namespace Umbraco.Tests.TestHelpers
var fileService = GetLazyService<IFileService>(factory, c => new FileService(scopeProvider, logger, eventMessagesFactory, GetRepo<IStylesheetRepository>(c), GetRepo<IScriptRepository>(c), GetRepo<ITemplateRepository>(c), GetRepo<IPartialViewRepository>(c), GetRepo<IPartialViewMacroRepository>(c), GetRepo<IAuditRepository>(c)));
var memberTypeService = GetLazyService<IMemberTypeService>(factory, c => new MemberTypeService(scopeProvider, logger, eventMessagesFactory, memberService.Value, GetRepo<IMemberTypeRepository>(c), GetRepo<IAuditRepository>(c), GetRepo<IEntityRepository>(c)));
var entityService = GetLazyService<IEntityService>(factory, c => new EntityService(
scopeProvider, logger, eventMessagesFactory,
contentService.Value, contentTypeService.Value, mediaService.Value, mediaTypeService.Value, dataTypeService.Value, memberService.Value, memberTypeService.Value,
idkMap,
GetRepo<IEntityRepository>(c)));
var entityService = GetLazyService<IEntityService>(factory, c => new EntityService(scopeProvider, logger, eventMessagesFactory, idkMap, GetRepo<IEntityRepository>(c)));
var macroService = GetLazyService<IMacroService>(factory, c => new MacroService(scopeProvider, logger, eventMessagesFactory, GetRepo<IMacroRepository>(c), GetRepo<IAuditRepository>(c)));
var packagingService = GetLazyService<IPackagingService>(factory, c =>
+6 -6
View File
@@ -41,12 +41,12 @@ module.exports = {
assets: "./src/assets/**"
}
},
root: "../Umbraco.Web.UI/Umbraco/",
root: "../Umbraco.Web.UI/",
targets: {
js: "js/",
lib: "lib/",
views: "views/",
css: "assets/css/",
assets: "assets/"
js: "Umbraco/js/",
lib: "Umbraco/lib/",
views: "Umbraco/views/",
css: "Umbraco/assets/css/",
assets: "Umbraco/assets/"
}
};
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,7 @@
var evts = [];
var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode;
var watchingCulture = false;
//setup scope vars
$scope.defaultButton = null;
@@ -26,26 +27,58 @@
$scope.allowOpen = true;
$scope.app = null;
//initializes any watches
function startWatches(content) {
//watch for changes to isNew & the content.id, set the page.isNew accordingly and load the breadcrumb if we can
$scope.$watchGroup(['isNew', 'content.id'], function (newVal, oldVal) {
var contentId = newVal[1];
$scope.page.isNew = Object.toBoolean(newVal[0]);
//We fetch all ancestors of the node to generate the footer breadcrumb navigation
if (!$scope.page.isNew && contentId && content.parentId && content.parentId !== -1) {
loadBreadcrumb();
if (!watchingCulture) {
$scope.$watch('culture',
function (value, oldValue) {
if (value !== oldValue) {
loadBreadcrumb();
}
});
}
}
});
}
//this initializes the editor with the data which will be called more than once if the data is re-loaded
function init() {
var content = $scope.content;
if (content.id && content.isChildOfListView && content.trashed === false) {
$scope.page.listViewPath = ($routeParams.page) ?
"/content/content/edit/" + content.parentId + "?page=" + $routeParams.page :
"/content/content/edit/" + content.parentId;
}
// we need to check wether an app is present in the current data, if not we will present the default app.
var isAppPresent = false;
// on first init, we dont have any apps. but if we are re-initializing, we do, but ...
if ($scope.app) {
// lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.)
_.forEach(content.apps, function(app) {
_.forEach(content.apps, function (app) {
if (app === $scope.app) {
isAppPresent = true;
}
});
// if we did reload our DocType, but still have the same app we will try to find it by the alias.
if (isAppPresent === false) {
_.forEach(content.apps, function(app) {
_.forEach(content.apps, function (app) {
if (app.alias === $scope.app.alias) {
isAppPresent = true;
app.active = true;
@@ -53,7 +86,7 @@
}
});
}
}
// if we still dont have a app, lets show the first one:
@@ -61,21 +94,8 @@
content.apps[0].active = true;
$scope.appChanged(content.apps[0]);
}
editorState.set(content);
//We fetch all ancestors of the node to generate the footer breadcrumb navigation
if (!$scope.page.isNew) {
if (content.parentId && content.parentId !== -1) {
loadBreadcrumb();
$scope.$watch('culture',
function (value, oldValue) {
if (value !== oldValue) {
loadBreadcrumb();
}
});
}
}
editorState.set(content);
bindEvents();
@@ -118,10 +138,10 @@
function isContentCultureVariant() {
return $scope.content.variants.length > 1;
}
function reload() {
$scope.page.loading = true;
loadContent().then(function() {
loadContent().then(function () {
$scope.page.loading = false;
});
}
@@ -134,7 +154,7 @@
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 && $scope.content.documentType.id === args.documentType.id) {
if (args && args.documentType && $scope.content.documentType.id === args.documentType.id) {
reload();
}
}));
@@ -152,12 +172,6 @@
$scope.content = data;
if (data.isChildOfListView && data.trashed === false) {
$scope.page.listViewPath = ($routeParams.page) ?
"/content/content/edit/" + data.parentId + "?page=" + $routeParams.page :
"/content/content/edit/" + data.parentId;
}
init();
syncTreeNode($scope.content, $scope.content.path, true);
@@ -219,7 +233,7 @@
$scope.page.showPreviewButton = true;
}
/** Syncs the content item to it's tree node - this occurs on first load and after saving */
function syncTreeNode(content, path, initialLoad) {
@@ -228,9 +242,13 @@
}
if (!$scope.content.isChildOfListView) {
navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) {
$scope.page.menu.currentNode = syncArgs.node;
});
navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true })
.then(function (syncArgs) {
$scope.page.menu.currentNode = syncArgs.node;
}, function () {
//handle the rejection
console.log("A problem occurred syncing the tree! A path is probably incorrect.")
});
}
else if (initialLoad === true) {
@@ -328,7 +346,7 @@
$scope.contentForm.$dirty = false;
for (var i = 0; i < $scope.content.variants.length; i++) {
if($scope.content.variants[i].isDirty){
if ($scope.content.variants[i].isDirty) {
$scope.contentForm.$dirty = true;
return;
}
@@ -337,7 +355,7 @@
// This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish
function performSave(args) {
//Used to check validility of nested form - coming from Content Apps mostly
//Set them all to be invalid
var fieldsToRollback = checkValidility();
@@ -348,7 +366,8 @@
scope: $scope,
content: $scope.content,
action: args.action,
showNotifications: args.showNotifications
showNotifications: args.showNotifications,
softRedirect: true
}).then(function (data) {
//success
init();
@@ -364,11 +383,6 @@
function (err) {
syncTreeNode($scope.content, $scope.content.path);
//error
if (err) {
editorState.set($scope.content);
}
resetNestedFieldValiation(fieldsToRollback);
return $q.reject(err);
@@ -421,9 +435,9 @@
//need to show a notification else it's not clear there was an error.
localizationService.localizeMany([
"speechBubbles_validationFailedHeader",
"speechBubbles_validationFailedMessage"
]
"speechBubbles_validationFailedHeader",
"speechBubbles_validationFailedMessage"
]
).then(function (data) {
notificationsService.error(data[0], data[1]);
});
@@ -440,6 +454,7 @@
$scope.content = data;
init();
startWatches($scope.content);
resetLastListPageNumber($scope.content);
@@ -454,13 +469,14 @@
$scope.page.loading = true;
loadContent().then(function () {
startWatches($scope.content);
$scope.page.loading = false;
});
}
$scope.unpublish = function () {
clearNotifications($scope.content);
if (formHelper.submitForm({ scope: $scope, action: "unpublish", skipValidation: true })) {
if (formHelper.submitForm({ scope: $scope, action: "unpublish", skipValidation: true })) {
var dialog = {
parentScope: $scope,
view: "views/content/overlays/unpublish.html",
@@ -494,6 +510,7 @@
overlayService.close();
}
};
overlayService.open(dialog);
}
};
@@ -510,7 +527,7 @@
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
submitButtonLabelKey: "buttons_saveToPublish",
submit: function(model) {
submit: function (model) {
model.submitButtonState = "busy";
clearNotifications($scope.content);
//we need to return this promise so that the dialog can handle the result and wire up the validation response
@@ -518,14 +535,14 @@
saveMethod: contentResource.sendToPublish,
action: "sendToPublish",
showNotifications: false
}).then(function(data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
clearNotifications($scope.content);
overlayService.close();
return $q.when(data);
},
function(err) {
}).then(function (data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
clearNotifications($scope.content);
overlayService.close();
return $q.when(data);
},
function (err) {
clearDirtyState($scope.content.variants);
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
@@ -534,7 +551,7 @@
return $q.when(err);
});
},
close: function() {
close: function () {
overlayService.close();
}
};
@@ -563,14 +580,13 @@
if (isContentCultureVariant()) {
//before we launch the dialog we want to execute all client side validations first
if (formHelper.submitForm({ scope: $scope, action: "publish" })) {
var dialog = {
parentScope: $scope,
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
submitButtonLabelKey: "buttons_saveAndPublish",
submit: function(model) {
submit: function (model) {
model.submitButtonState = "busy";
clearNotifications($scope.content);
//we need to return this promise so that the dialog can handle the result and wire up the validation response
@@ -578,14 +594,14 @@
saveMethod: contentResource.publish,
action: "publish",
showNotifications: false
}).then(function(data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
clearNotifications($scope.content);
overlayService.close();
return $q.when(data);
},
function(err) {
}).then(function (data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
clearNotifications($scope.content);
overlayService.close();
return $q.when(data);
},
function (err) {
clearDirtyState($scope.content.variants);
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
@@ -594,11 +610,10 @@
return $q.when(err);
});
},
close: function() {
close: function () {
overlayService.close();
}
};
overlayService.open(dialog);
}
else {
@@ -617,7 +632,7 @@
$scope.page.buttonGroupState = "success";
}, function () {
$scope.page.buttonGroupState = "error";
});;
});
}
};
@@ -634,7 +649,7 @@
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
submitButtonLabelKey: "buttons_save",
submit: function(model) {
submit: function (model) {
model.submitButtonState = "busy";
clearNotifications($scope.content);
//we need to return this promise so that the dialog can handle the result and wire up the validation response
@@ -642,14 +657,14 @@
saveMethod: $scope.saveMethod(),
action: "save",
showNotifications: false
}).then(function(data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
clearNotifications($scope.content);
overlayService.close();
return $q.when(data);
},
function(err) {
}).then(function (data) {
//show all notifications manually here since we disabled showing them automatically in the save method
formHelper.showNotifications(data);
clearNotifications($scope.content);
overlayService.close();
return $q.when(data);
},
function (err) {
clearDirtyState($scope.content.variants);
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
@@ -658,7 +673,7 @@
return $q.when(err);
});
},
close: function(oldModel) {
close: function (oldModel) {
overlayService.close();
}
};
@@ -685,7 +700,7 @@
};
$scope.schedule = function() {
$scope.schedule = function () {
clearNotifications($scope.content);
//before we launch the dialog we want to execute all client side validations first
if (formHelper.submitForm({ scope: $scope, action: "schedule" })) {
@@ -755,7 +770,7 @@
}
};
$scope.publishDescendants = function() {
$scope.publishDescendants = function () {
clearNotifications($scope.content);
//before we launch the dialog we want to execute all client side validations first
if (formHelper.submitForm({ scope: $scope, action: "publishDescendants" })) {
@@ -883,13 +898,13 @@
* @param {any} app
*/
$scope.appChanged = function (app) {
$scope.app = app;
$scope.$broadcast("editors.apps.appChanged", { app: app });
createButtons($scope.content);
};
/**
@@ -907,11 +922,11 @@
$scope.infiniteModel.close($scope.infiniteModel);
}
};
/**
* Call back when user click the back-icon
*/
$scope.onBack = function() {
$scope.onBack = function () {
if ($scope.infiniteModel && $scope.infiniteModel.close) {
$scope.infiniteModel.close($scope.infiniteModel);
} else {
@@ -927,9 +942,7 @@
}
//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();
}
serverValidationManager.clear();
});
}
@@ -328,6 +328,8 @@
isInfoTab = true;
loadAuditTrail();
loadRedirectUrls();
setNodePublishStatus();
formatDatesToLocal();
} else {
isInfoTab = false;
}
@@ -344,6 +346,7 @@
loadAuditTrail(true);
loadRedirectUrls();
setNodePublishStatus();
formatDatesToLocal();
}
updateCurrentUrls();
});
@@ -1,10 +1,12 @@
(function () {
'use strict';
function EditorContentHeader() {
function EditorContentHeader(serverValidationManager) {
function link(scope, el, attr, ctrl) {
var unsubscribe = [];
if (!scope.serverValidationNameField) {
scope.serverValidationNameField = "Name";
}
@@ -15,9 +17,44 @@
scope.vm = {};
scope.vm.dropdownOpen = false;
scope.vm.currentVariant = "";
scope.vm.variantsWithError = [];
scope.vm.defaultVariant = null;
scope.vm.errorsOnOtherVariants = false;// indicating wether to show that other variants, than the current, have errors.
function checkErrorsOnOtherVariants() {
var check = false;
angular.forEach(scope.content.variants, function (variant) {
if (scope.openVariants.indexOf(variant.language.culture) === -1 && scope.variantHasError(variant.language.culture)) {
check = true;
}
});
scope.vm.errorsOnOtherVariants = check;
}
function onCultureValidation(valid, errors, allErrors, culture) {
var index = scope.vm.variantsWithError.indexOf(culture);
if(valid === true) {
if (index !== -1) {
scope.vm.variantsWithError.splice(index, 1);
}
} else {
if (index === -1) {
scope.vm.variantsWithError.push(culture);
}
}
checkErrorsOnOtherVariants();
}
function onInit() {
// find default.
angular.forEach(scope.content.variants, function (variant) {
if (variant.language.isDefault) {
scope.vm.defaultVariant = variant;
}
});
setCurrentVariant();
angular.forEach(scope.content.apps, (app) => {
@@ -26,12 +63,22 @@
}
});
angular.forEach(scope.content.variants, function (variant) {
unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation));
});
unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation));
}
function setCurrentVariant() {
angular.forEach(scope.content.variants, function (variant) {
if (variant.active) {
scope.vm.currentVariant = variant;
checkErrorsOnOtherVariants();
}
});
}
@@ -80,9 +127,24 @@
* @param {any} culture
*/
scope.variantIsOpen = function(culture) {
if(scope.openVariants.indexOf(culture) !== -1) {
return (scope.openVariants.indexOf(culture) !== -1);
}
/**
* Check whether a variant has a error, used to display errors in variant switcher.
* @param {any} culture
*/
scope.variantHasError = function(culture) {
// if we are looking for the default language we also want to check for invariant.
if (culture === scope.vm.defaultVariant.language.culture) {
if(scope.vm.variantsWithError.indexOf("invariant") !== -1) {
return true;
}
}
if(scope.vm.variantsWithError.indexOf(culture) !== -1) {
return true;
}
return false;
}
onInit();
@@ -103,6 +165,12 @@
}
});
}
scope.$on('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();
}
});
}
@@ -3,107 +3,113 @@
**/
angular.module('umbraco.directives')
.directive('onDragEnter', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragEnter);
};
elm.on("dragenter", f);
scope.$on("$destroy", function(){ elm.off("dragenter", f);} );
}
};
})
.directive('onDragLeave', function () {
return function (scope, elm, attrs) {
var f = function (event) {
var rect = this.getBoundingClientRect();
var getXY = function getCursorPosition(event) {
var x, y;
if (typeof event.clientX === 'undefined') {
// try touch screen
x = event.pageX + document.documentElement.scrollLeft;
y = event.pageY + document.documentElement.scrollTop;
} else {
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
return { x: x, y : y };
};
var e = getXY(event.originalEvent);
// Check the mouseEvent coordinates are outside of the rectangle
if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) {
scope.$apply(attrs.onDragLeave);
.directive('onDragEnter', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragEnter);
};
elm.on("dragenter", f);
scope.$on("$destroy", function () { elm.off("dragenter", f); });
}
};
})
elm.on("dragleave", f);
scope.$on("$destroy", function(){ elm.off("dragleave", f);} );
};
})
.directive('onDragLeave', function () {
return function (scope, elm, attrs) {
var f = function (event) {
var rect = this.getBoundingClientRect();
var getXY = function getCursorPosition(event) {
var x, y;
.directive('onDragOver', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragOver);
if (typeof event.clientX === 'undefined') {
// try touch screen
x = event.pageX + document.documentElement.scrollLeft;
y = event.pageY + document.documentElement.scrollTop;
} else {
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
return { x: x, y: y };
};
var e = getXY(event.originalEvent);
// Check the mouseEvent coordinates are outside of the rectangle
if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) {
scope.$apply(attrs.onDragLeave);
}
};
elm.on("dragover", f);
scope.$on("$destroy", function(){ elm.off("dragover", f);} );
}
};
})
.directive('onDragStart', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragStart);
};
elm.on("dragstart", f);
scope.$on("$destroy", function(){ elm.off("dragstart", f);} );
}
};
})
elm.on("dragleave", f);
scope.$on("$destroy", function () { elm.off("dragleave", f); });
};
})
.directive('onDragEnd', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragEnd);
};
elm.on("dragend", f);
scope.$on("$destroy", function(){ elm.off("dragend", f);} );
}
};
})
.directive('onDragOver', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragOver);
};
elm.on("dragover", f);
scope.$on("$destroy", function () { elm.off("dragover", f); });
}
};
})
.directive('onDrop', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDrop);
};
elm.on("drop", f);
scope.$on("$destroy", function(){ elm.off("drop", f);} );
}
};
})
.directive('onDragStart', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragStart);
};
elm.on("dragstart", f);
scope.$on("$destroy", function () { elm.off("dragstart", f); });
}
};
})
.directive('onOutsideClick', function ($timeout, angularHelper) {
return function (scope, element, attrs) {
.directive('onDragEnd', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDragEnd);
};
elm.on("dragend", f);
scope.$on("$destroy", function () { elm.off("dragend", f); });
}
};
})
var eventBindings = [];
.directive('onDrop', function () {
return {
link: function (scope, elm, attrs) {
var f = function () {
scope.$apply(attrs.onDrop);
};
elm.on("drop", f);
scope.$on("$destroy", function () { elm.off("drop", f); });
}
};
})
.directive('onOutsideClick', function ($timeout, angularHelper) {
return function (scope, element, attrs) {
var eventBindings = [];
function oneTimeClick(event) {
var el = event.target.nodeName;
//ignore link and button clicks
var els = ["INPUT", "A", "BUTTON"];
if (els.indexOf(el) >= 0) { return; }
function oneTimeClick(event) {
// ignore clicks on new overlay
var parents = $(event.target).parents(".umb-overlay,.umb-tour");
if(parents.length > 0){
var parents = $(event.target).parents("a,button,.umb-overlay,.umb-tour");
if (parents.length > 0) {
return;
}
@@ -126,70 +132,70 @@ angular.module('umbraco.directives')
}
//ignore clicks inside this element
if( $(element).has( $(event.target) ).length > 0 ){
if ($(element).has($(event.target)).length > 0) {
return;
}
scope.$apply(attrs.onOutsideClick);
}
$timeout(function(){
if ("bindClickOn" in attrs) {
eventBindings.push(scope.$watch(function() {
return attrs.bindClickOn;
}, function(newValue) {
if (newValue === "true") {
$(document).on("click", oneTimeClick);
} else {
$(document).off("click", oneTimeClick);
}
}));
} else {
$(document).on("click", oneTimeClick);
angularHelper.safeApply(scope, attrs.onOutsideClick);
}
scope.$on("$destroy", function() {
$(document).off("click", oneTimeClick);
// unbind watchers
for (var e in eventBindings) {
eventBindings[e]();
$timeout(function () {
if ("bindClickOn" in attrs) {
eventBindings.push(scope.$watch(function () {
return attrs.bindClickOn;
}, function (newValue) {
if (newValue === "true") {
$(document).on("click", oneTimeClick);
} else {
$(document).off("click", oneTimeClick);
}
}));
} else {
$(document).on("click", oneTimeClick);
}
scope.$on("$destroy", function () {
$(document).off("click", oneTimeClick);
// unbind watchers
for (var e in eventBindings) {
eventBindings[e]();
}
});
}); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution.
};
})
.directive('onRightClick', function ($parse) {
document.oncontextmenu = function (e) {
if (e.target.hasAttribute('on-right-click')) {
e.preventDefault();
e.stopPropagation();
return false;
}
};
return function (scope, el, attrs) {
el.on('contextmenu', function (e) {
e.preventDefault();
e.stopPropagation();
var fn = $parse(attrs.onRightClick);
scope.$apply(function () {
fn(scope, { $event: e });
});
return false;
});
}); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution.
};
})
};
})
.directive('onRightClick',function($parse){
document.oncontextmenu = function (e) {
if(e.target.hasAttribute('on-right-click')) {
e.preventDefault();
e.stopPropagation();
return false;
}
};
return function(scope,el,attrs){
el.on('contextmenu',function(e){
e.preventDefault();
e.stopPropagation();
var fn = $parse(attrs.onRightClick);
scope.$apply(function () {
fn(scope, { $event: e });
});
return false;
});
};
})
.directive('onDelayedMouseleave', function ($timeout, $parse) {
.directive('onDelayedMouseleave', function ($timeout, $parse) {
return {
restrict: 'A',
@@ -198,20 +204,20 @@ angular.module('umbraco.directives')
var active = false;
var fn = $parse(attrs.onDelayedMouseleave);
var leave_f = function(event) {
var callback = function() {
fn(scope, {$event:event});
var leave_f = function (event) {
var callback = function () {
fn(scope, { $event: event });
};
active = false;
$timeout(function(){
if(active === false){
$timeout(function () {
if (active === false) {
scope.$apply(callback);
}
}, 650);
};
var enter_f = function(event, args){
var enter_f = function (event, args) {
active = true;
};
@@ -220,7 +226,7 @@ angular.module('umbraco.directives')
element.on("mouseenter", enter_f);
//unsub events
scope.$on("$destroy", function(){
scope.$on("$destroy", function () {
element.off("mouseleave", leave_f);
element.off("mouseenter", enter_f);
});
@@ -12,7 +12,6 @@ function treeSearchBox(localizationService, searchService, $q) {
searchFromName: "@",
showSearch: "@",
section: "@",
ignoreUserStartNodes: "@",
hideSearchCallback: "=",
searchCallback: "="
},
@@ -35,7 +34,6 @@ function treeSearchBox(localizationService, searchService, $q) {
scope.showSearch = "false";
}
//used to cancel any request in progress if another one needs to take it's place
var canceler = null;
@@ -62,11 +60,6 @@ function treeSearchBox(localizationService, searchService, $q) {
searchArgs["searchFrom"] = scope.searchFromId;
}
//append ignoreUserStartNodes value if there is one
if (scope.ignoreUserStartNodes) {
searchArgs["ignoreUserStartNodes"] = scope.ignoreUserStartNodes;
}
searcher(searchArgs).then(function (data) {
scope.searchCallback(data);
//set back to null so it can be re-created
@@ -96,13 +96,13 @@ Use this directive to generate a pagination.
scope.pageNumber = parseInt(scope.pageNumber);
}
scope.pagination = [];
let tempPagination = [];
var i = 0;
if (scope.totalPages <= 10) {
for (i = 0; i < scope.totalPages; i++) {
scope.pagination.push({
tempPagination.push({
val: (i + 1),
isActive: scope.pageNumber === (i + 1)
});
@@ -119,7 +119,7 @@ Use this directive to generate a pagination.
start = Math.min(maxIndex, start);
for (i = start; i < (10 + start) ; i++) {
scope.pagination.push({
tempPagination.push({
val: (i + 1),
isActive: scope.pageNumber === (i + 1)
});
@@ -129,7 +129,7 @@ Use this directive to generate a pagination.
if (start > 0) {
localizationService.localize("general_first").then(function(value){
var firstLabel = value;
scope.pagination.unshift({ name: firstLabel, val: 1, isActive: false }, {val: "...",isActive: false});
tempPagination.unshift({ name: firstLabel, val: 1, isActive: false }, {val: "...",isActive: false});
});
}
@@ -137,9 +137,11 @@ Use this directive to generate a pagination.
if (start < maxIndex) {
localizationService.localize("general_last").then(function(value){
var lastLabel = value;
scope.pagination.push({ val: "...", isActive: false }, { name: lastLabel, val: scope.totalPages, isActive: false });
tempPagination.push({ val: "...", isActive: false }, { name: lastLabel, val: scope.totalPages, isActive: false });
});
}
scope.pagination = tempPagination;
}
}
@@ -12,27 +12,50 @@
function valPropertyMsg(serverValidationManager) {
return {
require: ['^^form', '^^valFormManager', '^^umbProperty'],
require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'],
replace: true,
restrict: "E",
template: "<div ng-show=\"errorMsg != ''\" class='alert alert-error property-error' >{{errorMsg}}</div>",
scope: {},
link: function (scope, element, attrs, ctrl) {
var unsubscribe = [];
var watcher = null;
var hasError = false;
//create properties on our custom scope so we can use it in our template
scope.errorMsg = "";
//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];
//the variants controller api
var umbVariantCtrl = ctrl[3];
scope.currentProperty = umbPropCtrl.property;
var currentProperty = umbPropCtrl.property;
scope.currentProperty = currentProperty;
var currentCulture = currentProperty.culture;
//if the property is invariant (no culture), then we will explicitly set it to the string 'invariant'
//since this matches the value that the server will return for an invariant property.
var currentCulture = scope.currentProperty.culture || "invariant";
if (umbVariantCtrl) {
//if we are inside of an umbVariantContent directive
var watcher = null;
var currentVariant = umbVariantCtrl.editor.content;
// Lets check if we have variants and we are on the default language then ...
if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) {
//This property is locked cause its a invariant property shown on a non-default language.
//Therefor do not validate this field.
return;
}
}
// if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language.
currentCulture = currentCulture || "invariant";
// Gets the error message to display
function getErrorMsg() {
@@ -138,13 +161,6 @@ function valPropertyMsg(serverValidationManager) {
}
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.
@@ -7,7 +7,7 @@
**/
function valServer(serverValidationManager) {
return {
require: ['ngModel', '?^^umbProperty'],
require: ['ngModel', '?^^umbProperty', '?^^umbVariantContent'],
restrict: "A",
scope: {},
link: function (scope, element, attr, ctrls) {
@@ -18,9 +18,29 @@ function valServer(serverValidationManager) {
//we cannot proceed, this validator will be disabled
return;
}
// optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages.
var umbVariantCtrl = ctrls.length > 2 ? ctrls[2] : null;
var currentProperty = umbPropCtrl.property;
var currentCulture = currentProperty.culture;
if (umbVariantCtrl) {
//if we are inside of an umbVariantContent directive
var currentVariant = umbVariantCtrl.editor.content;
// Lets check if we have variants and we are on the default language then ...
if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) {
//This property is locked cause its a invariant property shown on a non-default language.
//Therefor do not validate this field.
return;
}
}
// if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language.
currentCulture = currentCulture || "invariant";
var watcher = null;
var unsubscribe = [];
@@ -365,28 +365,17 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
* </pre>
*
* @param {Int} id id of content item to return
* @param {Bool} options.ignoreUserStartNodes set to true to allow a user to choose nodes that they normally don't have access to
* @param {Int} culture optional culture to retrieve the item in
* @returns {Promise} resourcePromise object containing the content item.
*
*/
getById: function (id, options) {
var defaults = {
ignoreUserStartNodes: false
};
if (options === undefined) {
options = {};
}
//overwrite the defaults if there are any specified
angular.extend(defaults, options);
//now copy back to the options we will use
options = defaults;
getById: function (id) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetById",
[{ id: id }, { ignoreUserStartNodes: options.ignoreUserStartNodes }])),
{ id: id })),
'Failed to retrieve data for content id ' + id)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));
@@ -284,31 +284,15 @@ function entityResource($q, $http, umbRequestHelper) {
* @returns {Promise} resourcePromise object containing the entity.
*
*/
getAncestors: function (id, type, culture, options) {
var defaults = {
ignoreUserStartNodes: false
};
if (options === undefined) {
options = {};
}
//overwrite the defaults if there are any specified
angular.extend(defaults, options);
//now copy back to the options we will use
options = defaults;
getAncestors: function (id, type, culture) {
if (culture === undefined) culture = "";
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"entityApiBaseUrl",
"GetAncestors",
[
{ id: id },
{ type: type },
{ culture: culture },
{ ignoreUserStartNodes: options.ignoreUserStartNodes }
])),
'Failed to retrieve ancestor data for id ' + id);
[{ id: id }, { type: type }, { culture: culture }])),
'Failed to retrieve ancestor data for id ' + id);
},
/**
@@ -440,8 +424,7 @@ function entityResource($q, $http, umbRequestHelper) {
pageNumber: 1,
filter: '',
orderDirection: "Ascending",
orderBy: "SortOrder",
ignoreUserStartNodes: false
orderBy: "SortOrder"
};
if (options === undefined) {
options = {};
@@ -470,8 +453,7 @@ function entityResource($q, $http, umbRequestHelper) {
pageSize: options.pageSize,
orderBy: options.orderBy,
orderDirection: options.orderDirection,
filter: encodeURIComponent(options.filter),
ignoreUserStartNodes: options.ignoreUserStartNodes
filter: encodeURIComponent(options.filter)
}
)),
'Failed to retrieve child data for id ' + parentId);
@@ -499,19 +481,12 @@ function entityResource($q, $http, umbRequestHelper) {
* @returns {Promise} resourcePromise object containing the entity array.
*
*/
search: function (query, type, options, canceler) {
search: function (query, type, searchFrom, canceler) {
var args = [{ query: query }, { type: type }];
if(options !== undefined) {
if (options.searchFrom) {
args.push({ searchFrom: options.searchFrom });
}
if (options.ignoreUserStartNodes) {
args.push({ ignoreUserStartNodes: options.ignoreUserStartNodes });
}
if (searchFrom) {
args.push({ searchFrom: searchFrom });
}
var httpConfig = {};
if (canceler) {
@@ -1,39 +1,39 @@
/**
* @ngdoc service
* @name umbraco.resources.logViewerResource
* @description Retrives Umbraco log items (by default from JSON files on disk)
*
*
**/
function logViewerResource($q, $http, umbRequestHelper) {
* @ngdoc service
* @name umbraco.resources.logViewerResource
* @description Retrives Umbraco log items (by default from JSON files on disk)
*
*
**/
function logViewerResource($q, $http, umbRequestHelper) {
//the factory object returned
return {
getNumberOfErrors: function () {
getNumberOfErrors: function (startDate, endDate) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetNumberOfErrors")),
"GetNumberOfErrors")+ '?startDate='+startDate+ '&endDate='+ endDate ),
'Failed to retrieve number of errors in logs');
},
getLogLevelCounts: function () {
getLogLevelCounts: function (startDate, endDate) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetLogLevelCounts")),
"GetLogLevelCounts")+ '?startDate='+startDate+ '&endDate='+ endDate ),
'Failed to retrieve log level counts');
},
getMessageTemplates: function () {
getMessageTemplates: function (startDate, endDate) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetMessageTemplates")),
"GetMessageTemplates")+ '?startDate='+startDate+ '&endDate='+ endDate ),
'Failed to retrieve log templates');
},
@@ -93,12 +93,12 @@
'Failed to retrieve common log messages');
},
canViewLogs: function () {
canViewLogs: function (startDate, endDate) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"logViewerApiBaseUrl",
"GetCanViewLogs")),
"GetCanViewLogs") + '?startDate='+startDate+ '&endDate='+ endDate ),
'Failed to retrieve state if logs can be viewed');
}
@@ -329,8 +329,7 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) {
filter: '',
orderDirection: "Ascending",
orderBy: "SortOrder",
orderBySystemField: true,
ignoreUserStartNodes: false
orderBySystemField: true
};
if (options === undefined) {
options = {};
@@ -368,7 +367,6 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) {
"GetChildren",
[
{ id: parentId },
{ ignoreUserStartNodes: options.ignoreUserStartNodes },
{ pageNumber: options.pageNumber },
{ pageSize: options.pageSize },
{ orderBy: options.orderBy },
@@ -5,7 +5,7 @@
* @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by
* all editors to share logic and reduce the amount of replicated code among editors.
**/
function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, navigationService, localizationService, serverValidationManager, formHelper) {
function contentEditingHelper(fileManager, $q, $location, $routeParams, editorState, notificationsService, navigationService, localizationService, serverValidationManager, formHelper) {
function isValidIdentifier(id) {
@@ -34,9 +34,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
return {
//TODO: We need to move some of this to formHelper for saving, too many editors use this method for saving when this entire
//service should only be used for content/media/members
/** Used by the content editor and mini content editor to perform saving operations */
// TODO: Make this a more helpful/reusable method for other form operations! we can simplify this form most forms
// = this is already done in the formhelper service
contentEditorPerformSave: function (args) {
if (!angular.isObject(args)) {
throw "args must be an object";
@@ -53,18 +54,19 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
if (args.showNotifications === undefined) {
args.showNotifications = true;
}
var redirectOnSuccess = args.redirectOnSuccess !== undefined ? args.redirectOnSuccess : true;
var redirectOnFailure = args.redirectOnFailure !== undefined ? args.redirectOnFailure : true;
if (args.softRedirect === undefined) {
//when true, the url will change but it won't actually re-route
//this is merely here for compatibility, if only the content/media/members used this service we'd prob be ok but tons of editors
//use this service unfortunately and probably packages too.
args.softRedirect = false;
}
var self = this;
//we will use the default one for content if not specified
var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback;
if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, action: args.action })) {
args.scope.busy = true;
if (formHelper.submitForm({ scope: args.scope, action: args.action })) {
return args.saveMethod(args.content, $routeParams.create, fileManager.getFiles(), args.showNotifications)
.then(function (data) {
@@ -74,26 +76,30 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
self.handleSuccessfulSave({
scope: args.scope,
savedContent: data,
redirectOnSuccess: redirectOnSuccess,
softRedirect: args.softRedirect,
rebindCallback: function () {
rebindCallback.apply(self, [args.content, data]);
}
});
args.scope.busy = false;
//update editor state to what is current
editorState.set(args.content);
return $q.resolve(data);
}, function (err) {
self.handleSaveError({
showNotifications: args.showNotifications,
redirectOnFailure: redirectOnFailure,
softRedirect: args.softRedirect,
err: err,
rebindCallback: function () {
rebindCallback.apply(self, [args.content, err.data]);
}
});
args.scope.busy = false;
//update editor state to what is current
editorState.set(args.content);
return $q.reject(err);
});
}
@@ -265,7 +271,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
// if publishing is allowed also allow schedule publish
// we add this manually becuase it doesn't have a permission so it wont
// get picked up by the loop through permissions
if( _.contains(args.content.allowedActions, "U")) {
if (_.contains(args.content.allowedActions, "U")) {
buttons.subButtons.push(createButtonDefinition("SCHEDULE"));
buttons.subButtons.push(createButtonDefinition("PUBLISH_DESCENDANTS"));
}
@@ -274,7 +280,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
// so long as it's already published and if the user has access to publish
// and the user has access to unpublish (may have been removed via Event)
if (!args.create) {
var hasPublishedVariant = args.content.variants.filter(function(variant) { return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); }).length > 0;
var hasPublishedVariant = args.content.variants.filter(function (variant) { return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); }).length > 0;
if (hasPublishedVariant && _.contains(args.content.allowedActions, "U") && _.contains(args.content.allowedActions, "Z")) {
buttons.subButtons.push(createButtonDefinition("Z"));
}
@@ -444,7 +450,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
var shouldIgnore = function (propName) {
return _.some([
"variants",
"tabs",
"properties",
"apps",
@@ -574,15 +580,16 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
* A function to handle what happens when we have validation issues from the server side
*
*/
//TODO: Too many editors use this method for saving when this entire service should only be used for content/media/members,
// there is formHelper.handleError for other editors which should be used!
handleSaveError: function (args) {
if (!args.err) {
throw "args.err cannot be null";
}
if (args.redirectOnFailure === undefined || args.redirectOnFailure === null) {
throw "args.redirectOnFailure must be set to true or false";
}
//When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors.
//Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
//Or, some strange server error
@@ -600,16 +607,16 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
}
}
if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) {
//we are not redirecting because this is not new content, it is existing content. In this case
// we need to detect what properties have changed and re-bind them with the server data. Then we need
// to re-bind any server validation errors after the digest takes place.
if (!this.redirectToCreatedContent(args.err.data.id) || args.softRedirect) {
// If we are not redirecting it's because this is not newly created content, else in some cases we are
// soft-redirecting which means the URL will change but the route wont (i.e. creating content).
// In this case we need to detect what properties have changed and re-bind them with the server data.
if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
args.rebindCallback();
}
//notify all validators (don't clear the server validations though since we need to maintain their state because of
// In this case notify all validators (don't clear the server validations though since we need to maintain their state because of
// how the variant switcher works in content). server validation state is always cleared when an editor first loads
// and in theory when an editor is destroyed.
serverValidationManager.notify();
@@ -633,6 +640,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
* ensure the notifications are displayed and that the appropriate events are fired. This will also check if we need to redirect
* when we're creating new content.
*/
//TODO: We need to move some of this to formHelper for saving, too many editors use this method for saving when this entire
//service should only be used for content/media/members
handleSuccessfulSave: function (args) {
if (!args) {
@@ -642,14 +653,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
throw "args.savedContent cannot be null";
}
// the default behaviour is to redirect on success. This adds option to prevent when false
args.redirectOnSuccess = args.redirectOnSuccess !== undefined ? args.redirectOnSuccess : true;
if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id) || args.softRedirect) {
if (!args.redirectOnSuccess || !this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) {
// If we are not redirecting it's because this is not newly created content, else in some cases we are
// soft-redirecting which means the URL will change but the route wont (i.e. creating content).
//we are not redirecting because this is not new content, it is existing content. In this case
// we need to detect what properties have changed and re-bind them with the server data.
//call the callback
// In this case we need to detect what properties have changed and re-bind them with the server data.
if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
args.rebindCallback();
}
@@ -667,7 +676,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
* We need to decide if we need to redirect to edito mode or if we will remain in create mode.
* We will only need to maintain create mode if we have not fulfilled the basic requirements for creating an entity which is at least having a name and ID
*/
redirectToCreatedContent: function (id, modelState) {
redirectToCreatedContent: function (id) {
//only continue if we are currently in create mode and not in infinite mode and if the resulting ID is valid
if ($routeParams.create && (isValidIdentifier(id))) {
@@ -679,7 +688,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
//clear the query strings
navigationService.clearSearch(["cculture"]);
//change to new path
$location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id);
//don't add a browser history for this
@@ -699,6 +708,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica
* For some editors like scripts or entites that have names as ids, these names can change and we need to redirect
* to their new paths, this is helper method to do that.
*/
//TODO: We need to move some of this to formHelper for saving, too many editors use this method for saving when this entire
//service should only be used for content/media/members
redirectToRenamedContent: function (id) {
//clear the query strings
navigationService.clearSearch();
@@ -5,13 +5,15 @@
*
* @description
* Tracks the parent object for complex editors by exposing it as
* an object reference via editorState.current.entity
* an object reference via editorState.current.getCurrent().
* The state is cleared on each successful route.
*
* it is possible to modify this object, so should be used with care
*/
angular.module('umbraco.services').factory("editorState", function() {
angular.module('umbraco.services').factory("editorState", function ($rootScope) {
var current = null;
var state = {
/**
@@ -40,7 +42,7 @@ angular.module('umbraco.services').factory("editorState", function() {
* Since the editorstate entity is read-only, you cannot set it to null
* only through the reset() method
*/
reset: function() {
reset: function () {
current = null;
},
@@ -59,9 +61,10 @@ angular.module('umbraco.services').factory("editorState", function() {
* editorState.current can not be overwritten, you should only read values from it
* since modifying individual properties should be handled by the property editors
*/
getCurrent: function() {
getCurrent: function () {
return current;
}
};
// TODO: This shouldn't be removed! use getCurrent() method instead of a hacked readonly property which is confusing.
@@ -76,5 +79,13 @@ angular.module('umbraco.services').factory("editorState", function() {
}
});
//execute on each successful route (this is only bound once per application since a service is a singleton)
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
//reset the editorState on each successful route chage
state.reset();
});
return state;
});
@@ -8,11 +8,12 @@
* that need to attach files.
* When a route changes successfully, we ensure that the collection is cleared.
*/
function fileManager() {
function fileManager($rootScope) {
var fileCollection = [];
return {
var mgr = {
/**
* @ngdoc function
* @name umbraco.services.fileManager#addFiles
@@ -24,7 +25,7 @@ function fileManager() {
* for the files collection that effectively clears the files for the specified editor.
*/
setFiles: function (args) {
//propertyAlias, files
if (!angular.isString(args.propertyAlias)) {
throw "args.propertyAlias must be a non empty string";
@@ -52,7 +53,7 @@ function fileManager() {
fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, metaData: metaData });
}
},
/**
* @ngdoc function
* @name umbraco.services.fileManager#getFiles
@@ -62,10 +63,10 @@ function fileManager() {
* @description
* Returns all of the files attached to the file manager
*/
getFiles: function() {
getFiles: function () {
return fileCollection;
},
/**
* @ngdoc function
* @name umbraco.services.fileManager#clearFiles
@@ -78,7 +79,17 @@ function fileManager() {
clearFiles: function () {
fileCollection = [];
}
};
};
//execute on each successful route (this is only bound once per application since a service is a singleton)
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
//reset the file manager on each route change, the file collection is only relavent
// when working in an editor and submitting data to the server.
//This ensures that memory remains clear of any files and that the editors don't have to manually clear the files.
mgr.clearFiles();
});
return mgr;
}
angular.module('umbraco.services').factory('fileManager', fileManager);
@@ -40,7 +40,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
else {
currentForm = args.formCtrl;
}
//the first thing any form must do is broadcast the formSubmitting event
args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action });
@@ -53,10 +53,10 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
//reset the server validations
serverValidationManager.reset();
return true;
},
/**
* @ngdoc function
* @name umbraco.services.formHelper#submitForm
@@ -75,21 +75,21 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
if (!args.scope) {
throw "args.scope cannot be null";
}
args.scope.$broadcast("formSubmitted", { scope: args.scope });
},
showNotifications: function (args) {
if (!args || !args.notifications) {
return false;
}
if (angular.isArray(args.notifications)) {
for (var i = 0; i < args.notifications.length; i++) {
notificationsService.showNotification(args.notifications[i]);
if (!args || !args.notifications) {
return false;
}
return true;
}
return false;
if (angular.isArray(args.notifications)) {
for (var i = 0; i < args.notifications.length; i++) {
notificationsService.showNotification(args.notifications[i]);
}
return true;
}
return false;
},
/**
@@ -104,7 +104,12 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
*
* @param {object} err The error object returned from the http promise
*/
handleError: function (err) {
handleError: function (err) {
//TODO: Potentially add in the logic to showNotifications like the contentEditingHelper.handleSaveError does so that
// non content editors can just use this method instead of contentEditingHelper.handleSaveError which they should not use
// and they won't need to manually do it.
//When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors.
//Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
//Or, some strange server error
@@ -116,7 +121,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
this.handleServerValidation(err.data.ModelState);
//execute all server validation events and subscribers
serverValidationManager.notifyAndClearAllSubscriptions();
serverValidationManager.notifyAndClearAllSubscriptions();
}
}
else {
@@ -124,7 +129,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
// TODO: All YSOD handling should be done with an interceptor
overlayService.ysod(err);
}
},
/**
@@ -184,8 +189,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
serverValidationManager.addPropertyError(propertyAlias, culture, "", modelState[e][0]);
}
}
else {
} else {
//Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example:
// Groups[0].Properties[2].Alias
@@ -143,27 +143,21 @@ function mediaHelper(umbRequestHelper) {
*/
resolveFileFromEntity: function (mediaEntity, thumbnail) {
if (!angular.isObject(mediaEntity.metaData)) {
if (!angular.isObject(mediaEntity.metaData) || !mediaEntity.metaData.MediaPath) {
throw "Cannot resolve the file url from the mediaEntity, it does not contain the required metaData";
}
var values = _.values(mediaEntity.metaData);
for (var i = 0; i < values.length; i++) {
var val = values[i];
if (angular.isObject(val) && val.PropertyEditorAlias) {
for (var resolver in _mediaFileResolvers) {
if (val.PropertyEditorAlias === resolver) {
//we need to format a property variable that coincides with how the property would be structured
// if it came from the mediaResource just to keep things slightly easier for the file resolvers.
var property = { value: val.Value };
return _mediaFileResolvers[resolver](property, mediaEntity, thumbnail);
}
}
if (thumbnail) {
if (this.detectIfImageByExtension(mediaEntity.metaData.MediaPath)) {
return this.getThumbnailFromPath(mediaEntity.metaData.MediaPath);
}
else {
return null;
}
}
return "";
else {
return mediaEntity.metaData.MediaPath;
}
},
/**
@@ -13,7 +13,7 @@
* Section navigation and search, and maintain their state for the entire application lifetime
*
*/
function navigationService($routeParams, $location, $q, $timeout, $injector, eventsService, umbModelMapper, treeService, appState) {
function navigationService($routeParams, $location, $q, $injector, eventsService, umbModelMapper, treeService, appState) {
//the promise that will be resolved when the navigation is ready
var navReadyPromise = $q.defer();
@@ -31,7 +31,8 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve
//A list of query strings defined that when changed will not cause a reload of the route
var nonRoutingQueryStrings = ["mculture", "cculture", "lq"];
var retainedQueryStrings = ["mculture"];
//A list of trees that don't cause a route when creating new items (TODO: eventually all trees should do this!)
var nonRoutingTreesOnCreate = ["content", "contentblueprints"];
function setMode(mode) {
switch (mode) {
@@ -115,16 +116,17 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve
}
var service = {
/**
* @ngdoc method
* @name umbraco.services.navigationService#isRouteChangingNavigation
* @methodOf umbraco.services.navigationService
*
* @description
* Detects if the route param differences will cause a navigation change or if the route param differences are
* Detects if the route param differences will cause a navigation/route change or if the route param differences are
* only tracking state changes.
* This is used for routing operations where reloadOnSearch is false and when detecting form dirty changes when navigating to a different page.
* This is used for routing operations where "reloadOnSearch: false" or "reloadOnUrl: false", when detecting form dirty changes when navigating to a different page,
* and when we are creating new entities and moving from a route with the ?create=true parameter to an ID based parameter once it's created.
* @param {object} currUrlParams Either a string path or a dictionary of route parameters
* @param {object} nextUrlParams Either a string path or a dictionary of route parameters
*/
@@ -138,6 +140,14 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve
nextUrlParams = pathToRouteParts(nextUrlParams);
}
//first check if this is a ?create=true url being redirected to it's true url
if (currUrlParams.create === "true" && currUrlParams.id && currUrlParams.section && currUrlParams.tree && currUrlParams.method === "edit" &&
!nextUrlParams.create && nextUrlParams.id && nextUrlParams.section === currUrlParams.section && nextUrlParams.tree === currUrlParams.tree && nextUrlParams.method === currUrlParams.method &&
nonRoutingTreesOnCreate.indexOf(nextUrlParams.tree.toLowerCase()) >= 0) {
//this means we're coming from a path like /content/content/edit/1234?create=true to the created path like /content/content/edit/9999
return false;
}
var allowRoute = true;
//The only time that we want to not route is if only any of the nonRoutingQueryStrings have changed/added.
@@ -77,4 +77,5 @@
angular.module("umbraco.services").factory("overlayService", overlayService);
})();
@@ -42,11 +42,7 @@ angular.module('umbraco.services')
throw "args.term is required";
}
var options = {
searchFrom: args.searchFrom
}
return entityResource.search(args.term, "Member", options).then(function (data) {
return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) {
_.each(data, function (item) {
searchResultFormatter.configureMemberResult(item);
});
@@ -71,12 +67,7 @@ angular.module('umbraco.services')
throw "args.term is required";
}
var options = {
searchFrom: args.searchFrom,
ignoreUserStartNodes: args.ignoreUserStartNodes
}
return entityResource.search(args.term, "Document", options, args.canceler).then(function (data) {
return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) {
_.each(data, function (item) {
searchResultFormatter.configureContentResult(item);
});
@@ -101,12 +92,7 @@ angular.module('umbraco.services')
throw "args.term is required";
}
var options = {
searchFrom: args.searchFrom,
ignoreUserStartNodes: args.ignoreUserStartNodes
}
return entityResource.search(args.term, "Media", options).then(function (data) {
return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) {
_.each(data, function (item) {
searchResultFormatter.configureMediaResult(item);
});
@@ -13,12 +13,15 @@ function serverValidationManager($timeout) {
var callbacks = [];
/** calls the callback specified with the errors specified, used internally */
function executeCallback(self, errorsForCallback, callback) {
function executeCallback(self, errorsForCallback, callback, culture) {
callback.apply(self, [
false, //pass in a value indicating it is invalid
errorsForCallback, //pass in the errors for this item
self.items]); //pass in all errors in total
false, // pass in a value indicating it is invalid
errorsForCallback, // pass in the errors for this item
self.items, // pass in all errors in total
culture // pass the culture that we are listing for.
]
);
}
function getFieldErrors(self, fieldName) {
@@ -28,7 +31,7 @@ function serverValidationManager($timeout) {
//find errors for this field name
return _.filter(self.items, function (item) {
return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName);
});
}
@@ -39,27 +42,50 @@ function serverValidationManager($timeout) {
if (fieldName && !angular.isString(fieldName)) {
throw "fieldName must be a string";
}
if (!culture) {
culture = "invariant";
}
//find all errors for this property
return _.filter(self.items, function (item) {
return _.filter(self.items, function (item) {
return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
}
function getCultureErrors(self, culture) {
if (!culture) {
culture = "invariant";
}
//find all errors for this property
return _.filter(self.items, function (item) {
return (item.culture === culture);
});
}
function notifyCallbacks(self) {
for (var cb in callbacks) {
if (callbacks[cb].propertyAlias === null) {
if (callbacks[cb].propertyAlias === null && callbacks[cb].fieldName !== null) {
//its a field error callback
var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName);
if (fieldErrors.length > 0) {
executeCallback(self, fieldErrors, callbacks[cb].callback);
executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture);
}
}
else {
else if (callbacks[cb].propertyAlias != null) {
//its a property error
var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].fieldName);
if (propErrors.length > 0) {
executeCallback(self, propErrors, callbacks[cb].callback);
executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture);
}
}
else {
//its a culture error
var cultureErrors = getCultureErrors(self, callbacks[cb].culture);
if (cultureErrors.length > 0) {
executeCallback(self, cultureErrors, callbacks[cb].callback, callbacks[cb].culture);
}
}
}
@@ -130,11 +156,14 @@ function serverValidationManager($timeout) {
}
var id = String.CreateGuid();
if (!culture) {
culture = "invariant";
}
if (propertyAlias === null) {
callbacks.push({
propertyAlias: null,
culture: null,
culture: culture,
fieldName: fieldName,
callback: callback,
id: id
@@ -142,12 +171,11 @@ function serverValidationManager($timeout) {
}
else if (propertyAlias !== undefined) {
//normalize culture to null
if (!culture) {
culture = null;
}
callbacks.push({
propertyAlias: propertyAlias,
culture: culture, fieldName: fieldName,
culture: culture,
fieldName: fieldName,
callback: callback,
id: id
});
@@ -173,21 +201,20 @@ function serverValidationManager($timeout) {
*/
unsubscribe: function (propertyAlias, culture, fieldName) {
//normalize culture to null
if (!culture) {
culture = "invariant";
}
if (propertyAlias === null) {
//remove all callbacks for the content field
callbacks = _.reject(callbacks, function (item) {
return item.propertyAlias === null && item.culture === null && item.fieldName === fieldName;
return item.propertyAlias === null && item.culture === culture && item.fieldName === fieldName;
});
}
else if (propertyAlias !== undefined) {
//normalize culture to null
if (!culture) {
culture = null;
}
//remove all callbacks for the content property
callbacks = _.reject(callbacks, function (item) {
return item.propertyAlias === propertyAlias && item.culture === culture &&
@@ -213,7 +240,7 @@ function serverValidationManager($timeout) {
//normalize culture to null
if (!culture) {
culture = null;
culture = "invariant";
}
var found = _.filter(callbacks, function (item) {
@@ -235,7 +262,24 @@ function serverValidationManager($timeout) {
getFieldCallbacks: function (fieldName) {
var found = _.filter(callbacks, function (item) {
//returns any callback that have been registered directly against the field
return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName);
});
return found;
},
/**
* @ngdoc function
* @name getCultureCallbacks
* @methodOf umbraco.services.serverValidationManager
* @function
*
* @description
* Gets all callbacks that has been registered using the subscribe method for the culture.
*/
getCultureCallbacks: function (culture) {
var found = _.filter(callbacks, function (item) {
//returns any callback that have been registered directly/ONLY against the culture
return (item.culture === culture && item.propertyAlias === null && item.fieldName === null);
});
return found;
},
@@ -258,7 +302,7 @@ function serverValidationManager($timeout) {
if (!this.hasFieldError(fieldName)) {
this.items.push({
propertyAlias: null,
culture: null,
culture: "invariant",
fieldName: fieldName,
errorMsg: errorMsg
});
@@ -270,7 +314,7 @@ function serverValidationManager($timeout) {
var cbs = this.getFieldCallbacks(fieldName);
//call each callback for this error
for (var cb in cbs) {
executeCallback(this, errorsForCallback, cbs[cb].callback);
executeCallback(this, errorsForCallback, cbs[cb].callback, null);
}
},
@@ -288,9 +332,9 @@ function serverValidationManager($timeout) {
return;
}
//normalize culture to null
//normalize culture to "invariant"
if (!culture) {
culture = null;
culture = "invariant";
}
//only add the item if it doesn't exist
@@ -309,9 +353,16 @@ function serverValidationManager($timeout) {
var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName);
//call each callback for this error
for (var cb in cbs) {
executeCallback(this, errorsForCallback, cbs[cb].callback);
executeCallback(this, errorsForCallback, cbs[cb].callback, culture);
}
},
//execute culture specific callbacks here too when a propery error is added
var cultureCbs = this.getCultureCallbacks(culture);
//call each callback for this error
for (var cb in cultureCbs) {
executeCallback(this, errorsForCallback, cultureCbs[cb].callback, culture);
}
},
/**
* @ngdoc function
@@ -330,7 +381,7 @@ function serverValidationManager($timeout) {
//normalize culture to null
if (!culture) {
culture = null;
culture = "invariant";
}
//remove the item
@@ -384,7 +435,7 @@ function serverValidationManager($timeout) {
//normalize culture to null
if (!culture) {
culture = null;
culture = "invariant";
}
var err = _.find(this.items, function (item) {
@@ -406,7 +457,7 @@ function serverValidationManager($timeout) {
getFieldError: function (fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName);
});
return err;
},
@@ -424,7 +475,7 @@ function serverValidationManager($timeout) {
//normalize culture to null
if (!culture) {
culture = null;
culture = "invariant";
}
var err = _.find(this.items, function (item) {
@@ -446,11 +497,33 @@ function serverValidationManager($timeout) {
hasFieldError: function (fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === null && item.culture === null && item.fieldName === fieldName);
return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName);
});
return err ? true : false;
},
/**
* @ngdoc function
* @name hasCultureError
* @methodOf umbraco.services.serverValidationManager
* @function
*
* @description
* Checks if the given culture has an error
*/
hasCultureError: function (culture) {
//normalize culture to null
if (!culture) {
culture = "invariant";
}
var err = _.find(this.items, function (item) {
return item.culture === culture;
});
return err ? true : false;
},
/** The array of error messages */
items: []
};
@@ -422,23 +422,12 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
insertMediaInEditor: function (editor, img) {
if (img) {
var hasUdi = img.udi ? true : false;
var data = {
alt: img.altText || "",
src: (img.url) ? img.url : "nothing.jpg",
id: '__mcenew'
id: '__mcenew',
'data-udi': img.udi
};
if (hasUdi) {
data["data-udi"] = img.udi;
} else {
//Considering these fixed because UDI will now be used and thus
// we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595
//TODO: Kill rel attribute
data["rel"] = img.id;
data["data-id"] = img.id;
}
editor.selection.setContent(editor.dom.createHTML('img', data));
@@ -990,7 +979,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
}
}
if (!href) {
if (!href && !target.anchor) {
editor.execCommand('unlink');
return;
}
@@ -1004,6 +993,10 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
return;
}
if (!href) {
href = "";
}
// Is email and not //user@domain.com and protocol (e.g. mailto:, sip:) is not specified
if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf(':') === -1) {
// assume it's a mailto link
@@ -1153,25 +1146,11 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
let self = this;
function getIgnoreUserStartNodes(args) {
var ignoreUserStartNodes = false;
// Most property editors have a "config" property with ignoreUserStartNodes on then
if (args.model.config) {
ignoreUserStartNodes = Object.toBoolean(args.model.config.ignoreUserStartNodes);
}
// EXCEPT for the grid's TinyMCE editor, that one wants to be special and the config is called "configuration" instead
else if (args.model.configuration) {
ignoreUserStartNodes = Object.toBoolean(args.model.configuration.ignoreUserStartNodes);
}
return ignoreUserStartNodes;
}
//create link picker
self.createLinkPicker(args.editor, function (currentTarget, anchorElement) {
var linkPicker = {
currentTarget: currentTarget,
anchors: editorState.current ? self.getAnchorNames(JSON.stringify(editorState.current.properties)) : [],
ignoreUserStartNodes: getIgnoreUserStartNodes(args),
submit: function (model) {
self.insertLinkInEditor(args.editor, model.target, anchorElement);
editorService.close();
@@ -1185,25 +1164,13 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
//Create the insert media plugin
self.createMediaPicker(args.editor, function (currentTarget, userData) {
var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0];
var startNodeIsVirtual = userData.startMediaIds.length !== 1;
var ignoreUserStartNodes = getIgnoreUserStartNodes(args);
if (ignoreUserStartNodes) {
ignoreUserStartNodes = true;
startNodeId = -1;
startNodeIsVirtual = true;
}
var mediaPicker = {
currentTarget: currentTarget,
onlyImages: true,
showDetails: true,
disableFolderSelect: true,
startNodeId: startNodeId,
startNodeIsVirtual: startNodeIsVirtual,
ignoreUserStartNodes: ignoreUserStartNodes,
startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0],
startNodeIsVirtual: userData.startMediaIds.length !== 1,
submit: function (model) {
self.insertMediaInEditor(args.editor, model.selection[0]);
editorService.close();
+3 -10
View File
@@ -1,6 +1,6 @@
/** Executed when the application starts, binds to events and set global state */
app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', 'tourService', 'dashboardResource',
function (userService, $q, $log, $rootScope, $route, $location, urlHelper, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService, tourService, dashboardResource) {
app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService',
function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService) {
//This sets the default jquery ajax headers to include our csrf token, we
// need to user the beforeSend method because our token changes per user/login so
@@ -91,13 +91,6 @@ app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlH
$rootScope.locationTitle = "Umbraco - " + $location.$$host;
}
//reset the editorState on each successful route chage
editorState.reset();
//reset the file manager on each route change, the file collection is only relavent
// when working in an editor and submitting data to the server.
//This ensures that memory remains clear of any files and that the editors don't have to manually clear the files.
fileManager.clearFiles();
});
/** When the route change is rejected - based on checkAuth - we'll prevent the rejected route from executing including
@@ -122,7 +115,7 @@ app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlH
});
//Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered.
//This is the case when a route uses reloadOnSearch: false which is the case for many or our routes so that we are able to maintain
//This is the case when a route uses "reloadOnSearch: false" or "reloadOnUrl: false" which is the case for many or our routes so that we are able to maintain
//global state query strings without force re-loading views.
//We can then detect if it's a location change that should force a route or not programatically.
$rootScope.$on('$routeUpdate', function (event, next) {
@@ -174,6 +174,8 @@ a.umb-editor-header__close-split-view:hover {
max-width: 50%;
white-space: nowrap;
user-select: none;
span {
text-overflow: ellipsis;
overflow: hidden;
@@ -189,6 +191,25 @@ a.umb-variant-switcher__toggle {
color: @ui-action-discreet-type-hover;
}
}
&.--error {
&::before {
content: '!';
position: absolute;
top: -8px;
right: -10px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 10px;
text-align: center;
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
}
}
}
.umb-variant-switcher__expand {
@@ -205,20 +226,16 @@ a.umb-variant-switcher__toggle {
align-items: center;
border-bottom: 1px solid @gray-9;
position: relative;
&:hover .umb-variant-switcher__name-wrapper {
}
}
.umb-variant-switcher__item:last-child {
border-bottom: none;
}
.umb-variant-switcher_item--current {
.umb-variant-switcher__item.--current {
color: @ui-light-active-type;
}
.umb-variant-switcher_item--current .umb-variant-switcher__name-wrapper {
//background-color: @gray-10;
.umb-variant-switcher__item.--current .umb-variant-switcher__name-wrapper {
border-left: 4px solid @ui-active;
}
@@ -227,7 +244,7 @@ a.umb-variant-switcher__toggle {
outline: none;
}
.umb-variant-switcher_item--not-allowed:not(.umb-variant-switcher_item--current) .umb-variant-switcher__name-wrapper:hover {
.umb-variant-switcher__item.--not-allowed:not(.--current) .umb-variant-switcher__name-wrapper:hover {
//background-color: @white !important;
cursor: default;
}
@@ -237,6 +254,29 @@ a.umb-variant-switcher__toggle {
cursor: pointer;
}
.umb-variant-switcher__item.--error {
.umb-variant-switcher__name {
color: @red;
&::after {
content: '!';
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 5px;
top: -6px;
width: 14px;
height: 14px;
border-radius: 7px;
font-size: 8px;
text-align: center;
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
}
}
}
.umb-variant-switcher__name-wrapper {
font-size: 14px;
flex: 1;
@@ -20,6 +20,15 @@
.umb-logviewer__sidebar {
flex: 0 0 @sidebarwidth;
.flatpickr-input {
background-color: @white;
border: 0;
width: 100%;
text-align: center;
font-size: larger;
padding-top: 20px;
}
}
@media (max-width: 768px) {
@@ -72,3 +72,7 @@
line-height: 20px;
}
}
.mce-fullscreen {
position:absolute;
}
+1
View File
@@ -228,6 +228,7 @@ app.config(function ($routeProvider) {
},
reloadOnSearch: false,
reloadOnUrl: false,
resolve: canRoute(true)
})
.otherwise({ redirectTo: '/login' });
@@ -28,11 +28,9 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
searchFromName: null,
showSearch: false,
results: [],
selectedSearchResults: [],
ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes
selectedSearchResults: []
};
$scope.customTreeParams = dialogOptions.ignoreUserStartNodes ? "ignoreUserStartNodes=" + dialogOptions.ignoreUserStartNodes : "";
$scope.showTarget = $scope.model.hideTarget !== true;
// this ensures that we only sync the tree once and only when it's ready
@@ -89,11 +87,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
});
// get the content properties to build the anchor name list
var options = {};
options.ignoreUserStartNodes = dialogOptions.ignoreUserStartNodes;
contentResource.getById(id, options).then(function (resp) {
contentResource.getById(id).then(function (resp) {
handleContentTarget(resp);
});
}
@@ -143,11 +137,9 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
if (args.node.id < 0) {
$scope.model.target.url = "/";
} else {
var options = {};
options.ignoreUserStartNodes = dialogOptions.ignoreUserStartNodes;
contentResource.getById(args.node.id, options).then(function (resp) {
contentResource.getById(args.node.id).then(function (resp) {
handleContentTarget(resp);
});
}
@@ -170,17 +162,9 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
$scope.switchToMediaPicker = function () {
userService.getCurrentUser().then(function (userData) {
var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0];
var startNodeIsVirtual = userData.startMediaIds.length !== 1;
if (dialogOptions.ignoreUserStartNodes) {
startNodeId = -1;
startNodeIsVirtual = true;
}
var mediaPicker = {
startNodeId: startNodeId,
startNodeIsVirtual: startNodeIsVirtual,
ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes,
startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0],
startNodeIsVirtual: userData.startMediaIds.length !== 1,
submit: function (model) {
var media = model.selection[0];
@@ -66,7 +66,6 @@
search-from-id="{{searchInfo.searchFromId}}"
search-from-name="{{searchInfo.searchFromName}}"
show-search="{{searchInfo.showSearch}}"
ignore-user-start-nodes="{{searchInfo.ignoreUserStartNodes}}"
section="{{section}}">
</umb-tree-search-box>
@@ -83,7 +82,6 @@
section="content"
hideheader="true"
hideoptions="true"
customtreeparams="{{customTreeParams}}"
api="dialogTreeApi"
on-init="onTreeInit()"
enablelistviewexpand="true"
@@ -1,7 +1,7 @@
//used for the media picker dialog
angular.module("umbraco")
.controller("Umbraco.Editors.MediaPickerController",
function ($scope, mediaResource, entityResource, mediaHelper, mediaTypeHelper, eventsService, userService, treeService, localStorageService, localizationService, editorService) {
function($scope, mediaResource, entityResource, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService) {
if (!$scope.model.title) {
localizationService.localizeMany(["defaultdialogs_selectMedia", "general_includeFromsubFolders"])
@@ -20,13 +20,11 @@ angular.module("umbraco")
$scope.showDetails = dialogOptions.showDetails;
$scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false;
$scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1;
$scope.ignoreUserStartNodes = Object.toBoolean(dialogOptions.ignoreUserStartNodes);
$scope.cropSize = dialogOptions.cropSize;
$scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId");
$scope.lockedFolder = true;
$scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false;
var userStartNodes = [];
var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings;
var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles);
if ($scope.onlyImages) {
@@ -56,8 +54,7 @@ angular.module("umbraco")
pageSize: 100,
totalItems: 0,
totalPages: 0,
filter: "",
ignoreUserStartNodes: $scope.model.ignoreUserStartNodes
filter: ''
};
//preload selected item
@@ -67,19 +64,15 @@ angular.module("umbraco")
}
function onInit() {
userService.getCurrentUser().then(function(userData) {
userStartNodes = userData.startMediaIds;
if ($scope.startNodeId !== -1) {
entityResource.getById($scope.startNodeId, "media")
.then(function(ent) {
$scope.startNodeId = ent.id;
run();
});
} else {
run();
}
});
if ($scope.startNodeId !== -1) {
entityResource.getById($scope.startNodeId, "media")
.then(function (ent) {
$scope.startNodeId = ent.id;
run();
});
} else {
run();
}
}
function run() {
@@ -150,7 +143,7 @@ angular.module("umbraco")
}
};
$scope.gotoFolder = function (folder) {
$scope.gotoFolder = function(folder) {
if (!$scope.multiPicker) {
deselectAllImages($scope.model.selection);
}
@@ -159,10 +152,8 @@ angular.module("umbraco")
folder = { id: -1, name: "Media", icon: "icon-folder" };
}
var options = {};
if (folder.id > 0) {
options.ignoreUserStartNodes = $scope.model.ignoreUserStartNodes;
entityResource.getAncestors(folder.id, "media", options)
entityResource.getAncestors(folder.id, "media")
.then(function(anc) {
$scope.path = _.filter(anc,
function(f) {
@@ -178,26 +169,13 @@ angular.module("umbraco")
$scope.path = [];
}
$scope.lockedFolder = (folder.id === -1 && $scope.model.startNodeIsVirtual) || hasFolderAccess(folder) === false;
$scope.lockedFolder = folder.id === -1 && $scope.model.startNodeIsVirtual;
$scope.currentFolder = folder;
localStorageService.set("umbLastOpenedMediaNodeId", folder.id);
options.ignoreUserStartNodes = $scope.ignoreUserStartNodes;
return getChildren(folder.id, options);
return getChildren(folder.id);
};
function hasFolderAccess(node) {
var nodePath = node.path ? node.path.split(',') : [node.id];
for (var i = 0; i < nodePath.length; i++) {
if (userStartNodes.indexOf(parseInt(nodePath[i])) !== -1)
return true;
}
return false;
}
$scope.clickHandler = function(image, event, index) {
if (image.isFolder) {
if ($scope.disableFolderSelect) {
@@ -321,8 +299,7 @@ angular.module("umbraco")
pageSize: 100,
totalItems: 0,
totalPages: 0,
filter: "",
ignoreUserStartNodes: $scope.model.ignoreUserStartNodes
filter: ''
};
getChildren($scope.currentFolder.id);
}
@@ -390,9 +367,9 @@ angular.module("umbraco")
}
}
function getChildren(id, options) {
function getChildren(id) {
$scope.loading = true;
return mediaResource.getChildren(id, options)
return mediaResource.getChildren(id)
.then(function(data) {
$scope.searchOptions.filter = "";
$scope.images = data.items ? data.items : [];
@@ -36,7 +36,6 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController",
selectedSearchResults: []
}
vm.startNodeId = $scope.model.startNodeId;
vm.ignoreUserStartNodes = $scope.model.ignoreUserStartNodes;
//Used for toggling an empty-state message
//Some trees can have no items (dictionary & forms email templates)
vm.hasItems = true;
@@ -172,9 +171,6 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController",
if (vm.startNodeId) {
queryParams["startNodeId"] = $scope.model.startNodeId;
}
if (vm.ignoreUserStartNodes) {
queryParams["ignoreUserStartNodes"] = $scope.model.ignoreUserStartNodes;
}
if (vm.selectedLanguage && vm.selectedLanguage.id) {
queryParams["culture"] = vm.selectedLanguage.culture;
}
@@ -27,7 +27,7 @@
<a ng-click="vm.selectLanguage(language)" ng-repeat="language in vm.languages" href="">{{language.name}}</a>
</div>
</div>
<div class="umb-control-group">
<umb-tree-search-box
ng-if="vm.enableSearh"
@@ -36,7 +36,6 @@
search-from-id="{{vm.searchInfo.searchFromId}}"
search-from-name="{{vm.searchInfo.searchFromName}}"
show-search="{{vm.searchInfo.showSearch}}"
ignore-user-start-nodes="{{vm.ignoreUserStartNodes}}"
section="{{vm.section}}">
</umb-tree-search-box>
</div>
@@ -12,7 +12,7 @@
<ins class="umb-language-picker__expand" ng-class="{'icon-navigation-down': !page.languageSelectorIsOpen, 'icon-navigation-up': page.languageSelectorIsOpen}" class="icon-navigation-right">&nbsp;</ins>
</div>
<div class="umb-language-picker__dropdown" ng-if="page.languageSelectorIsOpen">
<a class="umb-language-picker__dropdown-item" ng-class="{'umb-language-picker__dropdown-item--current': language.active, 'bold': language.isDefault}" ng-click="selectLanguage(language)" ng-repeat="language in languages" href="">{{language.name}}</a>
<a class="umb-language-picker__dropdown-item" ng-class="{'umb-language-picker__dropdown-item--current': language.active}" ng-click="selectLanguage(language)" ng-repeat="language in languages" href="">{{language.name}}</a>
</div>
</div>
@@ -29,7 +29,7 @@
autocomplete="off" maxlength="255" />
</ng-form>
<a ng-if="content.variants.length > 0 && hideChangeVariant !== true" class="umb-variant-switcher__toggle" href="" ng-click="vm.dropdownOpen = !vm.dropdownOpen">
<a ng-if="content.variants.length > 0 && hideChangeVariant !== true" class="umb-variant-switcher__toggle" href="" ng-click="vm.dropdownOpen = !vm.dropdownOpen" ng-class="{'--error': vm.errorsOnOtherVariants}">
<span>{{vm.currentVariant.language.name}}</span>
<ins class="umb-variant-switcher__expand" ng-class="{'icon-navigation-down': !vm.dropdownOpen, 'icon-navigation-up': vm.dropdownOpen}">&nbsp;</ins>
</a>
@@ -39,9 +39,9 @@
</span>
<umb-dropdown ng-if="vm.dropdownOpen" style="min-width: 100%; max-height: 250px; overflow-y: auto; margin-top: 5px;" on-close="vm.dropdownOpen = false" umb-keyboard-list>
<umb-dropdown-item class="umb-variant-switcher__item" ng-class="{'umb-variant-switcher_item--current': variant.active, 'umb-variant-switcher_item--not-allowed': variantIsOpen(variant.language.culture)}" ng-repeat="variant in content.variants">
<umb-dropdown-item class="umb-variant-switcher__item" ng-class="{'--current': variant.active, '--not-allowed': variantIsOpen(variant.language.culture), '--error': variantHasError(variant.language.culture)}" ng-repeat="variant in content.variants">
<a href="" class="umb-variant-switcher__name-wrapper" ng-click="selectVariant($event, variant)" prevent-default>
<span class="umb-variant-switcher__name" ng-class="{'bold': variant.language.isDefault}">{{variant.language.name}}</span>
<span class="umb-variant-switcher__name">{{variant.language.name}}</span>
<umb-variant-state variant="variant" class="umb-variant-switcher__state"></umb-variant-state>
</a>
<div ng-if="splitViewOpen !== true && !variant.active" class="umb-variant-switcher__split-view" ng-click="openInSplitView($event, variant)">Open in split view</div>
@@ -7,7 +7,7 @@
</a>
</li>
<li ng-repeat="pgn in pagination track by pgn.val"
<li ng-repeat="pgn in pagination track by $index"
ng-class="{active:pgn.isActive}">
<a href="#" ng-click="goToPage(pgn.val - 1)" prevent-default
@@ -36,7 +36,6 @@
function(err) {
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: err
});
@@ -6,7 +6,7 @@
* @description
* The controller for the content editor
*/
function ContentEditController($scope, $rootScope, $routeParams, contentResource) {
function ContentEditController($scope, $routeParams, contentResource) {
var infiniteMode = $scope.model && $scope.model.infiniteMode;
@@ -24,13 +24,17 @@ function ContentEditController($scope, $rootScope, $routeParams, contentResource
$scope.page = $routeParams.page;
$scope.isNew = infiniteMode ? $scope.model.create : $routeParams.create;
//load the default culture selected in the main tree if any
$scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture;
$scope.culture = $routeParams.cculture ? $routeParams.cculture : ($routeParams.mculture === "true");
//Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered.
//This is so we can listen to changes on the cculture parameter since that will not cause a route change
// and then we can pass in the updated culture to the editor
//and then we can pass in the updated culture to the editor.
//This will also execute when we are redirecting from creating an item to a newly created item since that
//will not cause a route change and so we can update the isNew and contentId flags accordingly.
$scope.$on('$routeUpdate', function (event, next) {
$scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture;
$scope.isNew = next.params.create === "true";
$scope.contentId = infiniteMode ? $scope.model.id : $routeParams.id;
});
}
@@ -47,9 +47,13 @@ function ContentBlueprintEditController($scope, $routeParams, contentResource) {
//Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered.
//This is so we can listen to changes on the cculture parameter since that will not cause a route change
// and then we can pass in the updated culture to the editor
//and then we can pass in the updated culture to the editor.
//This will also execute when we are redirecting from creating an item to a newly created item since that
//will not cause a route change and so we can update the isNew and contentId flags accordingly.
$scope.$on('$routeUpdate', function (event, next) {
$scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture;
$scope.isNew = $routeParams.id === "-1";
$scope.contentId = $routeParams.id;
});
}
@@ -174,7 +174,6 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic
//NOTE: in the case of data type values we are setting the orig/new props
// to be the same thing since that only really matters for content/media.
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: err
});
@@ -91,7 +91,6 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes
function (err) {
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: err
});
@@ -317,10 +317,6 @@
saveMethod: contentTypeResource.save,
scope: $scope,
content: vm.contentType,
//We do not redirect on failure for doc types - this is because it is not possible to actually save the doc
// type when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
// we need to rebind... the IDs that have been created!
rebindCallback: function (origContentType, savedContentType) {
vm.contentType.id = savedContentType.id;
@@ -39,7 +39,7 @@
<tr ng-repeat="language in vm.languages track by language.id" ng-click="vm.editLanguage(language)" style="cursor: pointer;">
<td>
<i class="icon-globe" style="color: #BBBABF; margin-right: 5px;"></i>
<span ng-class="{'bold': language.isDefault}">{{ language.name }}</span>
<span>{{ language.name }}</span>
</td>
<td>
<span>{{ language.culture }}</span>
@@ -22,16 +22,32 @@
}
};
let querystring = $location.search();
if(querystring.startDate){
vm.startDate = querystring.startDate;
}else{
vm.startDate = new Date(Date.now());
vm.startDate.setDate(vm.startDate.getDate()-1);
vm.startDate = vm.startDate.toIsoDateString();
}
if(querystring.endDate){
vm.endDate = querystring.endDate;
}else{
vm.endDate = new Date(Date.now()).toIsoDateString();
}
vm.period = [vm.startDate, vm.endDate];
//functions
vm.searchLogQuery = searchLogQuery;
vm.findMessageTemplate = findMessageTemplate;
function preFlightCheck(){
vm.loading = true;
//Do our pre-flight check (to see if we can view logs)
//IE the log file is NOT too big such as 1GB & crash the site
logViewerResource.canViewLogs().then(function(result){
logViewerResource.canViewLogs(vm.startDate, vm.endDate).then(function(result){
vm.loading = false;
vm.canLoadLogs = result;
@@ -46,7 +62,7 @@
function init() {
vm.loading = true;
var savedSearches = logViewerResource.getSavedSearches().then(function (data) {
vm.searches = data;
},
@@ -80,11 +96,11 @@
]
});
var numOfErrors = logViewerResource.getNumberOfErrors().then(function (data) {
var numOfErrors = logViewerResource.getNumberOfErrors(vm.startDate, vm.endDate).then(function (data) {
vm.numberOfErrors = data;
});
var logCounts = logViewerResource.getLogLevelCounts().then(function (data) {
var logCounts = logViewerResource.getLogLevelCounts(vm.startDate, vm.endDate).then(function (data) {
vm.logTypeData = [];
vm.logTypeData.push(data.Information);
vm.logTypeData.push(data.Debug);
@@ -93,7 +109,7 @@
vm.logTypeData.push(data.Fatal);
});
var commonMsgs = logViewerResource.getMessageTemplates().then(function(data){
var commonMsgs = logViewerResource.getMessageTemplates(vm.startDate, vm.endDate).then(function(data){
vm.commonLogMessages = data;
});
@@ -108,7 +124,7 @@
}
function searchLogQuery(logQuery){
$location.path("/settings/logViewer/search").search({lq: logQuery});
$location.path("/settings/logViewer/search").search({lq: logQuery, startDate: vm.startDate, endDate: vm.endDate});
}
function findMessageTemplate(template){
@@ -116,8 +132,38 @@
searchLogQuery(logQuery);
}
preFlightCheck();
preFlightCheck();
/////////////////////
vm.config = {
enableTime: false,
dateFormat: "Y-m-d",
time_24hr: false,
mode: "range",
maxDate: "today",
conjunction: " to "
};
vm.dateRangeChange = function(selectedDates, dateStr, instance) {
if(selectedDates.length > 0){
vm.startDate = selectedDates[0].toIsoDateString();
vm.endDate = selectedDates[selectedDates.length-1].toIsoDateString(); // Take the last date as end
if(vm.startDate === vm.endDate){
vm.period = [vm.startDate];
}else{
vm.period = [vm.startDate, vm.endDate];
}
preFlightCheck();
}
}
}
angular.module("umbraco").controller("Umbraco.Editors.LogViewer.OverviewController", LogViewerOverviewController);
@@ -14,45 +14,45 @@
<umb-load-indicator ng-if="vm.loading"></umb-load-indicator>
<!-- Warning message (if unable to view log files) -->
<div ng-show="!vm.loading && !vm.canLoadLogs">
<umb-box>
<umb-box-header title="Unable to view logs"/>
<umb-box-content>
<p>Today's log file is too large to be viewed and would cause performance problems.</p>
<p>If you need to view the log files, try opening them manually</p>
</umb-box-content>
</umb-box>
</div>
<div class="umb-logviewer" ng-show="!vm.loading && vm.canLoadLogs">
<div class="umb-logviewer" ng-show="!vm.loading">
<div class="umb-logviewer__main-content">
<!-- Saved Searches -->
<umb-box>
<umb-box-header title="Saved Searches"></umb-box-header>
<umb-box-content>
<table>
<tr>
<td>
<a ng-click="vm.searchLogQuery()" title="View all Logs" class="btn btn-link">All Logs <i class="icon-search"></i></a>
</td>
</tr>
<!-- Fetch saved searches -->
<tr ng-repeat="search in vm.searches">
<td>
<a ng-click="vm.searchLogQuery(search.query)" title="{{search.name}}" class="btn btn-link">{{search.name}} <i class="icon-search"></i></a>
</td>
</tr>
</table>
</umb-box-content>
</umb-box>
<div ng-show="!vm.canLoadLogs">
<umb-box>
<umb-box-header title="Unable to view logs"/>
<umb-box-content>
<p>Today's log file is too large to be viewed and would cause performance problems.</p>
<p>If you need to view the log files, try opening them manually</p>
</umb-box-content>
</umb-box>
</div>
<div ng-show="vm.canLoadLogs">
<!-- Saved Searches -->
<umb-box>
<umb-box-header title="Saved Searches"></umb-box-header>
<umb-box-content>
<table>
<tr>
<td>
<a ng-click="vm.searchLogQuery()" title="View all Logs" class="btn btn-link">All Logs <i class="icon-search"></i></a>
</td>
</tr>
<!-- Fetch saved searches -->
<tr ng-repeat="search in vm.searches">
<td>
<a ng-click="vm.searchLogQuery(search.query)" title="{{search.name}}" class="btn btn-link">{{search.name}} <i class="icon-search"></i></a>
</td>
</tr>
</table>
</umb-box-content>
</umb-box>
<!-- List of top X common log messages -->
<umb-box>
<umb-box-header title="Common Log Messages"></umb-box-header>
<umb-box-content class="block-form">
<em>Total Unique Message types</em>: {{ vm.commonLogMessages.length }}
<table class="table table-hover">
<tbody>
<!-- List of top X common log messages -->
<umb-box>
<umb-box-header title="Common Log Messages"></umb-box-header>
<umb-box-content class="block-form">
<em>Total Unique Message types</em>: {{ vm.commonLogMessages.length }}
<table class="table table-hover">
<tbody>
<tr ng-repeat="template in vm.commonLogMessages | limitTo:vm.commonLogMessagesCount" ng-click="vm.findMessageTemplate(template)" style="cursor: pointer;">
<td>
{{ template.MessageTemplate }}
@@ -61,38 +61,54 @@
{{ template.Count }}
</td>
</tr>
</tbody>
</table>
<a class="btn btn-primary" ng-if="vm.commonLogMessagesCount < vm.commonLogMessages.length" ng-click="vm.commonLogMessagesCount = vm.commonLogMessagesCount +10">Show More</a>
</umb-box-content>
</umb-box>
</tbody>
</table>
<a class="btn btn-primary" ng-if="vm.commonLogMessagesCount < vm.commonLogMessages.length" ng-click="vm.commonLogMessagesCount = vm.commonLogMessagesCount +10">Show More</a>
</umb-box-content>
</umb-box>
</div>
</div>
<div class="umb-logviewer__sidebar">
<!-- No of Errors -->
<umb-box ng-click="vm.searchLogQuery('Has(@Exception)')" style="cursor:pointer;">
<umb-box-header title="Number of Errors"></umb-box-header>
<umb-box-content class="block-form" style="font-size: 40px; font-weight:900; text-align:center; color:#fe6561;">
{{ vm.numberOfErrors }}
</umb-box-content>
</umb-box>
<!-- Chart of diff log types -->
<!-- Time period -->
<umb-box>
<umb-box-header title="Log Types"></umb-box-header>
<umb-box-content class="block-form">
<umb-box-header title="Time Period"></umb-box-header>
<canvas chart-doughnut
chart-data="vm.logTypeData"
chart-labels="vm.logTypeLabels"
chart-colors="vm.logTypeColors"
chart-options="vm.chartOptions">
</canvas>
</umb-box-content>
<umb-flatpickr
class="datepicker"
ng-model="vm.period"
options="vm.config"
on-close="vm.dateRangeChange(selectedDates, dateStr, instance)">
</umb-flatpickr>
</umb-box>
<div ng-show=" vm.canLoadLogs">
<!-- No of Errors -->
<umb-box ng-click="vm.searchLogQuery('Has(@Exception)')" style="cursor:pointer;">
<umb-box-header title="Number of Errors"></umb-box-header>
<umb-box-content class="block-form" style="font-size: 40px; font-weight:900; text-align:center; color:#fe6561;">
{{ vm.numberOfErrors }}
</umb-box-content>
</umb-box>
<!-- Chart of diff log types -->
<umb-box>
<umb-box-header title="Log Types"></umb-box-header>
<umb-box-content class="block-form">
<canvas chart-doughnut
chart-data="vm.logTypeData"
chart-labels="vm.logTypeLabels"
chart-colors="vm.logTypeColors"
chart-options="vm.chartOptions">
</canvas>
</umb-box-content>
</umb-box>
</div>
</div>
</div>
</umb-editor-container>
</umb-editor-view>
</div>
@@ -96,6 +96,14 @@
vm.logOptions.filterExpression = querystring.lq;
}
if(querystring.startDate){
vm.logOptions.startDate = querystring.startDate;
}
if(querystring.endDate){
vm.logOptions.endDate = querystring.endDate;
}
vm.loading = true;
logViewerResource.getSavedSearches().then(function (data) {
@@ -270,7 +278,11 @@
submitButtonLabel: "Save Search",
disableSubmitButton: true,
view: "logviewersearch",
query: vm.logOptions.filterExpression,
query: {
filterExpression: vm.logOptions.filterExpression,
startDate: vm.logOptions.startDate,
endDate: vm.logOptions.endDate
},
submit: function (model) {
//Resource call with two params (name & query)
//API that opens the JSON and adds it to the bottom
@@ -35,7 +35,6 @@ function MacrosEditController($scope, $q, $routeParams, macroResource, editorSta
vm.page.saveButtonState = "success";
}, function (error) {
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: error
});
@@ -184,7 +184,6 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource,
contentEditingHelper.handleSuccessfulSave({
scope: $scope,
savedContent: data,
redirectOnSuccess: !infiniteMode,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data)
});
@@ -206,7 +205,6 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource,
contentEditingHelper.handleSaveError({
err: err,
redirectOnFailure: !infiniteMode,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data)
});
@@ -269,10 +269,6 @@
saveMethod: mediaTypeResource.save,
scope: $scope,
content: vm.contentType,
//We do not redirect on failure for doc types - this is because it is not possible to actually save the doc
// type when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
// we need to rebind... the IDs that have been created!
rebindCallback: function (origContentType, savedContentType) {
vm.contentType.id = savedContentType.id;
@@ -137,6 +137,13 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR
$scope.page.saveButtonState = "busy";
//anytime a user is changing a member's password without the oldPassword, we are in effect resetting it so we need to set that flag here
var passwordProp = _.find(contentEditingHelper.getAllProps($scope.content), function (e) { return e.alias === '_umb_password' });
if (!passwordProp.value.reset) {
//so if the admin is not explicitly resetting the password, flag it for resetting if a new password is being entered
passwordProp.value.reset = !passwordProp.value.oldPassword && passwordProp.config.allowManuallyChangingPassword;
}
memberResource.save($scope.content, $routeParams.create, fileManager.getFiles())
.then(function(data) {
@@ -161,7 +168,6 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR
}, function (err) {
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: err,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data)
});
@@ -86,7 +86,6 @@ function MemberGroupsEditController($scope, $routeParams, appState, navigationSe
}, function (err) {
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: err
});
@@ -180,10 +180,6 @@
saveMethod: memberTypeResource.save,
scope: $scope,
content: vm.contentType,
//We do not redirect on failure for doc types - this is because it is not possible to actually save the doc
// type when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
// we need to rebind... the IDs that have been created!
rebindCallback: function (origContentType, savedContentType) {
vm.contentType.id = savedContentType.id;
@@ -65,10 +65,6 @@
saveMethod: codefileResource.save,
scope: $scope,
content: vm.partialViewMacro,
// We do not redirect on failure for partial view macros - this is because it is not possible to actually save the partial view
// when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
rebindCallback: function (orignal, saved) {}
}).then(function (saved) {
// create macro if needed
@@ -83,10 +83,6 @@
saveMethod: codefileResource.save,
scope: $scope,
content: vm.partialView,
//We do not redirect on failure for partialviews - this is because it is not possible to actually save the partialviews
// type when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
rebindCallback: function (orignal, saved) {}
}).then(function (saved) {
@@ -80,7 +80,6 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
showOpenButton: false,
showEditButton: false,
showPathOnHover: false,
ignoreUserStartNodes: false,
maxNumber: 1,
minNumber: 0,
startNode: {
@@ -118,8 +117,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
$scope.model.config.showOpenButton = Object.toBoolean($scope.model.config.showOpenButton);
$scope.model.config.showEditButton = Object.toBoolean($scope.model.config.showEditButton);
$scope.model.config.showPathOnHover = Object.toBoolean($scope.model.config.showPathOnHover);
$scope.model.config.ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes);
var entityType = $scope.model.config.startNode.type === "member"
? "Member"
: $scope.model.config.startNode.type === "media"
@@ -135,7 +133,6 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
entityType: entityType,
filterCssClass: "not-allowed not-published",
startNodeId: null,
ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes,
currentNode: editorState ? editorState.current : null,
callback: function (data) {
if (angular.isArray(data)) {
@@ -14,7 +14,6 @@
sortable="!sortableOptions.disabled"
allow-remove="allowRemoveButton"
allow-open="model.config.showOpenButton && allowOpenButton && !dialogEditor"
ignore-user-startnodes="model.config.ignoreUserStartNodes"
on-remove="remove($index)"
on-open="openContentEditor(node)">
</umb-node-preview>
@@ -1,32 +1,25 @@
angular.module("umbraco")
.controller("Umbraco.PropertyEditors.Grid.MediaController",
function ($scope, $timeout, userService, editorService) {
var ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes);
$scope.thumbnailUrl = getThumbnailUrl();
if (!$scope.model.config.startNodeId) {
if (ignoreUserStartNodes === true) {
$scope.model.config.startNodeId = -1;
$scope.model.config.startNodeIsVirtual = true;
} else {
userService.getCurrentUser().then(function (userData) {
$scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0];
$scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1;
});
}
userService.getCurrentUser().then(function (userData) {
$scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0];
$scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1;
});
}
$scope.setImage = function(){
var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined;
var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined;
var mediaPicker = {
startNodeId: startNodeId,
startNodeIsVirtual: startNodeIsVirtual,
ignoreUserStartNodes: ignoreUserStartNodes,
cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined,
showDetails: true,
disableFolderSelect: true,
@@ -34,7 +34,7 @@ angular.module("umbraco")
// Sortable options
// *********************************************
var draggedRteSettings;
var draggedRteSettings;// holds a dictionary of RTE settings to remember when dragging things around.
$scope.sortableOptionsRow = {
distance: 10,
@@ -92,7 +92,7 @@ angular.module("umbraco")
}
};
var notIncludedRte = [];
var notIncludedRte = [];// used for RTEs that has been affected by the sorting
var cancelMove = false;
var startingArea;
$scope.sortableOptionsCell = {
@@ -134,17 +134,17 @@ angular.module("umbraco")
(startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1)) {
$scope.$apply(function () {
event.target.getScope_HackForSortable().area.dropNotAllowed = true;
area.dropNotAllowed = true;
});
ui.placeholder.hide();
cancelMove = true;
}
else {
if (event.target.getScope_HackForSortable().area.controls.length == 0) {
if (area.controls.length == 0) {
$scope.$apply(function () {
event.target.getScope_HackForSortable().area.dropOnEmpty = true;
area.dropOnEmpty = true;
});
ui.placeholder.hide();
} else {
@@ -169,16 +169,32 @@ angular.module("umbraco")
ui.item.sortable.cancel();
}
ui.item.parents(".umb-cell-content").find(".umb-rte").each(function (key, value) {
var v1 = value.id;
var rteId = value.id;
if ($.inArray(v1, notIncludedRte) < 0) {
notIncludedRte.splice(0, 0, v1);
if ($.inArray(rteId, notIncludedRte) < 0) {
// remember this RTEs settings, cause we need to update it later.
var editor = _.findWhere(tinyMCE.editors, { id: rteId })
if (editor) {
draggedRteSettings[rteId] = editor.settings;
}
notIncludedRte.splice(0, 0, rteId);
}
});
}
else {
$(event.target).find(".umb-rte").each(function () {
if ($.inArray($(this).attr("id"), notIncludedRte) < 0) {
var rteId = $(this).attr("id");
if ($.inArray(rteId, notIncludedRte) < 0) {
// remember this RTEs settings, cause we need to update it later.
var editor = _.findWhere(tinyMCE.editors, { id: rteId })
if (editor) {
draggedRteSettings[rteId] = editor.settings;
}
notIncludedRte.splice(0, 0, $(this).attr("id"));
}
});
@@ -196,17 +212,20 @@ angular.module("umbraco")
ui.item[0].style.opacity = "0.5";
// reset dragged RTE settings in case a RTE isn't dragged
draggedRteSettings = undefined;
draggedRteSettings = {};
notIncludedRte = [];
ui.item[0].style.display = "block";
ui.item.find(".umb-rte").each(function (key, value) {
notIncludedRte = [];
var rteId = value.id;
var editors = _.findWhere(tinyMCE.editors, { id: rteId });
// remember this RTEs settings, cause we need to update it later.
var editor = _.findWhere(tinyMCE.editors, { id: rteId });
// save the dragged RTE settings
if (editors) {
draggedRteSettings = editors.settings;
if (editor) {
draggedRteSettings[rteId] = editor.settings;
// remove the dragged RTE
tinyMCE.execCommand("mceRemoveEditor", false, rteId);
@@ -223,22 +242,29 @@ angular.module("umbraco")
ui.item.offsetParent().find(".umb-rte").each(function (key, value) {
var rteId = value.id;
if ($.inArray(rteId, notIncludedRte) < 0) {
var editor = _.findWhere(tinyMCE.editors, { id: rteId });
if (editor) {
draggedRteSettings[rteId] = editor.settings;
}
// add all dragged's neighbouring RTEs in the new cell
notIncludedRte.splice(0, 0, rteId);
}
});
// reconstruct the dragged RTE (could be undefined when dragging something else than RTE)
if (draggedRteSettings !== undefined) {
tinyMCE.init(draggedRteSettings);
}
_.forEach(notIncludedRte, function (id) {
_.forEach(notIncludedRte, function (rteId) {
// reset all the other RTEs
if (draggedRteSettings === undefined || id !== draggedRteSettings.id) {
var rteSettings = _.findWhere(tinyMCE.editors, { id: id }).settings;
tinyMCE.execCommand("mceRemoveEditor", false, id);
tinyMCE.init(rteSettings);
if (draggedRteSettings === undefined || rteId !== draggedRteSettings.id) {
tinyMCE.execCommand("mceRemoveEditor", false, rteId);
if (draggedRteSettings[rteId]) {
tinyMCE.init(draggedRteSettings[rteId]);
}
}
});
@@ -3,19 +3,19 @@
<div ng-if="!vm.languages">
<p><localize key="prompt_confirmListViewPublish"></localize></p>
</div>
<div ng-if="vm.languages.length > 1">
<div class="mb3">
<p><localize key="content_languagesToPublish"></localize></p>
</div>
<div ng-if="vm.loading" style="min-height: 50px; position: relative;">
<umb-load-indicator></umb-load-indicator>
</div>
<div class="umb-list umb-list--condensed" ng-if="!vm.loading">
<div class="umb-list-item" ng-repeat="language in vm.languages track by language.id">
<ng-form name="publishLanguageSelectorForm">
<div class="flex">
@@ -28,20 +28,20 @@
style="margin-right: 8px;" />
<div>
<label for="{{language.culture}}" style="margin-bottom: 2px;">
<span ng-class="{'bold': language.isDefault}">{{ language.name }}</span>
<span>{{ language.name }}</span>
</label>
<div class="umb-list-item__description">
<span ng-if="language.isMandatory"><localize key="languages_mandatoryLanguage"></localize></span>
</div>
</div>
</div>
</ng-form>
</div>
</div>
</div>
</div>
@@ -4,16 +4,16 @@
<div ng-if="!vm.languages">
<p><localize key="prompt_confirmListViewUnpublish"></localize></p>
</div>
<!-- Multiple languages -->
<div ng-if="vm.languages.length > 0">
<div class="mb3">
<p><localize key="content_languagesToUnpublish"></localize></p>
</div>
<div class="umb-list umb-list--condensed">
<div class="umb-list-item" ng-repeat="language in vm.languages track by language.id">
<ng-form name="unpublishLanguageSelectorForm">
<div class="flex">
@@ -26,21 +26,21 @@
style="margin-right: 8px;" />
<div>
<label for="{{language.culture}}" style="margin-bottom: 2px;">
<span ng-class="{'bold': language.isDefault}">{{ language.name }}</span>
<span>{{ language.name }}</span>
</label>
<div class="umb-list-item__description">
<span ng-if="language.isMandatory"><localize key="languages_mandatoryLanguage"></localize></span>
</div>
</div>
</div>
</ng-form>
</div>
</div>
</div>
</div>
@@ -7,12 +7,9 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false;
var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false;
var disableFolderSelect = $scope.model.config.disableFolderSelect && $scope.model.config.disableFolderSelect !== '0' ? true : false;
var ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes);
$scope.allowEditMedia = false;
$scope.allowAddMedia = false;
function setupViewModel() {
$scope.mediaItems = [];
$scope.ids = [];
@@ -108,16 +105,12 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
}
function init() {
userService.getCurrentUser().then(function (userData) {
if (!$scope.model.config.startNodeId) {
$scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0];
$scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1;
}
if (ignoreUserStartNodes === true) {
$scope.model.config.startNodeId = -1;
$scope.model.config.startNodeIsVirtual = true;
}
// only allow users to add and edit media if they have access to the media section
var hasAccessToMedia = userData.allowedSections.indexOf("media") !== -1;
$scope.allowEditMedia = hasAccessToMedia;
@@ -175,7 +168,6 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
var mediaPicker = {
startNodeId: $scope.model.config.startNodeId,
startNodeIsVirtual: $scope.model.config.startNodeIsVirtual,
ignoreUserStartNodes: ignoreUserStartNodes,
multiPicker: multiPicker,
onlyImages: onlyImages,
disableFolderSelect: disableFolderSelect,
@@ -68,10 +68,9 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en
url: link.url,
target: link.target
} : null;
var linkPicker = {
currentTarget: target,
ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes),
submit: function (model) {
if (model.target.url || model.target.anchor) {
// if an anchor exists, check that it is appropriately prefixed
@@ -25,7 +25,6 @@
section: "content",
treeAlias: "content",
multiPicker: false,
ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes),
idType: $scope.model.config.idType ? $scope.model.config.idType : "int",
submit: function (model) {
select(model.selection[0]);
@@ -48,7 +47,6 @@
section: "content",
treeAlias: "content",
multiPicker: false,
ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes),
idType: $scope.model.config.idType ? $scope.model.config.idType : "udi",
submit: function (model) {
select(model.selection[0]);
@@ -24,13 +24,6 @@
text="{{css.name}}"/>
</div>
</umb-control-group>
<umb-control-group label="Ignore user start nodes" description="Selecting this option allows a user to choose nodes that they normally don't have access to. Note: this applies only to rich text editors in this grid editor.">
<div>
<label>
<umb-checkbox model="model.value.ignoreUserStartNodes"/>
</label>
</div>
</umb-control-group>
<umb-control-group label="Dimensions" description="Set the editor dimensions">
@@ -119,7 +119,6 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource,
}, function (error) {
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: error
});
@@ -45,10 +45,6 @@
saveMethod: codefileResource.save,
scope: $scope,
content: vm.script,
// We do not redirect on failure for scripts - this is because it is not possible to actually save the script
// when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
rebindCallback: function (orignal, saved) {}
}).then(function (saved) {
@@ -68,10 +68,6 @@
saveMethod: codefileResource.save,
scope: $scope,
content: vm.stylesheet,
// We do not redirect on failure for style sheets - this is because it is not possible to actually save the style sheet
// when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
rebindCallback: function (orignal, saved) {}
}).then(function (saved) {
@@ -83,10 +83,6 @@
saveMethod: templateResource.save,
scope: $scope,
content: vm.template,
// We do not redirect on failure for templates - this is because it is not possible to actually save the template
// type when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
rebindCallback: function (orignal, saved) {}
}).then(function (saved) {
@@ -81,10 +81,6 @@
saveMethod: userGroupsResource.saveUserGroup,
scope: $scope,
content: vm.userGroup,
// We do not redirect on failure for users - this is because it is not possible to actually save a user
// when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
rebindCallback: function (orignal, saved) { }
}).then(function (saved) {
@@ -136,7 +136,10 @@
//anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here
if (vm.user.changePassword) {
vm.user.changePassword.reset = !vm.user.changePassword.oldPassword && !vm.user.isCurrentUser;
//NOTE: the check for allowManuallyChangingPassword is due to this legacy user membership provider setting, if that is true, then the current user
//can change their own password without entering their current one (this is a legacy setting since that is a security issue but we need to maintain compat).
//if allowManuallyChangingPassword=false, then we are using default settings and the user will need to enter their old password to change their own password.
vm.user.changePassword.reset = (!vm.user.changePassword.oldPassword && !vm.user.isCurrentUser) || vm.changePasswordModel.config.allowManuallyChangingPassword;
}
vm.page.saveButtonState = "busy";
@@ -169,7 +172,6 @@
}, function (err) {
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: err,
showNotifications: true
});

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