Merge remote-tracking branch 'origin/dev-v7.8' into temp-U4-10803

# Conflicts:
#	src/Umbraco.Web.UI.Client/bower.json
This commit is contained in:
Shannon
2018-01-12 14:32:54 +11:00
72 changed files with 1619 additions and 935 deletions
+1 -1
View File
@@ -32,7 +32,7 @@
<file src="$BuildTmp$\WebApp\bin\amd64\**" target="UmbracoFiles\bin\amd64" />
<file src="$BuildTmp$\WebApp\bin\x86\**" target="UmbracoFiles\bin\x86" />
<file src="$BuildTmp$\WebApp\config\splashes\**" target="UmbracoFiles\Config\splashes" />
<file src="$BuildTmp$\WebApp\config\BackOfficeTours\**" target="UmbracoFiles\Config\BackOfficeTours" />
<file src="$BuildTmp$\WebApp\config\BackOfficeTours\**" target="Content\Config\BackOfficeTours" />
<file src="$BuildTmp$\WebApp\umbraco\**" target="UmbracoFiles\umbraco" />
<file src="$BuildTmp$\WebApp\umbraco_client\**" target="UmbracoFiles\umbraco_client" />
<file src="$BuildTmp$\WebApp\Media\Web.config" target="Content\Media\Web.config" />
+3 -3
View File
@@ -79,14 +79,14 @@
<add sortOrder="1" alias="dataTypes" application="developer"
xdt:Locator="Match(application,alias)"
xdt:Transform="SetAttributes(sortOrder)" />
<add application="developer" alias="macros" title="Macros" type="umbraco.loadMacros, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="2"
<add initialize="true" sortOrder="2" alias="macros" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.MacroTreeController, umbraco"
xdt:Locator="Match(application,alias)"
xdt:Transform="SetAttributes()" />
<add application="developer" alias="relationTypes" title="Relation Types" type="umbraco.loadRelationTypes, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="4"
xdt:Locator="Match(application,alias)"
xdt:Transform="SetAttributes()" />
<add application="developer" alias="xslt" title="XSLT Files" type="umbraco.loadXslt, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="5"
<add initialize="true" sortOrder="5" alias="xslt" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.XsltTreeController, umbraco"
xdt:Locator="Match(application,alias)"
xdt:Transform="SetAttributes()" />
+1 -1
View File
@@ -12,4 +12,4 @@ using System.Resources;
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("7.8.0")]
[assembly: AssemblyInformationalVersion("7.8.0-beta")]
[assembly: AssemblyInformationalVersion("7.8.0-beta005")]
@@ -146,7 +146,7 @@ namespace Umbraco.Core.Cache
#region Insert
#endregion
private class NoopLocker : DisposableObject
private class NoopLocker : DisposableObjectSlim
{
protected override void DisposeResources()
{ }
+33
View File
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Collections
{
/// <summary>
/// Represents a list of types.
/// </summary>
/// <remarks>Types in the list are, or derive from, or implement, the base type.</remarks>
/// <typeparam name="TBase">The base type.</typeparam>
internal class TypeList<TBase>
{
private readonly List<Type> _list = new List<Type>();
/// <summary>
/// Adds a type to the list.
/// </summary>
/// <typeparam name="T">The type to add.</typeparam>
public void Add<T>()
where T : TBase
{
_list.Add(typeof(T));
}
/// <summary>
/// Determines whether a type is in the list.
/// </summary>
public bool Contains(Type type)
{
return _list.Contains(type);
}
}
}
@@ -73,6 +73,11 @@
/// </summary>
public const string Media = "media";
/// <summary>
/// alias for the macro tree.
/// </summary>
public const string Macros = "macros";
/// <summary>
/// alias for the datatype tree.
/// </summary>
+43 -43
View File
@@ -2,60 +2,60 @@ using System;
using System.Threading;
namespace Umbraco.Core
{
/// <summary>
/// Abstract implementation of IDisposable.
/// </summary>
/// <remarks>
/// Can also be used as a pattern for when inheriting is not possible.
///
{
/// <summary>
/// Abstract implementation of IDisposable.
/// </summary>
/// <remarks>
/// Can also be used as a pattern for when inheriting is not possible.
///
/// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx
/// See also: https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/
///
/// Note: if an object's ctor throws, it will never be disposed, and so if that ctor
/// has allocated disposable objects, it should take care of disposing them.
/// </remarks>
public abstract class DisposableObject : IDisposable
{
private bool _disposed;
private readonly object _locko = new object();
// gets a value indicating whether this instance is disposed.
/// </remarks>
public abstract class DisposableObject : IDisposable
{
private bool _disposed;
private readonly object _locko = new object();
// gets a value indicating whether this instance is disposed.
// for internal tests only (not thread safe)
//TODO make this internal + rename "Disposed" when we can break compatibility
public bool IsDisposed { get { return _disposed; } }
// implements IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// finalizer
~DisposableObject()
{
Dispose(false);
}
public bool IsDisposed { get { return _disposed; } }
// implements IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// finalizer
~DisposableObject()
{
Dispose(false);
}
//TODO make this private, non-virtual when we can break compatibility
protected virtual void Dispose(bool disposing)
{
lock (_locko)
{
if (_disposed) return;
_disposed = true;
}
protected virtual void Dispose(bool disposing)
{
lock (_locko)
{
if (_disposed) return;
_disposed = true;
}
DisposeUnmanagedResources();
if (disposing)
DisposeResources();
}
protected abstract void DisposeResources();
protected virtual void DisposeUnmanagedResources()
{ }
}
DisposeResources();
}
protected abstract void DisposeResources();
protected virtual void DisposeUnmanagedResources()
{ }
}
}
+38
View File
@@ -0,0 +1,38 @@
using System;
namespace Umbraco.Core
{
/// <summary>
/// This should not be use if there are ubmanaged resources to be disposed, use DisposableObject instead
/// </summary>
public abstract class DisposableObjectSlim : IDisposable
{
private bool _disposed;
private readonly object _locko = new object();
// gets a value indicating whether this instance is disposed.
// for internal tests only (not thread safe)
internal bool Disposed { get { return _disposed; } }
// implements IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
lock (_locko)
{
if (_disposed) return;
_disposed = true;
}
if (disposing)
DisposeResources();
}
protected abstract void DisposeResources();
}
}
+8 -8
View File
@@ -11,8 +11,8 @@ namespace Umbraco.Core
/// <summary>
/// Starts the timer and invokes a callback upon disposal. Provides a simple way of timing an operation by wrapping it in a <code>using</code> (C#) statement.
/// </summary>
public class DisposableTimer : DisposableObject
{
public class DisposableTimer : DisposableObjectSlim
{
private readonly ILogger _logger;
private readonly LogType? _logType;
private readonly IProfiler _profiler;
@@ -209,13 +209,13 @@ namespace Umbraco.Core
loggerType,
startMessage(),
completeMessage());
}
}
#endregion
/// <summary>
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObject"/> which handles common required locking logic.
/// </summary>
protected override void DisposeResources()
/// <summary>
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObjectSlim"/> which handles common required locking logic.
/// </summary>
protected override void DisposeResources()
{
if (_profiler != null)
{
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Umbraco.Core.Events
/// <summary>
/// Event messages collection
/// </summary>
public sealed class EventMessages : DisposableObject
public sealed class EventMessages : DisposableObjectSlim
{
private readonly List<EventMessage> _msgs = new List<EventMessage>();
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Umbraco.Core
/// This will use the crypto libs to generate the hash and will try to ensure that
/// strings, etc... are not re-allocated so it's not consuming much memory.
/// </remarks>
internal class HashGenerator : DisposableObject
internal class HashGenerator : DisposableObjectSlim
{
public HashGenerator()
{
+1 -1
View File
@@ -6,7 +6,7 @@ using Umbraco.Core.Logging;
namespace Umbraco.Core.Manifest
{
internal class ManifestWatcher : DisposableObject
internal class ManifestWatcher : DisposableObjectSlim
{
private readonly ILogger _logger;
private readonly List<FileSystemWatcher> _fws = new List<FileSystemWatcher>();
@@ -394,7 +394,7 @@ namespace Umbraco.Core.ObjectResolution
/// WARNING! Do not use this unless you know what you are doing, clear all types registered and instances
/// created. Typically only used if a resolver is no longer used in an application and memory is to be GC'd
/// </summary>
internal void ResetCollections()
internal virtual void ResetCollections()
{
using (new WriteLock(_lock))
{
@@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence
/// it will create one per context, otherwise it will be a global singleton object which is NOT thread safe
/// since we need (at least) a new instance of the database object per thread.
/// </remarks>
internal class DefaultDatabaseFactory : DisposableObject, IDatabaseFactory2
internal class DefaultDatabaseFactory : DisposableObjectSlim, IDatabaseFactory2
{
private readonly string _connectionStringName;
private readonly ILogger _logger;
@@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories
/// <remarks>
/// This is limited to objects that are based in the umbracoNode-table.
/// </remarks>
internal class EntityRepository : DisposableObject, IEntityRepository
internal class EntityRepository : DisposableObjectSlim, IEntityRepository
{
private readonly IDatabaseUnitOfWork _work;
@@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.UnitOfWork;
namespace Umbraco.Core.Persistence.Repositories
{
internal abstract class FileRepository<TId, TEntity> : DisposableObject, IUnitOfWorkRepository, IRepository<TId, TEntity>
internal abstract class FileRepository<TId, TEntity> : DisposableObjectSlim, IUnitOfWorkRepository, IRepository<TId, TEntity>
where TEntity : IFile
{
private IUnitOfWork _work;
@@ -10,7 +10,7 @@ using Umbraco.Core.Scoping;
namespace Umbraco.Core.Persistence.Repositories
{
internal abstract class RepositoryBase : DisposableObject
internal abstract class RepositoryBase : DisposableObjectSlim
{
private readonly IScopeUnitOfWork _work;
private readonly CacheHelper _globalCache;
@@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.UnitOfWork
/// <summary>
/// Represents a scoped unit of work.
/// </summary>
internal class ScopeUnitOfWork : DisposableObject, IScopeUnitOfWork
internal class ScopeUnitOfWork : DisposableObjectSlim, IScopeUnitOfWork
{
private readonly Queue<Operation> _operations = new Queue<Operation>();
private readonly IsolationLevel _isolationLevel;
+5 -5
View File
@@ -1,10 +1,8 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
// General Information about an assembly is controlled through the following
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Umbraco.Core")]
@@ -12,8 +10,8 @@ using System.Security.Permissions;
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyProduct("Umbraco CMS")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
@@ -50,6 +48,8 @@ using System.Security.Permissions;
[assembly: InternalsVisibleTo("Umbraco.Forms.Core.Providers")]
[assembly: InternalsVisibleTo("Umbraco.Forms.Web")]
[assembly: InternalsVisibleTo("Umbraco.Headless")]
//allow this to be mocked in our unit tests
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
@@ -18,7 +18,7 @@ using Task = System.Threading.Tasks.Task;
namespace Umbraco.Core.Security
{
public class BackOfficeUserStore : DisposableObject,
public class BackOfficeUserStore : DisposableObjectSlim,
IUserStore<BackOfficeIdentityUser, int>,
IUserPasswordStore<BackOfficeIdentityUser, int>,
IUserEmailStore<BackOfficeIdentityUser, int>,
@@ -58,7 +58,7 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObject"/> which handles common required locking logic.
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObjectSlim"/> which handles common required locking logic.
/// </summary>
protected override void DisposeResources()
{
+2
View File
@@ -178,6 +178,7 @@
<Compile Include="CodeAnnotations\UmbracoProposedPublicAttribute.cs" />
<Compile Include="Collections\DeepCloneableList.cs" />
<Compile Include="Collections\ListCloneBehavior.cs" />
<Compile Include="Collections\TypeList.cs" />
<Compile Include="ConcurrentHashSet.cs" />
<Compile Include="Configuration\BaseRest\IBaseRestSection.cs" />
<Compile Include="Configuration\BaseRest\IExtension.cs" />
@@ -348,6 +349,7 @@
<Compile Include="Deploy\Direction.cs" />
<Compile Include="Deploy\IFileType.cs" />
<Compile Include="Deploy\IFileTypeCollection.cs" />
<Compile Include="DisposableObjectSlim.cs" />
<Compile Include="EmailSender.cs" />
<Compile Include="Events\EventDefinitionFilter.cs" />
<Compile Include="Events\IDeletingMediaFilesEventArgs.cs" />
@@ -548,7 +548,7 @@ namespace Umbraco.Tests.Scheduling
runner.Shutdown(false, true);
// check that task has been disposed (timer has been killed, etc)
Assert.IsTrue(task.IsDisposed);
Assert.IsTrue(task.Disposed);
}
}
@@ -17,6 +17,7 @@ using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.ControllerTesting;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Web.Editors;
using Umbraco.Web.Features;
using Umbraco.Web.Models.ContentEditing;
using IUser = Umbraco.Core.Models.Membership.IUser;
@@ -26,6 +27,11 @@ namespace Umbraco.Tests.Web.Controllers
[TestFixture]
public class UsersControllerTests : BaseDatabaseFactoryTest
{
protected override void FreezeResolution()
{
FeaturesResolver.Current = new FeaturesResolver(new UmbracoFeatures());
base.FreezeResolution();
}
[Test]
public async void Save_User()
+7 -19
View File
@@ -33,56 +33,44 @@
"clipboard": "1.7.1",
"font-awesome": "~4.2"
},
"install": {
"path": "lib-bower",
"ignore": [
"font-awesome",
"angular",
"bootstrap",
"codemirror"
],
"sources": {
"moment": "bower_components/moment/min/moment-with-locales.js",
"underscore": [
"bower_components/underscore/underscore-min.js",
"bower_components/underscore/underscore-min.map"
],
"jquery": [
"bower_components/jquery/dist/jquery.min.js",
"bower_components/jquery/dist/jquery.min.map"
],
"angular-dynamic-locale": [
"bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js",
"bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js.map"
],
"angular-local-storage": [
"bower_components/angular-local-storage/dist/angular-local-storage.min.js",
"bower_components/angular-local-storage/dist/angular-local-storage.min.js.map"
],
"tinymce": [
"bower_components/tinymce/tinymce.min.js"
],
"typeahead.js": "bower_components/typeahead.js/dist/typeahead.bundle.min.js",
"rgrove-lazyload":"bower_components/rgrove-lazyload/lazyload.js",
"ng-file-upload":"bower_components/ng-file-upload/ng-file-upload.min.js",
"jquery-ui":"bower_components/jquery-ui/jquery-ui.min.js",
"jquery-migrate":"bower_components/jquery-migrate/jquery-migrate.min.js",
"rgrove-lazyload": "bower_components/rgrove-lazyload/lazyload.js",
"ng-file-upload": "bower_components/ng-file-upload/ng-file-upload.min.js",
"jquery-ui": "bower_components/jquery-ui/jquery-ui.min.js",
"jquery-migrate": "bower_components/jquery-migrate/jquery-migrate.min.js",
"clipboard": "bower_components/clipboard/dist/clipboard.min.js"
}
},
"resolutions": {
"moment": ">=2.19.3 <3.0.0"
}
}
@@ -16,52 +16,16 @@ function eventsService($q, $rootScope) {
return {
/** raise an event with a given name, returns an array of promises for each listener */
emit: function (name, args) {
/** raise an event with a given name */
emit: function (name, args) {
//there are no listeners
if (!$rootScope.$$listeners[name]) {
return;
//return [];
}
//send the event
$rootScope.$emit(name, args);
//PP: I've commented out the below, since we currently dont
// expose the eventsService as a documented api
// and think we need to figure out our usecases for this
// since the below modifies the return value of the then on() method
/*
//setup a deferred promise for each listener
var deferred = [];
for (var i = 0; i < $rootScope.$$listeners[name].length; i++) {
deferred.push($q.defer());
}*/
//create a new event args object to pass to the
// $emit containing methods that will allow listeners
// to return data in an async if required
/*
var eventArgs = {
args: args,
reject: function (a) {
deferred.pop().reject(a);
},
resolve: function (a) {
deferred.pop().resolve(a);
}
};*/
/*
//return an array of promises
var promises = _.map(deferred, function(p) {
return p.promise;
});
return promises;*/
},
/** subscribe to a method, or use scope.$on = same thing */
@@ -17,6 +17,7 @@
* Registers all tours from the server and returns a promise
*/
function registerAllTours() {
tours = [];
return tourResource.getTours().then(function(tourFiles) {
angular.forEach(tourFiles, function (tourFile) {
angular.forEach(tourFile.tours, function(newTour) {
@@ -218,7 +219,38 @@
var deferred = $q.defer();
var tours = getTours();
setTourStatuses(tours).then(function() {
var groupedTours = _.groupBy(tours, "group");
var groupedTours = [];
tours.forEach(function (item) {
var groupExists = false;
var newGroup = {
"group": "",
"tours": []
};
groupedTours.forEach(function(group){
// extend existing group if it is already added
if(group.group === item.group) {
if(item.groupOrder) {
group.groupOrder = item.groupOrder
}
groupExists = true;
group.tours.push(item)
}
});
// push new group to array if it doesn't exist
if(!groupExists) {
newGroup.group = item.group;
if(item.groupOrder) {
newGroup.groupOrder = item.groupOrder
}
newGroup.tours.push(item);
groupedTours.push(newGroup);
}
});
deferred.resolve(groupedTours);
});
return deferred.promise;
@@ -124,7 +124,7 @@
function showTourButton(index, tourGroup) {
if(index !== 0) {
var prevTour = tourGroup[index - 1];
var prevTour = tourGroup.tours[index - 1];
if(prevTour.completed) {
return true;
}
@@ -147,12 +147,12 @@
// Finding out, how many tours are completed for the progress circle
angular.forEach(vm.tours, function(group){
var completedTours = 0;
angular.forEach(group, function(tour){
angular.forEach(group.tours, function(tour){
if(tour.completed) {
completedTours++;
}
});
group.completedPercentage = Math.round((completedTours/group.length)*100);
group.completedPercentage = Math.round((completedTours/group.tours.length)*100);
});
}
@@ -12,23 +12,23 @@
<h5 style="margin-bottom: 10px; margin-top: 0;">Tours</h5>
<div ng-repeat="(key,value) in vm.tours" style="margin-bottom: 5px;">
<div ng-repeat="tourGroup in vm.tours | orderBy:'groupOrder'" style="margin-bottom: 5px;">
<div class="umb-help-list">
<a href="" class="umb-help-list-item umb-help-list-item__content flex items-center justify-between" style="text-decoration: none;" ng-click="value.open = !value.open">
<h5 class="umb-help-list-item__group-title"><i style="margin-right: 2px;text-decoration: none;" ng-class="{'icon-navigation-right': !value.open, 'icon-navigation-down': value.open}"></i>
<span ng-if="key !== 'undefined'">{{key}}</span>
<span ng-if="key === 'undefined'">Other</span>
<a href="" class="umb-help-list-item umb-help-list-item__content flex items-center justify-between" style="text-decoration: none;" ng-click="tourGroup.open = !tourGroup.open">
<h5 class="umb-help-list-item__group-title"><i style="margin-right: 2px;text-decoration: none;" ng-class="{'icon-navigation-right': !tourGroup.open, 'icon-navigation-down': tourGroup.open}"></i>
<span ng-if="tourGroup.group !== 'undefined'">{{tourGroup.group}}</span>
<span ng-if="tourGroup.group === 'undefined'">Other</span>
</h5>
<umb-progress-circle
percentage="{{value.completedPercentage}}"
percentage="{{tourGroup.completedPercentage}}"
size="40">
</umb-progress-circle>
</a>
<div ng-if="value.open">
<div data-element="tour-{{tour.alias}}" class="umb-help-list-item" ng-repeat="tour in value">
<div ng-if="tourGroup.open">
<div data-element="tour-{{tour.alias}}" class="umb-help-list-item" ng-repeat="tour in tourGroup.tours">
<div class="umb-help-list-item__content justify-between">
<div class="flex items-center">
<div ng-if="!tour.completed" class="umb-number-badge umb-number-badge--xs umb-help-list-item__icon">{{ $index + 1 }}</div>
@@ -36,7 +36,7 @@
<span ng-class="{'strike': tour.completed}" class="umb-help-list-item__title">{{ tour.name }}</span>
</div>
<div>
<umb-button ng-if="!tour.completed && vm.showTourButton($index, value)" button-style="primary" size="xxs" type="button" label="Start" action="vm.startTour(tour)"></umb-button>
<umb-button ng-if="!tour.completed && vm.showTourButton($index, tourGroup)" button-style="primary" size="xxs" type="button" label="Start" action="vm.startTour(tour)"></umb-button>
<umb-button ng-if="tour.completed" size="xxs" type="button" label="Rerun" action="vm.startTour(tour)"></umb-button>
</div>
</div>
@@ -1,7 +1,7 @@
(function () {
"use strict";
function UserEditController($scope, $timeout, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) {
function UserEditController($scope, eventsService, $q, $timeout, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) {
var vm = this;
@@ -16,7 +16,7 @@
vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB";
vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes);
vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail;
//create the initial model for change password
vm.changePasswordModel = {
config: {},
@@ -33,6 +33,7 @@
vm.unlockUser = unlockUser;
vm.clearAvatar = clearAvatar;
vm.save = save;
vm.toggleChangePassword = toggleChangePassword;
function init() {
@@ -117,38 +118,83 @@
function save() {
vm.page.saveButtonState = "busy";
vm.user.resetPasswordValue = null;
if (formHelper.submitForm({ scope: $scope, statusMessage: vm.labels.saving })) {
//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;
//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;
}
vm.page.saveButtonState = "busy";
vm.user.resetPasswordValue = null;
//save current nav to be restored later so that the tabs dont change
var currentNav = vm.user.navigation;
usersResource.saveUser(vm.user)
.then(function (saved) {
//if the user saved, then try to execute all extended save options
extendedSave(saved).then(function(result) {
//if all is good, then reset the form
formHelper.resetForm({ scope: $scope, notifications: saved.notifications });
}, function(err) {
//otherwise show the notifications for the user being saved
formHelper.showNotifications(saved);
});
vm.user = _.omit(saved, "navigation");
//restore
vm.user.navigation = currentNav;
setUserDisplayState();
formatDatesToLocal(vm.user);
vm.changePasswordModel.isChanging = false;
//the user has a password if they are not states: Invited, NoCredentials
vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4;
vm.page.saveButtonState = "success";
}, function (err) {
contentEditingHelper.handleSaveError({
redirectOnFailure: false,
err: err
});
//show any notifications
if (err.data) {
formHelper.showNotifications(err.data);
}
vm.page.saveButtonState = "error";
});
}
}
contentEditingHelper.contentEditorPerformSave({
statusMessage: vm.labels.saving,
saveMethod: usersResource.saveUser,
scope: $scope,
content: vm.user,
// 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) {
/**
* Used to emit the save event and await any async operations being performed by editor extensions
* @param {any} savedUser
*/
function extendedSave(savedUser) {
vm.user = saved;
setUserDisplayState();
formatDatesToLocal(vm.user);
//used to track any promises added by the event handlers to be awaited
var promises = [];
var args = {
//getPromise: getPromise,
user: savedUser,
//a promise can be added by the event handler if the handler needs an async operation to be awaited
addPromise: function (p) {
promises.push(p);
}
};
vm.changePasswordModel.isChanging = false;
vm.page.saveButtonState = "success";
//the user has a password if they are not states: Invited, NoCredentials
vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4;
}, function (err) {
vm.page.saveButtonState = "error";
});
//emit the event
eventsService.emit("editors.user.editController.save", args);
//await all promises to complete
var resultPromise = $q.all(promises);
return resultPromise;
}
function goToPage(ancestor) {
@@ -1,406 +1,27 @@
<div ng-controller="Umbraco.Editors.Users.UserController as vm" class="clearfix">
<umb-load-indicator ng-if="vm.loading"></umb-load-indicator>
<form name="editUserForm" novalidate val-form-manager>
<umb-editor-view>
<umb-editor-header
name="vm.user.name"
hide-icon="true"
hide-description="true"
hide-alias="true">
<umb-editor-header name="vm.user.name"
hide-icon="true"
hide-description="true"
hide-alias="true"
navigation="vm.user.navigation">
</umb-editor-header>
<umb-editor-container>
<umb-load-indicator
ng-if="vm.loading">
</umb-load-indicator>
<div ng-if="!vm.loading" class="umb-packages-view-wrapper" style="padding: 0;">
<div class="umb-package-details">
<div ng-if="!vm.loading" class="umb-packages-view-wrapper" style="padding: 0;">
<div class="umb-package-details__main-content">
<umb-box>
<umb-box-header title-key="user_profile"></umb-box-header>
<umb-box-content class="block-form">
<umb-control-group label="@general_email" required="true">
<input
type="email"
localize="placeholder"
placeholder="@placeholders_enteremail"
class="input-block-level"
ng-model="vm.user.email"
umb-auto-focus name="email"
val-email
required
val-server-field="Email" />
<span class="help-inline" val-msg-for="email" val-toggle-msg="required"><localize key="general_required">Required</localize></span>
<span class="help-inline" val-msg-for="email" val-toggle-msg="valServerField"></span>
</umb-control-group>
<umb-control-group label="@general_username" ng-if="!vm.usernameIsEmail" required="true">
<input
type="text"
localize="placeholder"
placeholder="@placeholders_enterusername"
class="input-block-level"
ng-model="vm.user.username"
umb-auto-focus name="username"
required
val-server-field="Username" />
<span class="help-inline" val-msg-for="username" val-toggle-msg="required"><localize key="general_required">Required</localize></span>
<span class="help-inline" val-msg-for="username" val-toggle-msg="valServerField"></span>
</umb-control-group>
<umb-control-group label="@user_language" description="@user_languageHelp">
<select
class="input-block-level"
ng-model="vm.user.culture"
ng-options="key as value for (key, value) in vm.user.availableCultures"
name="culture"
required
val-server-field="Culture">
</select>
<span class="help-inline" val-msg-for="culture" val-toggle-msg="required"><localize key="general_required">Required</localize></span>
<span class="help-inline" val-msg-for="culture" val-toggle-msg="valServerField"></span>
</umb-control-group>
</umb-box-content>
</umb-box>
<umb-box>
<umb-box-header title-key="user_assignAccess"></umb-box-header>
<umb-box-content class="block-form">
<umb-control-group style="margin-bottom: 25px;" label="@general_groups" description="@user_groupsHelp" required="true">
<umb-user-group-preview
ng-repeat="userGroup in vm.user.userGroups"
icon="userGroup.icon"
name="userGroup.name"
sections="userGroup.sections"
content-start-node="userGroup.contentStartNode"
media-start-node="userGroup.mediaStartNode"
allow-remove="!vm.user.isCurrentUser"
on-remove="vm.removeSelectedItem($index, vm.user.userGroups)">
</umb-user-group-preview>
<a href=""
ng-if="!vm.user.isCurrentUser"
style="max-width: 100%;"
class="umb-node-preview-add"
ng-click="vm.openUserGroupPicker()"
prevent-default>
<localize key="general_add">Add</localize>
</a>
</umb-control-group>
<umb-control-group style="margin-bottom: 25px;" label="@user_startnodes" description="@user_startnodeshelp">
<umb-node-preview
style="max-width: 100%;"
ng-repeat="node in vm.user.startContentIds"
icon="node.icon"
name="node.name"
allow-remove="!vm.user.isCurrentUser"
on-remove="vm.removeSelectedItem($index, vm.user.startContentIds)">
</umb-node-preview>
<umb-node-preview
ng-if="vm.user.startContentIds.length === 0 && vm.user.isCurrentUser"
style="max-width: 100%;"
name="vm.labels.noStartNodes">
</umb-node-preview>
<a href=""
ng-if="!vm.user.isCurrentUser"
style="max-width: 100%;"
class="umb-node-preview-add"
ng-click="vm.openContentPicker()"
prevent-default>
<localize key="general_add">Add</localize>
</a>
</umb-control-group>
<umb-control-group label="@user_mediastartnodes" description="@user_mediastartnodeshelp">
<umb-node-preview
style="max-width: 100%;"
ng-repeat="node in vm.user.startMediaIds"
icon="node.icon"
name="node.name"
allow-remove="!vm.user.isCurrentUser"
on-remove="vm.removeSelectedItem($index, vm.user.startMediaIds)">
</umb-node-preview>
<umb-node-preview
ng-if="vm.user.startMediaIds.length === 0 && vm.user.isCurrentUser"
style="max-width: 100%;"
name="vm.labels.noStartNodes">
</umb-node-preview>
<a href=""
ng-if="!vm.user.isCurrentUser"
style="max-width: 100%;"
class="umb-node-preview-add"
ng-click="vm.openMediaPicker()"
prevent-default>
<localize key="general_add">Add</localize>
</a>
</umb-control-group>
</umb-box-content>
</umb-box>
<umb-box>
<umb-box-header title-key="user_access" description-key="user_accessHelp"></umb-box-header>
<umb-box-content class="block-form" style="padding-bottom: 0;">
<umb-control-group label="@sections_content">
<umb-node-preview
style="max-width: 100%;"
ng-repeat="node in vm.user.calculatedStartContentIds"
icon="node.icon"
name="node.name">
</umb-node-preview>
<umb-node-preview
ng-if="vm.user.calculatedStartContentIds.length === 0"
style="max-width: 100%;"
name="vm.labels.noStartNodes">
</umb-node-preview>
</umb-control-group>
<umb-control-group label="@sections_media">
<umb-node-preview
style="max-width: 100%;"
ng-repeat="node in vm.user.calculatedStartMediaIds"
icon="node.icon"
name="node.name">
</umb-node-preview>
<umb-node-preview
ng-if="vm.user.calculatedStartMediaIds.length === 0"
style="max-width: 100%;"
name="vm.labels.noStartNodes">
</umb-node-preview>
</umb-control-group>
</umb-box-content>
</umb-box>
</div>
<div class="umb-package-details__sidebar">
<div class="umb-package-details__section">
<!-- Avatar -->
<div style="margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #d8d7d9;">
<ng-form name="avatarForm" class="flex flex-column justify-center items-center">
<umb-avatar
style="margin-bottom: 15px;"
color="secondary"
size="xxl"
name="{{vm.user.name}}"
img-src="{{vm.user.avatars[3]}}"
img-srcset="{{vm.user.avatars[4]}} 2x, {{vm.user.avatars[4]}} 3x">
</umb-avatar>
<umb-progress-bar
style="max-width: 120px;"
ng-if="vm.avatarFile.uploadStatus === 'uploading'"
progress="{{ vm.avatarFile.uploadProgress }}"
size="s">
</umb-progress-bar>
<div class="flex items-center" ng-if="vm.avatarFile.uploadStatus !== 'uploading'">
<a href=""
class="umb-user-group-preview__action"
ngf-select ng-model="filesHolder"
ngf-change="changeAvatar($files, $event)"
ngf-multiple="false"
ngf-pattern="{{vm.acceptedFileTypes}}"
ngf-max-size="{{ vm.maxFileSize }}">
<localize key="user_changePhoto">Change photo</localize>
</a>
<a href=""
ng-if="vm.user.avatars"
class="umb-user-group-preview__action umb-user-group-preview__action--red"
ng-click="vm.clearAvatar()"
prevent-default>
<localize key="user_removePhoto">Remove photo</localize>
</a>
</div>
</ng-form>
</div>
<!-- Actions -->
<div style="margin-bottom: 20px;">
<div style="margin-bottom: 10px;">
<umb-button
ng-if="vm.user.userDisplayState.key === 'Disabled' && !vm.user.isCurrentUser"
type="button"
button-style="[success,block]"
state="vm.enableUserButtonState"
action="vm.enableUser()"
label="Enable"
label-key="actions_enable"
size="s">
</umb-button>
</div>
<div style="margin-bottom: 10px;">
<umb-button
ng-if="vm.user.userDisplayState.key === 'LockedOut' && !vm.user.isCurrentUser"
type="button"
button-style="[success,block]"
state="vm.unlockUserButtonState"
action="vm.unlockUser()"
label="Unlock"
label-key="actions_unlock"
size="s">
</umb-button>
</div>
<div style="margin-bottom: 10px;">
<umb-button
ng-if="vm.user.userDisplayState.key !== 'Disabled' && !vm.user.isCurrentUser"
type="button"
button-style="[info,block]"
action="vm.disableUser()"
state="vm.disableUserButtonState"
label="Disable"
label-key="actions_disable"
size="s">
</umb-button>
</div>
<umb-button
type="button"
button-style="[info,block]"
action="vm.toggleChangePassword()"
label="Change password"
label-key="general_changePassword"
state="changePasswordButtonState"
ng-if="vm.changePasswordModel.isChanging === false"
size="s">
</umb-button>
<ng-form ng-if="vm.changePasswordModel.isChanging" name="passwordForm" class="block-form" val-form-manager>
<change-password
password-values="vm.user.changePassword"
config="vm.changePasswordModel.config">
</change-password>
<umb-button
type="button"
action="vm.toggleChangePassword()"
label="Cancel"
label-key="general_cancel"
button-style="cancel">
</umb-button>
</ng-form>
<div ng-if="vm.user.resetPasswordValue">
<p><br />Password reset to value: <strong>{{vm.user.resetPasswordValue}}</strong></p>
</div>
</div>
<!-- User stats -->
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="general_status">Status</localize>:
</div>
<div class="umb-package-details__information-item-content">
<umb-badge style="margin-top: 4px;" size="s" color="{{vm.user.userDisplayState.color}}">
{{vm.user.userDisplayState.name}}
</umb-badge>
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_lastLogin">Last login</localize>:
</div>
<div class="umb-package-details__information-item-content">
<span ng-if="vm.user.lastLoginDate">{{ vm.user.formattedLastLogin }}</span>
<span ng-if="!vm.user.lastLoginDate">{{ vm.user.name | umbWordLimit:1 }} <localize key="user_noLogin">has not logged in yet</localize></span>
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_failedPasswordAttempts">Failed login attempts</localize>:
</div>
<div class="umb-package-details__information-item-content">
{{ vm.user.failedPasswordAttempts }}
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_lastLockoutDate">Last lockout date</localize>:
</div>
<div class="umb-package-details__information-item-content">
<span ng-if="vm.user.lastLockoutDate === '0001-01-01T00:00:00'">
{{ vm.user.name | umbWordLimit:1 }} <localize key="user_noLockouts">hasn't been locked out</localize>
</span>
<span ng-if="vm.user.lastLockoutDate !== '0001-01-01T00:00:00'">{{ vm.user.formattedLastLockoutDate }}</span>
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_lastPasswordChangeDate">Password is last changed</localize>:
</div>
<div class="umb-package-details__information-item-content">
<span ng-if="vm.user.lastPasswordChangeDate === '0001-01-01T00:00:00'">
<localize key="user_noPasswordChange">The password hasn't been changed</localize>
</span>
<span ng-if="vm.user.lastPasswordChangeDate !== '0001-01-01T00:00:00'">{{ vm.user.formattedLastPasswordChangeDate }}</span>
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_createDate">User is created</localize>:
</div>
<div class="umb-package-details__information-item-content">
{{ vm.user.formattedCreateDate }}
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_updateDate">User is last updated</localize>:
</div>
<div class="umb-package-details__information-item-content">
{{ vm.user.formattedUpdateDate }}
</div>
</div>
</div>
</div>
</div>
<umb-editor-sub-views
ng-if="!vm.loading"
sub-views="vm.user.navigation"
model="vm">
</umb-editor-sub-views>
</div>
@@ -410,33 +31,30 @@
<umb-editor-footer-content-left>
<umb-breadcrumbs
ancestors="vm.breadcrumbs"
allow-on-open="true"
on-open="vm.goToPage(ancestor)">
<umb-breadcrumbs ancestors="vm.breadcrumbs"
allow-on-open="true"
on-open="vm.goToPage(ancestor)">
</umb-breadcrumbs>
</umb-editor-footer-content-left>
<umb-editor-footer-content-right>
<umb-button
type="button"
action="vm.goToPage(vm.breadcrumbs[0])"
label="Return to list"
label-key="buttons_returnToList"
disabled="vm.loading">
<umb-button type="button"
action="vm.goToPage(vm.breadcrumbs[0])"
label="Return to list"
label-key="buttons_returnToList"
disabled="vm.loading">
</umb-button>
<umb-button
type="button"
action="vm.save()"
state="vm.page.saveButtonState"
button-style="success"
shortcut="ctrl+s"
label="Save"
label-key="buttons_save"
disabled="vm.loading">
<umb-button type="button"
action="vm.save()"
state="vm.page.saveButtonState"
button-style="success"
shortcut="ctrl+s"
label="Save"
label-key="buttons_save"
disabled="vm.loading">
</umb-button>
</umb-editor-footer-content-right>
@@ -447,25 +65,22 @@
</form>
<umb-overlay
ng-if="vm.userGroupPicker.show"
model="vm.userGroupPicker"
view="vm.userGroupPicker.view"
position="right">
<umb-overlay ng-if="vm.userGroupPicker.show"
model="vm.userGroupPicker"
view="vm.userGroupPicker.view"
position="right">
</umb-overlay>
<umb-overlay
ng-if="vm.contentPicker.show"
model="vm.contentPicker"
view="vm.contentPicker.view"
position="right">
<umb-overlay ng-if="vm.contentPicker.show"
model="vm.contentPicker"
view="vm.contentPicker.view"
position="right">
</umb-overlay>
<umb-overlay
ng-if="vm.mediaPicker.show"
model="vm.mediaPicker"
view="vm.mediaPicker.view"
position="right">
<umb-overlay ng-if="vm.mediaPicker.show"
model="vm.mediaPicker"
view="vm.mediaPicker.view"
position="right">
</umb-overlay>
</div>
@@ -0,0 +1,361 @@
<div class="umb-package-details">
<div class="umb-package-details__main-content">
<umb-box>
<umb-box-header title-key="user_profile"></umb-box-header>
<umb-box-content class="block-form">
<umb-control-group label="@general_email" required="true">
<input type="email"
localize="placeholder"
placeholder="@placeholders_enteremail"
class="input-block-level"
ng-model="model.user.email"
umb-auto-focus name="email"
val-email
required
val-server-field="Email" />
<span class="help-inline" val-msg-for="email" val-toggle-msg="required"><localize key="general_required">Required</localize></span>
<span class="help-inline" val-msg-for="email" val-toggle-msg="valServerField"></span>
</umb-control-group>
<umb-control-group label="@general_username" ng-if="!model.usernameIsEmail" required="true">
<input type="text"
localize="placeholder"
placeholder="@placeholders_enterusername"
class="input-block-level"
ng-model="model.user.username"
umb-auto-focus name="username"
required
val-server-field="Username" />
<span class="help-inline" val-msg-for="username" val-toggle-msg="required"><localize key="general_required">Required</localize></span>
<span class="help-inline" val-msg-for="username" val-toggle-msg="valServerField"></span>
</umb-control-group>
<umb-control-group label="@user_language" description="@user_languageHelp">
<select class="input-block-level"
ng-model="model.user.culture"
ng-options="key as value for (key, value) in model.user.availableCultures"
name="culture"
required
val-server-field="Culture"></select>
<span class="help-inline" val-msg-for="culture" val-toggle-msg="required"><localize key="general_required">Required</localize></span>
<span class="help-inline" val-msg-for="culture" val-toggle-msg="valServerField"></span>
</umb-control-group>
</umb-box-content>
</umb-box>
<umb-box>
<umb-box-header title-key="user_assignAccess"></umb-box-header>
<umb-box-content class="block-form">
<umb-control-group style="margin-bottom: 25px;" label="@general_groups" description="@user_groupsHelp" required="true">
<umb-user-group-preview ng-repeat="userGroup in model.user.userGroups"
icon="userGroup.icon"
name="userGroup.name"
sections="userGroup.sections"
content-start-node="userGroup.contentStartNode"
media-start-node="userGroup.mediaStartNode"
allow-remove="!model.user.isCurrentUser"
on-remove="model.removeSelectedItem($index, model.user.userGroups)">
</umb-user-group-preview>
<a href=""
ng-if="!model.user.isCurrentUser"
style="max-width: 100%;"
class="umb-node-preview-add"
ng-click="model.openUserGroupPicker()"
prevent-default>
<localize key="general_add">Add</localize>
</a>
</umb-control-group>
<umb-control-group style="margin-bottom: 25px;" label="@user_startnodes" description="@user_startnodeshelp">
<umb-node-preview style="max-width: 100%;"
ng-repeat="node in model.user.startContentIds"
icon="node.icon"
name="node.name"
allow-remove="!model.user.isCurrentUser"
on-remove="model.removeSelectedItem($index, model.user.startContentIds)">
</umb-node-preview>
<umb-node-preview ng-if="model.user.startContentIds.length === 0 && model.user.isCurrentUser"
style="max-width: 100%;"
name="model.labels.noStartNodes">
</umb-node-preview>
<a href=""
ng-if="!model.user.isCurrentUser"
style="max-width: 100%;"
class="umb-node-preview-add"
ng-click="model.openContentPicker()"
prevent-default>
<localize key="general_add">Add</localize>
</a>
</umb-control-group>
<umb-control-group label="@user_mediastartnodes" description="@user_mediastartnodeshelp">
<umb-node-preview style="max-width: 100%;"
ng-repeat="node in model.user.startMediaIds"
icon="node.icon"
name="node.name"
allow-remove="!model.user.isCurrentUser"
on-remove="model.removeSelectedItem($index, model.user.startMediaIds)">
</umb-node-preview>
<umb-node-preview ng-if="model.user.startMediaIds.length === 0 && model.user.isCurrentUser"
style="max-width: 100%;"
name="model.labels.noStartNodes">
</umb-node-preview>
<a href=""
ng-if="!model.user.isCurrentUser"
style="max-width: 100%;"
class="umb-node-preview-add"
ng-click="model.openMediaPicker()"
prevent-default>
<localize key="general_add">Add</localize>
</a>
</umb-control-group>
</umb-box-content>
</umb-box>
<umb-box>
<umb-box-header title-key="user_access" description-key="user_accessHelp"></umb-box-header>
<umb-box-content class="block-form" style="padding-bottom: 0;">
<umb-control-group label="@sections_content">
<umb-node-preview style="max-width: 100%;"
ng-repeat="node in model.user.calculatedStartContentIds"
icon="node.icon"
name="node.name">
</umb-node-preview>
<umb-node-preview ng-if="model.user.calculatedStartContentIds.length === 0"
style="max-width: 100%;"
name="model.labels.noStartNodes">
</umb-node-preview>
</umb-control-group>
<umb-control-group label="@sections_media">
<umb-node-preview style="max-width: 100%;"
ng-repeat="node in model.user.calculatedStartMediaIds"
icon="node.icon"
name="node.name">
</umb-node-preview>
<umb-node-preview ng-if="model.user.calculatedStartMediaIds.length === 0"
style="max-width: 100%;"
name="model.labels.noStartNodes">
</umb-node-preview>
</umb-control-group>
</umb-box-content>
</umb-box>
</div>
<div class="umb-package-details__sidebar">
<div class="umb-package-details__section">
<!-- Avatar -->
<div style="margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #d8d7d9;">
<ng-form name="avatarForm" class="flex flex-column justify-center items-center">
<umb-avatar style="margin-bottom: 15px;"
color="secondary"
size="xxl"
name="{{model.user.name}}"
img-src="{{model.user.avatars[3]}}"
img-srcset="{{model.user.avatars[4]}} 2x, {{model.user.avatars[4]}} 3x">
</umb-avatar>
<umb-progress-bar style="max-width: 120px;"
ng-if="model.avatarFile.uploadStatus === 'uploading'"
progress="{{ model.avatarFile.uploadProgress }}"
size="s">
</umb-progress-bar>
<div class="flex items-center" ng-if="model.avatarFile.uploadStatus !== 'uploading'">
<a href=""
class="umb-user-group-preview__action"
ngf-select ng-model="filesHolder"
ngf-change="changeAvatar($files, $event)"
ngf-multiple="false"
ngf-pattern="{{model.acceptedFileTypes}}"
ngf-max-size="{{ model.maxFileSize }}">
<localize key="user_changePhoto">Change photo</localize>
</a>
<a href=""
ng-if="model.user.avatars"
class="umb-user-group-preview__action umb-user-group-preview__action--red"
ng-click="model.clearAvatar()"
prevent-default>
<localize key="user_removePhoto">Remove photo</localize>
</a>
</div>
</ng-form>
</div>
<!-- Actions -->
<div style="margin-bottom: 20px;">
<div style="margin-bottom: 10px;">
<umb-button ng-if="model.user.userDisplayState.key === 'Disabled' && !model.user.isCurrentUser"
type="button"
button-style="[success,block]"
state="model.enableUserButtonState"
action="model.enableUser()"
label="Enable"
label-key="actions_enable"
size="s">
</umb-button>
</div>
<div style="margin-bottom: 10px;">
<umb-button ng-if="model.user.userDisplayState.key === 'LockedOut' && !model.user.isCurrentUser"
type="button"
button-style="[success,block]"
state="model.unlockUserButtonState"
action="model.unlockUser()"
label="Unlock"
label-key="actions_unlock"
size="s">
</umb-button>
</div>
<div style="margin-bottom: 10px;">
<umb-button ng-if="model.user.userDisplayState.key !== 'Disabled' && !model.user.isCurrentUser"
type="button"
button-style="[info,block]"
action="model.disableUser()"
state="model.disableUserButtonState"
label="Disable"
label-key="actions_disable"
size="s">
</umb-button>
</div>
<umb-button type="button"
button-style="[info,block]"
action="model.toggleChangePassword()"
label="Change password"
label-key="general_changePassword"
state="changePasswordButtonState"
ng-if="model.changePasswordModel.isChanging === false"
size="s">
</umb-button>
<ng-form ng-if="model.changePasswordModel.isChanging" name="passwordForm" class="block-form" val-form-manager>
<change-password password-values="model.user.changePassword"
config="model.changePasswordModel.config">
</change-password>
<umb-button type="button"
action="model.toggleChangePassword()"
label="Cancel"
label-key="general_cancel"
button-style="cancel">
</umb-button>
</ng-form>
<div ng-if="model.user.resetPasswordValue">
<p><br />Password reset to value: <strong>{{model.user.resetPasswordValue}}</strong></p>
</div>
</div>
<!-- User stats -->
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="general_status">Status</localize>:
</div>
<div class="umb-package-details__information-item-content">
<umb-badge style="margin-top: 4px;" size="s" color="{{model.user.userDisplayState.color}}">
{{model.user.userDisplayState.name}}
</umb-badge>
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_lastLogin">Last login</localize>:
</div>
<div class="umb-package-details__information-item-content">
<span ng-if="model.user.lastLoginDate">{{ model.user.formattedLastLogin }}</span>
<span ng-if="!model.user.lastLoginDate">{{ model.user.name | umbWordLimit:1 }} <localize key="user_noLogin">has not logged in yet</localize></span>
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_failedPasswordAttempts">Failed login attempts</localize>:
</div>
<div class="umb-package-details__information-item-content">
{{ model.user.failedPasswordAttempts }}
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_lastLockoutDate">Last lockout date</localize>:
</div>
<div class="umb-package-details__information-item-content">
<span ng-if="model.user.lastLockoutDate === '0001-01-01T00:00:00'">
{{ model.user.name | umbWordLimit:1 }} <localize key="user_noLockouts">hasn't been locked out</localize>
</span>
<span ng-if="model.user.lastLockoutDate !== '0001-01-01T00:00:00'">{{ model.user.formattedLastLockoutDate }}</span>
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_lastPasswordChangeDate">Password is last changed</localize>:
</div>
<div class="umb-package-details__information-item-content">
<span ng-if="model.user.lastPasswordChangeDate === '0001-01-01T00:00:00'">
<localize key="user_noPasswordChange">The password hasn't been changed</localize>
</span>
<span ng-if="model.user.lastPasswordChangeDate !== '0001-01-01T00:00:00'">{{ model.user.formattedLastPasswordChangeDate }}</span>
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_createDate">User is created</localize>:
</div>
<div class="umb-package-details__information-item-content">
{{ model.user.formattedCreateDate }}
</div>
</div>
<div class="umb-package-details__information-item">
<div class="umb-package-details__information-item-label">
<localize key="user_updateDate">User is last updated</localize>:
</div>
<div class="umb-package-details__information-item-content">
{{ model.user.formattedUpdateDate }}
</div>
</div>
</div>
</div>
</div>
@@ -3,6 +3,7 @@
"name": "Introduction",
"alias": "umbIntroIntroduction",
"group": "Getting Started",
"groupOrder": 100,
"allowDisable": true,
"steps": [
{
@@ -86,113 +87,114 @@
"name": "Create document type",
"alias": "umbIntroCreateDocType",
"group": "Getting Started",
"groupOrder": 100,
"steps": [
{
title: "Create your first Document Type",
content: "<p>Step 1 of any site is to create a <strong>Document Type</strong>.<br> A Document Type is a template for content. For each <em>type</em> of content you want to create you'll create a Document Type. This will define were content based on this Document Type can be created, how many properties it holds and what the input method should be for these properties.</p><p>When you have at least one Document type in place you can start creating content and this content can the be used in a template.</p><p>In this tour you will learn how to set up a basic Document Type with a property to enter a short text.</p>",
type: "intro"
"title": "Create your first Document Type",
"content": "<p>Step 1 of any site is to create a <strong>Document Type</strong>.<br> A Document Type is a template for content. For each <em>type</em> of content you want to create you'll create a Document Type. This will define were content based on this Document Type can be created, how many properties it holds and what the input method should be for these properties.</p><p>When you have at least one Document type in place you can start creating content and this content can the be used in a template.</p><p>In this tour you will learn how to set up a basic Document Type with a property to enter a short text.</p>",
"type": "intro"
},
{
element: "#applications [data-element='section-settings']",
title: "Navigate to the Settings sections",
content: "In the <b>Settings section</b> you can create and manage Document types.",
event: "click",
backdropOpacity: 0.6
"element": "#applications [data-element='section-settings']",
"title": "Navigate to the Settings sections",
"content": "In the <b>Settings section</b> you can create and manage Document types.",
"event": "click",
"backdropOpacity": 0.6
},
{
element: "#tree [data-element='tree-item-documentTypes']",
title: "Create Document Type",
content: "<p>Hover the Document Type tree and click the <b>three small dots</b> to open the <b>context menu</b>.</p>",
event: "click",
eventElement: "#tree [data-element='tree-item-documentTypes'] [data-element='tree-item-options']"
"element": "#tree [data-element='tree-item-documentTypes']",
"title": "Create Document Type",
"content": "<p>Hover the Document Type tree and click the <b>three small dots</b> to open the <b>context menu</b>.</p>",
"event": "click",
"eventElement": "#tree [data-element='tree-item-documentTypes'] [data-element='tree-item-options']"
},
{
element: "#dialog [data-element='action-documentType']",
title: "Create Document Type",
content: "<p>Click <b>Document Type</b> to create a new document type with a template. The template will be automatically created and set as the default template for this Document Type</p><p>You will use the template in a later tour render content.</p>",
event: "click"
"element": "#dialog [data-element='action-documentType']",
"title": "Create Document Type",
"content": "<p>Click <b>Document Type</b> to create a new document type with a template. The template will be automatically created and set as the default template for this Document Type</p><p>You will use the template in a later tour render content.</p>",
"event": "click"
},
{
element: "[data-element='editor-name-field']",
title: "Enter a name",
content: "<p>Your Document Type needs a name. Enter <code>My Home Page</code> in the field and click <b>Next</b>.",
view: "doctypename"
"element": "[data-element='editor-name-field']",
"title": "Enter a name",
"content": "<p>Your Document Type needs a name. Enter <code>My Home Page</code> in the field and click <b>Next</b>.",
"view": "doctypename"
},
{
element: "[data-element='editor-description']",
title: "Enter a description",
content: "<p>A description helps to pick the right document type when creating content.</p><p>Write a description to our Home page. It could be: <br/><pre>The home page of the website</pre></p>"
"element": "[data-element='editor-description']",
"title": "Enter a description",
"content": "<p>A description helps to pick the right document type when creating content.</p><p>Write a description to our Home page. It could be: <br/><pre>The home page of the website</pre></p>"
},
{
element: "[data-element='group-add']",
title: "Add tab",
content: "Tabs are used to organize properties on content in the Content section. Click <b>Add new tab</b> to add a tab.",
event: "click"
"element": "[data-element='group-add']",
"title": "Add tab",
"content": "Tabs are used to organize properties on content in the Content section. Click <b>Add new tab</b> to add a tab.",
"event": "click"
},
{
element: "[data-element='group-name-field']",
title: "Name the tab",
content: "<p>Enter <code>Home</code> in the tab name.</p><p><em>You can name a tab anything you want and if you have a lot of properties it can be useful to add multiple tabs.</em></p>",
view: "tabName"
"element": "[data-element='group-name-field']",
"title": "Name the tab",
"content": "<p>Enter <code>Home</code> in the tab name.</p><p><em>You can name a tab anything you want and if you have a lot of properties it can be useful to add multiple tabs.</em></p>",
"view": "tabName"
},
{
element: "[data-element='property-add']",
title: "Add a property",
content: "<p>Properties are the different input fields on a content page.</p><p>On our Home Page we wan't to add a welcome text.</p><p>Click <b>Add property</b> to open the property dialog.</p>",
event: "click"
"element": "[data-element='property-add']",
"title": "Add a property",
"content": "<p>Properties are the different input fields on a content page.</p><p>On our Home Page we wan't to add a welcome text.</p><p>Click <b>Add property</b> to open the property dialog.</p>",
"event": "click"
},
{
element: "[data-element~='overlay-property-settings'] [data-element='property-name']",
title: "Name the property",
content: "Enter <code>Welcome Text</code> as the name for the property.",
view: "propertyname"
"element": "[data-element~='overlay-property-settings'] [data-element='property-name']",
"title": "Name the property",
"content": "Enter <code>Welcome Text</code> as the name for the property.",
"view": "propertyname"
},
{
element: "[data-element~='overlay-property-settings'] [data-element='property-description']",
title: "Enter a description",
content: "<p>A description will help to fill in the right content.</p><p>Enter a description for the property editor. It could be:<br/> <pre>Write a nice introduction text so the visitors feel welcome</pre></p>"
"element": "[data-element~='overlay-property-settings'] [data-element='property-description']",
"title": "Enter a description",
"content": "<p>A description will help to fill in the right content.</p><p>Enter a description for the property editor. It could be:<br/> <pre>Write a nice introduction text so the visitors feel welcome</pre></p>"
},
{
element: "[data-element~='overlay-property-settings'] [data-element='editor-add']",
title: "Add editor",
content: "When you add an editor you choose what the input method for this property will be. Click <b>Add editor</b> to open the editor picker dialog.",
event: "click"
"element": "[data-element~='overlay-property-settings'] [data-element='editor-add']",
"title": "Add editor",
"content": "When you add an editor you choose what the input method for this property will be. Click <b>Add editor</b> to open the editor picker dialog.",
"event": "click"
},
{
element: "[data-element~='overlay-editor-picker']",
elementPreventClick: true,
title: "Editor picker",
content: "<p>In the editor picker dialog we can pick one of the many build in editor.</p><p><em>You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors)</em></p>"
"element": "[data-element~='overlay-editor-picker']",
"elementPreventClick": true,
"title": "Editor picker",
"content": "<p>In the editor picker dialog we can pick one of the many build in editor.</p><p><em>You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors)</em></p>"
},
{
element: "[data-element~='overlay-editor-picker'] [data-element='editor-Textarea']",
title: "Select editor",
content: "Select the <b>Textarea</b> editor. This will add a textarea to the Welcome Text property.",
event: "click"
"element": "[data-element~='overlay-editor-picker'] [data-element='editor-Textarea']",
"title": "Select editor",
"content": "Select the <b>Textarea</b> editor. This will add a textarea to the Welcome Text property.",
"event": "click"
},
{
element: "[data-element~='overlay-editor-settings']",
elementPreventClick: true,
title: "Editor settings",
content: "Each property editor can have individual settings. For the textarea editor you can set a charachter limit but in this case it is not needed"
"element": "[data-element~='overlay-editor-settings']",
"elementPreventClick": true,
"title": "Editor settings",
"content": "Each property editor can have individual settings. For the textarea editor you can set a charachter limit but in this case it is not needed"
},
{
element: "[data-element~='overlay-editor-settings'] [data-element='button-overlaySubmit']",
title: "Save editor",
content: "Click <b>Submit</b> to save the editor.",
event: "click"
"element": "[data-element~='overlay-editor-settings'] [data-element='button-overlaySubmit']",
"title": "Save editor",
"content": "Click <b>Submit</b> to save the editor.",
"event": "click"
},
{
element: "[data-element~='overlay-property-settings'] [data-element='button-overlaySubmit']",
title: "Add property to document type",
content: "Click <b>Submit</b> to add the property to the document type.",
event: "click"
"element": "[data-element~='overlay-property-settings'] [data-element='button-overlaySubmit']",
"title": "Add property to document type",
"content": "Click <b>Submit</b> to add the property to the document type.",
"event": "click"
},
{
element: "[data-element='button-save']",
title: "Save the document type",
content: "All we need now is to save the document type. Click <b>Save</b> to create and save your new document type.",
event: "click"
"element": "[data-element='button-save']",
"title": "Save the document type",
"content": "All we need now is to save the document type. Click <b>Save</b> to create and save your new document type.",
"event": "click"
}
]
},
@@ -200,48 +202,49 @@
"name": "Create Content",
"alias": "umbIntroCreateContent",
"group": "Getting Started",
"groupOrder": 100,
"steps": [
{
title: "Creating your first content node",
content: "<p>In this tour you will learn how to create the home page for your website. It will use the <b>Home Page</b> Document type you created in the previous tour.</p>",
type: "intro"
"title": "Creating your first content node",
"content": "<p>In this tour you will learn how to create the home page for your website. It will use the <b>Home Page</b> Document type you created in the previous tour.</p>",
"type": "intro"
},
{
element: "#applications [data-element='section-content']",
title: "Navigate to the Content section",
content: "<p>In the <b>Content section</b> you can create and manage the content of the website.</p><p>The <b>Content section</b> contains the content of your website. Content is displayed as <b>nodes</b> in the content tree.</p>",
event: "click",
backdropOpacity: 0.6
"element": "#applications [data-element='section-content']",
"title": "Navigate to the Content section",
"content": "<p>In the <b>Content section</b> you can create and manage the content of the website.</p><p>The <b>Content section</b> contains the content of your website. Content is displayed as <b>nodes</b> in the content tree.</p>",
"event": "click",
"backdropOpacity": 0.6
},
{
element: "[data-element='tree-root']",
title: "Open context menu",
content: "<p>Open the context menu by hovering the root of the content section.</p><p>Now click the <b>three small dots</b> to the right.</p>",
event: "click",
eventElement: "[data-element='tree-root'] [data-element='tree-item-options']"
"element": "[data-element='tree-root']",
"title": "Open context menu",
"content": "<p>Open the context menu by hovering the root of the content section.</p><p>Now click the <b>three small dots</b> to the right.</p>",
"event": "click",
"eventElement": "[data-element='tree-root'] [data-element='tree-item-options']"
},
{
element: "[data-element='action-create-homePage']",
title: "Create Home page",
content: "<p>The context menu shows you all the actions that are available on a node</p><p>Click on <b>Home Page</b> to create a new page of type <b>Home Page</b>.</p>",
event: "click"
"element": "[data-element='action-create-homePage']",
"title": "Create Home page",
"content": "<p>The context menu shows you all the actions that are available on a node</p><p>Click on <b>Home Page</b> to create a new page of type <b>Home Page</b>.</p>",
"event": "click"
},
{
element: "[data-element='editor-content'] [data-element='editor-name-field']",
title: "Give your new page a name",
content: "<p>Our new page needs a name. Enter <code>Home</code> in the field and click <b>Next</b>.</p>",
view: "nodename"
"element": "[data-element='editor-content'] [data-element='editor-name-field']",
"title": "Give your new page a name",
"content": "<p>Our new page needs a name. Enter <code>Home</code> in the field and click <b>Next</b>.</p>",
"view": "nodename"
},
{
element: "[data-element='editor-content'] [data-element='property-welcomeText']",
title: "Add a welcome text",
content: "<p>Add content to the <b>Welcome Text</b> field</p><p>If you don't have any ideas here is a start:<br/> <pre>I am learning Umbraco. High Five I Rock #H5IR</pre>.</p>"
"element": "[data-element='editor-content'] [data-element='property-welcomeText']",
"title": "Add a welcome text",
"content": "<p>Add content to the <b>Welcome Text</b> field</p><p>If you don't have any ideas here is a start:<br/> <pre>I am learning Umbraco. High Five I Rock #H5IR</pre>.</p>"
},
{
element: "[data-element='editor-content'] [data-element='button-saveAndPublish']",
title: "Save and Publish",
content: "<p>Now click the <b>Save and publish</b> button to save and publish your changes.</p>",
event: "click"
"element": "[data-element='editor-content'] [data-element='button-saveAndPublish']",
"title": "Save and Publish",
"content": "<p>Now click the <b>Save and publish</b> button to save and publish your changes.</p>",
"event": "click"
}
]
},
@@ -249,44 +252,45 @@
"name": "Render in template",
"alias": "umbIntroRenderInTemplate",
"group": "Getting Started",
"groupOrder": 100,
"steps": [
{
title: "Render your content in a template",
content: "<p>Templating in Umbraco builds on the concept of <b>Razor Views</b> from asp.net MVC. - This tour is a sneak peak on how to write templates in Umbraco.</p><p>In this tour you will learn how to render content from the <b>Home Page</b> document type so you can see the content added to our <b>Home</b> content page.</p>",
type: "intro"
"title": "Render your content in a template",
"content": "<p>Templating in Umbraco builds on the concept of <b>Razor Views</b> from asp.net MVC. - This tour is a sneak peak on how to write templates in Umbraco.</p><p>In this tour you will learn how to render content from the <b>Home Page</b> document type so you can see the content added to our <b>Home</b> content page.</p>",
"type": "intro"
},
{
element: "#applications [data-element='section-settings']",
title: "Navigate to the Settings section",
content: "<p>In the <b>Settings</b> section you will find all the templates</p><p>It is of course also possible to edit all your code files in your favorite code editor.</p>",
event: "click",
backdropOpacity: 0.6
"element": "#applications [data-element='section-settings']",
"title": "Navigate to the Settings section",
"content": "<p>In the <b>Settings</b> section you will find all the templates</p><p>It is of course also possible to edit all your code files in your favorite code editor.</p>",
"event": "click",
"backdropOpacity": 0.6
},
{
element: "#tree [data-element='tree-item-templates']",
title: "Expand the Templates node",
content: "<p>To see all our templates click the <b>small triangle</b> to the left of the templates node.</p>",
event: "click",
eventElement: "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']",
view: "templatetree"
"element": "#tree [data-element='tree-item-templates']",
"title": "Expand the Templates node",
"content": "<p>To see all our templates click the <b>small triangle</b> to the left of the templates node.</p>",
"event": "click",
"eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']",
"view": "templatetree"
},
{
element: "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']",
title: "Open Home template",
content: "<p>Click the <b>Home Page</b> template to open and edit it.</p>",
eventElement: "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page'] a.umb-tree-item__label",
event: "click"
"element": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']",
"title": "Open Home template",
"content": "<p>Click the <b>Home Page</b> template to open and edit it.</p>",
"eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page'] a.umb-tree-item__label",
"event": "click"
},
{
element: "[data-element='editor-templates'] [data-element='code-editor']",
title: "Edit template",
content: '<p>The template can be edited here or in your favorite code editor.</p><p>To render the field from the document type add the following to the template:<br/> <pre>&#60;h1&#62;@Model.Content.Name&#60;/h1&#62;<br>&#60;p&#62;@Model.Content.WelcomeText&#60;/p&#62;</pre></p>'
"element": "[data-element='editor-templates'] [data-element='code-editor']",
"title": "Edit template",
"content": "<p>The template can be edited here or in your favorite code editor.</p><p>To render the field from the document type add the following to the template:<br/> <pre>&#60;h1&#62;@Model.Content.Name&#60;/h1&#62;<br>&#60;p&#62;@Model.Content.WelcomeText&#60;/p&#62;</pre></p>"
},
{
element: "[data-element='editor-templates'] [data-element='button-save']",
title: "Save the template",
content: "Click the <b>Save button</b> and your template will be saved.",
event: "click"
"element": "[data-element='editor-templates'] [data-element='button-save']",
"title": "Save the template",
"content": "Click the <b>Save button</b> and your template will be saved.",
"event": "click"
}
]
},
@@ -294,38 +298,39 @@
"name": "View Home page",
"alias": "umbIntroViewHomePage",
"group": "Getting Started",
"groupOrder": 100,
"steps": [
{
title: "View your Umbraco site",
content: "<p>Our three main components to a page is done: <b>Document type, Template, and Content</b> - it is now time to see the result.</p><p>In this tour you will learn how to see your published website.</p>",
type: "intro"
"title": "View your Umbraco site",
"content": "<p>Our three main components to a page is done: <b>Document type, Template, and Content</b> - it is now time to see the result.</p><p>In this tour you will learn how to see your published website.</p>",
"type": "intro"
},
{
element: "#applications [data-element='section-content']",
title: "Navigate to the content sections",
content: "In the <b>Content section</b> you will find the content of our website.",
event: "click",
backdropOpacity: 0.6
"element": "#applications [data-element='section-content']",
"title": "Navigate to the content sections",
"content": "In the <b>Content section</b> you will find the content of our website.",
"event": "click",
"backdropOpacity": 0.6
},
{
element: "#tree [data-element='tree-item-Home']",
title: "Open the Home page",
content: "<p>Click the <b>Home</b> page to open it</p>",
event: "click",
eventElement: "#tree [data-element='tree-item-Home'] a.umb-tree-item__label"
"element": "#tree [data-element='tree-item-Home']",
"title": "Open the Home page",
"content": "<p>Click the <b>Home</b> page to open it</p>",
"event": "click",
"eventElement": "#tree [data-element='tree-item-Home'] a.umb-tree-item__label"
},
{
element: "[data-element='editor-content'] [data-element='tab-_umb_infoTab']",
title: "Info",
content: "<p>Under the info tab you will find the default information about a content item.</p>",
event: "click"
"element": "[data-element='editor-content'] [data-element='tab-_umb_infoTab']",
"title": "Info",
"content": "<p>Under the info tab you will find the default information about a content item.</p>",
"event": "click"
},
{
element: "[data-element='editor-content'] [data-element='node-info-urls']",
title: "Open page",
content: "<p>Click the <b>Link to document</b> <i class='icon-out'></i> to view your page.</p><p>Tip: Click the preview button in the bottom right corner to preview changes without publishing them.</p>",
event: "click",
eventElement: "[data-element='editor-content'] [data-element='node-info-urls'] a[target='_blank']"
"element": "[data-element='editor-content'] [data-element='node-info-urls']",
"title": "Open page",
"content": "<p>Click the <b>Link to document</b> <i class='icon-out'></i> to view your page.</p><p>Tip: Click the preview button in the bottom right corner to preview changes without publishing them.</p>",
"event": "click",
"eventElement": "[data-element='editor-content'] [data-element='node-info-urls'] a[target='_blank']"
}
]
},
@@ -333,86 +338,87 @@
"name": "The Media library",
"alias": "umbIntroMediaSection",
"group": "Getting Started",
"groupOrder": 100,
"steps": [
{
title: "How to use the media library",
content: "<p>A website would be boring without media content. In Umbraco you can manage all your images, documents, videos etc. in the <b>Media section</b>. Here you can upload and organise your media items and see details about each item.</p><p>In this tour you will learn how to upload and organise your Media library in Umbraco. It will also show you how to view details about a specific media item.</p>",
type: "intro"
"title": "How to use the media library",
"content": "<p>A website would be boring without media content. In Umbraco you can manage all your images, documents, videos etc. in the <b>Media section</b>. Here you can upload and organise your media items and see details about each item.</p><p>In this tour you will learn how to upload and organise your Media library in Umbraco. It will also show you how to view details about a specific media item.</p>",
"type": "intro"
},
{
element: "#applications [data-element='section-media']",
title: "Navigate to the Media section",
content: "The <b>media</b> section is where you manage all your media items.",
event: "click",
backdropOpacity: 0.6
"element": "#applications [data-element='section-media']",
"title": "Navigate to the Media section",
"content": "The <b>media</b> section is where you manage all your media items.",
"event": "click",
"backdropOpacity": 0.6
},
{
element: "#tree [data-element='tree-root']",
title: "Create a new folder",
content: "<p>First create a folder for your images. Hover the media root node and click the <b>three small dots</b> on the right side of the item.</p>",
event: "click",
eventElement: "#tree [data-element='tree-root'] [data-element='tree-item-options']"
"element": "#tree [data-element='tree-root']",
"title": "Create a new folder",
"content": "<p>First create a folder for your images. Hover the media root node and click the <b>three small dots</b> on the right side of the item.</p>",
"event": "click",
"eventElement": "#tree [data-element='tree-root'] [data-element='tree-item-options']"
},
{
element: "#dialog [data-element='action-Folder']",
title: "Create a new folder",
content: "<p>Select the <b>Folder</b> option to select the type folder.</p>",
event: "click"
"element": "#dialog [data-element='action-Folder']",
"title": "Create a new folder",
"content": "<p>Select the <b>Folder</b> option to select the type folder.</p>",
"event": "click"
},
{
element: "[data-element='editor-media'] [data-element='editor-name-field']",
title: "Enter a name",
content: "<p>Enter <code>My Images</code> in the field.</p>"
"element": "[data-element='editor-media'] [data-element='editor-name-field']",
"title": "Enter a name",
"content": "<p>Enter <code>My Images</code> in the field.</p>"
},
{
element: "[data-element='editor-media'] [data-element='button-save']",
title: "Save the folder",
content: "<p>Click the <b>Save</b> button to create the new folder</p>",
event: "click"
"element": "[data-element='editor-media'] [data-element='button-save']",
"title": "Save the folder",
"content": "<p>Click the <b>Save</b> button to create the new folder</p>",
"event": "click"
},
{
element: "[data-element='editor-media'] [data-element='dropzone']",
title: "Upload images",
content: "<p>In the upload area you can upload your media items.</p><p>Click the <b>Click here to choose files</b>-button and select a couple of images on your computer and upload them.</p>",
view: "uploadimages"
"element": "[data-element='editor-media'] [data-element='dropzone']",
"title": "Upload images",
"content": "<p>In the upload area you can upload your media items.</p><p>Click the <b>Click here to choose files</b>-button and select a couple of images on your computer and upload them.</p>",
"view": "uploadimages"
},
{
element: "[data-element='editor-media'] [data-element='media-grid-item-0']",
title: "View media item details",
content: "Hover the media item and <b>Click the purple bar</b> to view details about the media item",
event: "click",
eventElement: "[data-element='editor-media'] [data-element='media-grid-item-0'] [data-element='media-grid-item-edit']"
"element": "[data-element='editor-media'] [data-element='media-grid-item-0']",
"title": "View media item details",
"content": "Hover the media item and <b>Click the purple bar</b> to view details about the media item",
"event": "click",
"eventElement": "[data-element='editor-media'] [data-element='media-grid-item-0'] [data-element='media-grid-item-edit']"
},
{
element: "[data-element='editor-media'] [data-element='property-umbracoFile']",
title: "The uploaded image",
content: "<p>Here you can see the image you have uploaded.</p>"
"element": "[data-element='editor-media'] [data-element='property-umbracoFile']",
"title": "The uploaded image",
"content": "<p>Here you can see the image you have uploaded.</p>"
},
{
element: "[data-element='editor-media'] [data-element='property-umbracoBytes']",
title: "Image size",
content: "<p>You will also find other details about the image, like the size.</p><p>Media items work in much the same way as content. So you can add extra properties to an image by creating or editing the <b>Media types</b> in the Settings section.</p>"
"element": "[data-element='editor-media'] [data-element='property-umbracoBytes']",
"title": "Image size",
"content": "<p>You will also find other details about the image, like the size.</p><p>Media items work in much the same way as content. So you can add extra properties to an image by creating or editing the <b>Media types</b> in the Settings section.</p>"
},
{
element: "[data-element='editor-media'] [data-element='tab-_umb_infoTab']",
title: "Info",
content: "Like the content section you can also find default information about the media item. You will find these under the info tab.",
event: "click"
"element": "[data-element='editor-media'] [data-element='tab-_umb_infoTab']",
"title": "Info",
"content": "Like the content section you can also find default information about the media item. You will find these under the info tab.",
"event": "click"
},
{
element: "[data-element='editor-media'] [data-element='node-info-urls']",
title: "Link to media",
content: "The path to the media item..."
"element": "[data-element='editor-media'] [data-element='node-info-urls']",
"title": "Link to media",
"content": "The path to the media item..."
},
{
element: "[data-element='editor-media'] [data-element='node-info-update-date']",
title: "Last edited",
content: "...and information about when the media item has been created and edited."
"element": "[data-element='editor-media'] [data-element='node-info-update-date']",
"title": "Last edited",
"content": "...and information about when the media item has been created and edited."
},
{
element: "[data-element='editor-container']",
title: "Using media items",
content: "You can reference a media item directly in a template by using the path or try adding a Media Picker to a document type property so you can select media items from the content section."
"element": "[data-element='editor-container']",
"title": "Using media items",
"content": "You can reference a media item directly in a template by using the path or try adding a Media Picker to a document type property so you can select media items from the content section."
}
]
}
@@ -22,9 +22,9 @@
<!--Developer-->
<add initialize="true" sortOrder="0" alias="packager" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.PackagesTreeController, umbraco" />
<add initialize="true" sortOrder="1" alias="dataTypes" application="developer" title="Data Types" iconClosed="icon-folder" iconOpen="icon-folder" type="Umbraco.Web.Trees.DataTypeTreeController, umbraco" />
<add application="developer" alias="macros" title="Macros" type="umbraco.loadMacros, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="2" />
<add initialize="true" sortOrder="2" alias="macros" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.MacroTreeController, umbraco" />
<add application="developer" alias="relationTypes" title="Relation Types" type="umbraco.loadRelationTypes, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="4" />
<add application="developer" alias="xslt" title="XSLT Files" type="umbraco.loadXslt, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="5" />
<add initialize="true" sortOrder="5" alias="xslt" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.XsltTreeController, umbraco" />
<add application="developer" alias="partialViewMacros" type="Umbraco.Web.Trees.PartialViewMacrosTreeController, umbraco" silent="false" initialize="true" sortOrder="6" title="Partial View Macro Files" iconClosed="icon-folder" iconOpen="icon-folder" />
<!--Users-->
+6 -6
View File
@@ -20,11 +20,11 @@
<!--Developer-->
<add initialize="true" sortOrder="0" alias="packager" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.PackagesTreeController, umbraco" />
<add initialize="true" sortOrder="1" alias="dataTypes" application="developer" title="Data Types" iconClosed="icon-folder" iconOpen="icon-folder" type="Umbraco.Web.Trees.DataTypeTreeController, umbraco" />
<add application="developer" alias="macros" title="Macros" type="umbraco.loadMacros, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="2" />
<add application="developer" alias="relationTypes" title="Relation Types" type="umbraco.loadRelationTypes, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="4" />
<add application="developer" alias="xslt" title="XSLT Files" type="umbraco.loadXslt, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="5" />
<add application="developer" alias="partialViewMacros" type="Umbraco.Web.Trees.PartialViewMacrosTreeController, umbraco" silent="false" initialize="true" sortOrder="6" title="Partial View Macro Files" iconClosed="icon-folder" iconOpen="icon-folder" />
<!--Users-->
<add initialize="true" sortOrder="2" alias="macros" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.MacroTreeController, umbraco" />
<add application="developer" alias="relationTypes" title="Relation Types" type="umbraco.loadRelationTypes, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="4" />
<add initialize="true" sortOrder="5" alias="xslt" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.XsltTreeController, umbraco" />
<add application="developer" alias="partialViewMacros" type="Umbraco.Web.Trees.PartialViewMacrosTreeController, umbraco" silent="false" initialize="true" sortOrder="6" title="Partial View Macro Files" iconClosed="icon-folder" iconOpen="icon-folder" />
<!--Users-->
<add initialize="true" sortOrder="0" alias="users" application="users" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.UserTreeController, umbraco" />
<!--Members-->
<add initialize="true" sortOrder="0" alias="member" application="member" title="Members" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.MemberTreeController, umbraco" />
@@ -39,5 +39,5 @@
<add initialize="true" sortOrder="2" alias="datasource" application="forms" title="Datasources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.DataSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="0" alias="form" application="forms" title="Forms" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="prevaluesource" application="forms" title="Prevalue sources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.PreValueSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="formsecurity" application="users" title="Forms Security" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormSecurityTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="formsecurity" application="users" title="Forms Security" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormSecurityTreeController, Umbraco.Forms.Web" />
</trees>
@@ -0,0 +1,33 @@
using System;
namespace Umbraco.Web.Editors
{
public sealed class EditorModelEventArgs<T> : EditorModelEventArgs
{
public EditorModelEventArgs(EditorModelEventArgs baseArgs)
: base(baseArgs.Model, baseArgs.UmbracoContext)
{
Model = (T)baseArgs.Model;
}
public EditorModelEventArgs(T model, UmbracoContext umbracoContext)
: base(model, umbracoContext)
{
Model = model;
}
public new T Model { get; private set; }
}
public class EditorModelEventArgs : EventArgs
{
public EditorModelEventArgs(object model, UmbracoContext umbracoContext)
{
Model = model;
UmbracoContext = umbracoContext;
}
public object Model { get; private set; }
public UmbracoContext UmbracoContext { get; private set; }
}
}
@@ -1,39 +1,9 @@
using System;
using System.Web.Http.Filters;
using Umbraco.Core.Events;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Editors
{
public class EditorModelEventArgs : EventArgs
{
public EditorModelEventArgs(object model, UmbracoContext umbracoContext)
{
Model = model;
UmbracoContext = umbracoContext;
}
public object Model { get; private set; }
public UmbracoContext UmbracoContext { get; private set; }
}
public sealed class EditorModelEventArgs<T> : EditorModelEventArgs
{
public EditorModelEventArgs(EditorModelEventArgs baseArgs)
: base(baseArgs.Model, baseArgs.UmbracoContext)
{
Model = (T)baseArgs.Model;
}
public EditorModelEventArgs(T model, UmbracoContext umbracoContext)
: base(model, umbracoContext)
{
Model = model;
}
public new T Model { get; private set; }
}
/// <summary>
/// Used to emit events for editor models in the back office
/// </summary>
@@ -42,6 +12,13 @@ namespace Umbraco.Web.Editors
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<ContentItemDisplay>> SendingContentModel;
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<MediaItemDisplay>> SendingMediaModel;
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<MemberDisplay>> SendingMemberModel;
public static event TypedEventHandler<HttpActionExecutedContext, EditorModelEventArgs<UserDisplay>> SendingUserModel;
private static void OnSendingUserModel(HttpActionExecutedContext sender, EditorModelEventArgs<UserDisplay> e)
{
var handler = SendingUserModel;
if (handler != null) handler(sender, e);
}
private static void OnSendingContentModel(HttpActionExecutedContext sender, EditorModelEventArgs<ContentItemDisplay> e)
{
@@ -85,6 +62,12 @@ namespace Umbraco.Web.Editors
{
OnSendingMemberModel(sender, new EditorModelEventArgs<MemberDisplay>(e));
}
var userDisplay = e.Model as UserDisplay;
if (userDisplay != null)
{
OnSendingUserModel(sender, new EditorModelEventArgs<UserDisplay>(e));
}
}
}
+72 -19
View File
@@ -2,9 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Web.Models;
@@ -25,24 +23,36 @@ namespace Umbraco.Web.Editors
if (UmbracoConfig.For.UmbracoSettings().BackOffice.Tours.EnableTours == false)
return result;
var coreTourFiles = Directory.GetFiles(
Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"), "*.json");
var filters = TourFilterResolver.Current.Filters.ToList();
//get all filters that will be applied to all tour aliases
var aliasOnlyFilters = filters.Where(x => x.PluginName == null && x.TourFileName == null).ToList();
foreach (var tourFile in coreTourFiles)
//don't pass in any filters for core tours that have a plugin name assigned
var nonPluginFilters = filters.Where(x => x.PluginName == null).ToList();
//add core tour files
var coreToursPath = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours");
if (Directory.Exists(coreToursPath))
{
var contents = File.ReadAllText(tourFile);
result.Add(new BackOfficeTourFile
foreach (var tourFile in Directory.EnumerateFiles(coreToursPath, "*.json"))
{
FileName = Path.GetFileNameWithoutExtension(tourFile),
Tours = JsonConvert.DeserializeObject<BackOfficeTour[]>(contents)
});
TryParseTourFile(tourFile, result, nonPluginFilters, aliasOnlyFilters);
}
}
//collect all tour files in packges
//collect all tour files in packages
foreach (var plugin in Directory.EnumerateDirectories(IOHelper.MapPath(SystemDirectories.AppPlugins)))
{
var pluginName = Path.GetFileName(plugin.TrimEnd('\\'));
var pluginFilters = filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)).ToList();
//If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely
var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null);
if (isPluginFiltered) continue;
//combine matched package filters with filters not specific to a package
var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList();
foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice"))
{
@@ -50,13 +60,7 @@ namespace Umbraco.Web.Editors
{
foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json"))
{
var contents = File.ReadAllText(tourFile);
result.Add(new BackOfficeTourFile
{
FileName = Path.GetFileNameWithoutExtension(tourFile),
PluginName = pluginName,
Tours = JsonConvert.DeserializeObject<BackOfficeTour[]>(contents)
});
TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName);
}
}
}
@@ -64,5 +68,54 @@ namespace Umbraco.Web.Editors
return result.OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase);
}
private void TryParseTourFile(string tourFile,
ICollection<BackOfficeTourFile> result,
List<BackOfficeTourFilter> filters,
List<BackOfficeTourFilter> aliasOnlyFilters,
string pluginName = null)
{
var fileName = Path.GetFileNameWithoutExtension(tourFile);
if (fileName == null) return;
//get the filters specific to this file
var fileFilters = filters.Where(x => x.TourFileName != null && x.TourFileName.IsMatch(fileName)).ToList();
//If there is any filter applied to match the file only (no tour alias) then ignore the file entirely
var isFileFiltered = fileFilters.Any(x => x.TourAlias == null);
if (isFileFiltered) return;
//now combine all aliases to filter below
var aliasFilters = aliasOnlyFilters.Concat(filters.Where(x => x.TourAlias != null))
.Select(x => x.TourAlias)
.ToList();
try
{
var contents = File.ReadAllText(tourFile);
var tours = JsonConvert.DeserializeObject<BackOfficeTour[]>(contents);
var tour = new BackOfficeTourFile
{
FileName = Path.GetFileNameWithoutExtension(tourFile),
PluginName = pluginName,
Tours = tours
.Where(x => aliasFilters.Count == 0 || aliasFilters.All(filter => filter.IsMatch(x.Alias)) == false)
.ToArray()
};
//don't add if all of the tours are filtered
if (tour.Tours.Any())
result.Add(tour);
}
catch (IOException e)
{
throw new IOException("Error while trying to read file: " + tourFile, e);
}
catch (JsonReaderException e)
{
throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e);
}
}
}
}
@@ -182,6 +182,7 @@ namespace Umbraco.Web.Editors
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[OutgoingEditorModelEvent]
public UserDisplay GetById(int id)
{
var user = Services.UserService.GetUserById(id);
@@ -494,6 +495,7 @@ namespace Umbraco.Web.Editors
/// </summary>
/// <param name="userSave"></param>
/// <returns></returns>
[OutgoingEditorModelEvent]
public async Task<UserDisplay> PostSaveUser(UserSave userSave)
{
if (userSave == null) throw new ArgumentNullException("userSave");
@@ -0,0 +1,24 @@
using Umbraco.Core.Collections;
using Umbraco.Web.WebApi;
namespace Umbraco.Web.Features
{
/// <summary>
/// Represents disabled features.
/// </summary>
internal class DisabledFeatures
{
/// <summary>
/// Initializes a new instance of the <see cref="DisabledFeatures"/> class.
/// </summary>
public DisabledFeatures()
{
Controllers = new TypeList<UmbracoApiControllerBase>();
}
/// <summary>
/// Gets the disabled controllers.
/// </summary>
public TypeList<UmbracoApiControllerBase> Controllers { get; private set; }
}
}
@@ -0,0 +1,28 @@
using Umbraco.Core.ObjectResolution;
namespace Umbraco.Web.Features
{
internal class FeaturesResolver : SingleObjectResolverBase<FeaturesResolver, UmbracoFeatures>
{
public FeaturesResolver(UmbracoFeatures value)
: base(value)
{ }
/// <summary>
/// Sets the features.
/// </summary>
/// <remarks>For developers, at application startup.</remarks>
public void SetFeatures(UmbracoFeatures features)
{
Value = features;
}
/// <summary>
/// Gets the features.
/// </summary>
public UmbracoFeatures Features
{
get { return Value; }
}
}
}
@@ -0,0 +1,42 @@
using System;
using Umbraco.Web.WebApi;
namespace Umbraco.Web.Features
{
/// <summary>
/// Represents the Umbraco features.
/// </summary>
internal class UmbracoFeatures
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoFeatures"/> class.
/// </summary>
public UmbracoFeatures()
{
Disabled = new DisabledFeatures();
}
// note
// currently, the only thing a FeatureSet does is list disabled controllers,
// but eventually we could enable and disable more parts of Umbraco. and then
// we would need some logic to figure out what's enabled/disabled - hence it's
// better to use IsEnabled, where the logic would go, rather than directly
// accessing the Disabled collection.
/// <summary>
/// Gets the disabled features.
/// </summary>
public DisabledFeatures Disabled { get; set; }
/// <summary>
/// Determines whether a feature is enabled.
/// </summary>
public bool IsEnabled(Type feature)
{
if (typeof(UmbracoApiControllerBase).IsAssignableFrom(feature))
return Disabled.Controllers.Contains(feature) == false;
throw new NotSupportedException("Not a supported feature type.");
}
}
}
+6 -5
View File
@@ -1,11 +1,10 @@
using System;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models
{
/// <summary>
/// A model representing a tour.
/// </summary>
[DataContract(Name = "tour", Namespace = "")]
public class BackOfficeTour
{
@@ -15,6 +14,8 @@ namespace Umbraco.Web.Models
public string Alias { get; set; }
[DataMember(Name = "group")]
public string Group { get; set; }
[DataMember(Name = "groupOrder")]
public int GroupOrder { get; set; }
[DataMember(Name = "allowDisable")]
public bool AllowDisable { get; set; }
[DataMember(Name = "steps")]
@@ -3,6 +3,9 @@ using System.Runtime.Serialization;
namespace Umbraco.Web.Models
{
/// <summary>
/// A model representing the file used to load a tour.
/// </summary>
[DataContract(Name = "tourFile", Namespace = "")]
public class BackOfficeTourFile
{
@@ -0,0 +1,61 @@
using System.Text.RegularExpressions;
namespace Umbraco.Web.Models
{
public class BackOfficeTourFilter
{
public Regex PluginName { get; private set; }
public Regex TourFileName { get; private set; }
public Regex TourAlias { get; private set; }
/// <summary>
/// Create a filter to filter out a whole plugin's tours
/// </summary>
/// <param name="pluginName"></param>
/// <returns></returns>
public static BackOfficeTourFilter FilterPlugin(Regex pluginName)
{
return new BackOfficeTourFilter(pluginName, null, null);
}
/// <summary>
/// Create a filter to filter out a whole tour file
/// </summary>
/// <param name="tourFileName"></param>
/// <returns></returns>
public static BackOfficeTourFilter FilterFile(Regex tourFileName)
{
return new BackOfficeTourFilter(null, tourFileName, null);
}
/// <summary>
/// Create a filter to filter out a tour alias, this will filter out the same alias found in all files
/// </summary>
/// <param name="tourAlias"></param>
/// <returns></returns>
public static BackOfficeTourFilter FilterAlias(Regex tourAlias)
{
return new BackOfficeTourFilter(null, null, tourAlias);
}
/// <summary>
/// Constructor to create a tour filter
/// </summary>
/// <param name="pluginName">Value to filter out tours by a plugin, can be null</param>
/// <param name="tourFileName">Value to filter out a tour file, can be null</param>
/// <param name="tourAlias">Value to filter out a tour alias, can be null</param>
/// <remarks>
/// Depending on what is null will depend on how the filter is applied.
/// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check tour alias is not NULL and then match it,
/// if any steps is NULL then the filters upstream are applied.
/// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from the plugin "hello" but not from other plugins if the same file name exists.
/// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the plugin or file name
/// </remarks>
public BackOfficeTourFilter(Regex pluginName, Regex tourFileName, Regex tourAlias)
{
PluginName = pluginName;
TourFileName = tourFileName;
TourAlias = tourAlias;
}
}
}
+4 -1
View File
@@ -2,6 +2,9 @@
namespace Umbraco.Web.Models
{
/// <summary>
/// A model representing a step in a tour.
/// </summary>
[DataContract(Name = "step", Namespace = "")]
public class BackOfficeTourStep
{
@@ -16,7 +19,7 @@ namespace Umbraco.Web.Models
[DataMember(Name = "elementPreventClick")]
public bool ElementPreventClick { get; set; }
[DataMember(Name = "backdropOpacity")]
public float BackdropOpacity { get; set; }
public float? BackdropOpacity { get; set; }
[DataMember(Name = "event")]
public string Event { get; set; }
}
@@ -0,0 +1,26 @@
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
{
/// <summary>
/// A model representing the navigation ("apps") inside an editor in the back office
/// </summary>
[DataContract(Name = "user", Namespace = "")]
public class EditorNavigation
{
[DataMember(Name = "name")]
public string Name { get; set; }
[DataMember(Name = "alias")]
public string Alias { get; set; }
[DataMember(Name = "icon")]
public string Icon { get; set; }
[DataMember(Name = "view")]
public string View { get; set; }
[DataMember(Name = "active")]
public bool Active { get; set; }
}
}
@@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
{
{
/// <summary>
/// Represents information for the current user
/// </summary>
@@ -29,7 +29,7 @@ namespace Umbraco.Web.Models.ContentEditing
[Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
[EditorBrowsable(EditorBrowsableState.Never)]
[ReadOnly(true)]
[DataMember(Name = "userType")]
[DataMember(Name = "userType")]
public string UserType { get; set; }
[ReadOnly(true)]
@@ -64,8 +64,8 @@ namespace Umbraco.Web.Models.ContentEditing
/// A list of sections the user is allowed to view.
/// </summary>
[DataMember(Name = "allowedSections")]
public IEnumerable<string> AllowedSections { get; set; }
public IEnumerable<string> AllowedSections { get; set; }
}
}
@@ -18,8 +18,13 @@ namespace Umbraco.Web.Models.ContentEditing
AvailableCultures = new Dictionary<string, string>();
StartContentIds = new List<EntityBasic>();
StartMediaIds = new List<EntityBasic>();
Navigation = new List<EditorNavigation>();
}
[DataMember(Name = "navigation")]
[ReadOnly(true)]
public IEnumerable<EditorNavigation> Navigation { get; set; }
/// <summary>
/// Gets the available cultures (i.e. to populate a drop down)
/// The key is the culture stored in the database, the value is the Name
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Models.Mapping;
@@ -231,12 +232,14 @@ namespace Umbraco.Web.Models.Mapping
});
//Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that
// this will cause an N+1 and we'll need to change how this works.
// this will cause an N+1 and we'll need to change how this works.
config.CreateMap<IUser, UserDisplay>()
.ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetUserAvatarUrls(applicationContext.ApplicationCache.RuntimeCache)))
.ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username))
.ForMember(detail => detail.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?) user.LastLoginDate))
.ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups))
.ForMember(detail => detail.Navigation, opt => opt.MapFrom(user => CreateUserEditorNavigation(applicationContext.Services.TextService)))
.ForMember(
detail => detail.CalculatedStartContentIds,
opt => opt.MapFrom(user => GetStartNodeValues(
@@ -255,7 +258,7 @@ namespace Umbraco.Web.Models.Mapping
"media/mediaRoot")))
.ForMember(
detail => detail.StartContentIds,
opt => opt.MapFrom(user => GetStartNodeValues(
opt => opt.MapFrom(user => GetStartNodeValues(
user.StartContentIds.ToArray(),
applicationContext.Services.TextService,
applicationContext.Services.EntityService,
@@ -263,7 +266,7 @@ namespace Umbraco.Web.Models.Mapping
"content/contentRoot")))
.ForMember(
detail => detail.StartMediaIds,
opt => opt.MapFrom(user => GetStartNodeValues(
opt => opt.MapFrom(user => GetStartNodeValues(
user.StartMediaIds.ToArray(),
applicationContext.Services.TextService,
applicationContext.Services.EntityService,
@@ -330,7 +333,7 @@ namespace Umbraco.Web.Models.Mapping
//the best we can do here is to return the user's first user group as a IUserType object
//but we should attempt to return any group that is the built in ones first
var groups = user.Groups.ToArray();
detail.UserGroups = user.Groups.Select(x => x.Alias).ToArray();
detail.UserGroups = user.Groups.Select(x => x.Alias).ToArray();
if (groups.Length == 0)
{
@@ -358,6 +361,21 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id)));
}
private IEnumerable<EditorNavigation> CreateUserEditorNavigation(ILocalizedTextService textService)
{
return new[]
{
new EditorNavigation
{
Active = true,
Alias = "details",
Icon = "icon-umb-users",
Name = textService.Localize("general/user"),
View = "views/users/views/user/details.html"
}
};
}
private IEnumerable<EntityBasic> GetStartNodeValues(int[] startNodeIds,
ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType,
string localizedKey)
+1 -1
View File
@@ -187,7 +187,7 @@ namespace Umbraco.Web.Models.Trees
nodeType,
item == null ? "" : item.Name, currentSection),
action => LaunchDialogUrl(action.Url, action.DialogTitle))
.OnFailure(() => LegacyTreeDataConverter.GetLegacyConfirmView(Action, currentSection),
.OnFailure(() => LegacyTreeDataConverter.GetLegacyConfirmView(Action),
view => LaunchDialogView(
view,
ui.GetText("defaultdialogs", "confirmdelete") + " '" + (item == null ? "" : item.Name) + "' ?"));
@@ -40,6 +40,7 @@ using System.Security;
[assembly: InternalsVisibleTo("Umbraco.Deploy.Cloud")]
[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")]
[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.AspNet")]
[assembly: InternalsVisibleTo("Umbraco.Headless")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Umbraco.Forms.Core")]
@@ -12,7 +12,7 @@ namespace Umbraco.Web.Scheduling
/// depending on whether the task is implemented as a sync or async method, and then
/// optionnally overriding RunsOnShutdown, to indicate whether the latched task should run
/// immediately on shutdown, or just be abandonned (default).</remarks>
public abstract class LatchedBackgroundTaskBase : DisposableObject, ILatchedBackgroundTask
public abstract class LatchedBackgroundTaskBase : DisposableObjectSlim, ILatchedBackgroundTask
{
private TaskCompletionSource<bool> _latch;
@@ -72,7 +72,7 @@ namespace Umbraco.Web.Scheduling
if (_runner.TryAdd(this))
_timer.Change(_periodMilliseconds, 0);
else
Dispose(true);
Dispose();
}
/// <summary>
+1 -1
View File
@@ -23,7 +23,7 @@ namespace Umbraco.Web.Security
/// <summary>
/// A utility class used for dealing with USER security in Umbraco
/// </summary>
public class WebSecurity : DisposableObject
public class WebSecurity : DisposableObjectSlim
{
private HttpContextBase _httpContext;
private ApplicationContext _applicationContext;
+79
View File
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Logging;
using Umbraco.Core.ObjectResolution;
using Umbraco.Web.Models;
namespace Umbraco.Web
{
/// <summary>
/// Allows for adding filters for tours during startup
/// </summary>
public class TourFilterResolver : ManyObjectsResolverBase<TourFilterResolver, BackOfficeTourFilter>
{
public TourFilterResolver(IServiceProvider serviceProvider, ILogger logger) : base(serviceProvider, logger)
{
}
private readonly HashSet<BackOfficeTourFilter> _instances = new HashSet<BackOfficeTourFilter>();
public IEnumerable<BackOfficeTourFilter> Filters
{
get { return Values; }
}
/// <summary>
/// Adds a filter instance
/// </summary>
/// <param name="filter"></param>
public void AddFilter(BackOfficeTourFilter filter)
{
using (Resolution.Configuration)
_instances.Add(filter);
}
/// <summary>
/// Removes a filter instance
/// </summary>
/// <param name="filter"></param>
public void RemoveFilter(BackOfficeTourFilter filter)
{
using (Resolution.Configuration)
_instances.Remove(filter);
}
/// <summary>
/// Removes a filter instance based on callback
/// </summary>
/// <param name="filter"></param>
public void RemoveFilterWhere(Func<BackOfficeTourFilter, bool> filter)
{
using (Resolution.Configuration)
_instances.RemoveWhere(new Predicate<BackOfficeTourFilter>(filter));
}
/// <inheritdoc />
/// <summary>
/// Overridden to return the combined created instances based on the resolved Types and the Concrete values added with AddFilter
/// </summary>
/// <returns></returns>
protected override IEnumerable<BackOfficeTourFilter> CreateInstances()
{
var createdInstances = base.CreateInstances();
return createdInstances.Concat(_instances);
}
public override void Clear()
{
base.Clear();
_instances.Clear();
}
internal override void ResetCollections()
{
base.ResetCollections();
_instances.Clear();
}
}
}
@@ -84,10 +84,19 @@ namespace Umbraco.Web.Trees
private async Task<TreeNode> GetRootForMultipleAppTree(ApplicationTree configTree, FormDataCollection queryStrings)
{
if (configTree == null) throw new ArgumentNullException("configTree");
var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext);
if (byControllerAttempt.Success)
{
return byControllerAttempt.Result;
try
{
var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext);
if (byControllerAttempt.Success)
{
return byControllerAttempt.Result;
}
}
catch (HttpResponseException)
{
//if this occurs its because the user isn't authorized to view that tree, in this case since we are loading multiple trees we
//will just return null so that it's not added to the list.
return null;
}
var legacyAttempt = configTree.TryGetRootNodeFromLegacyTree(queryStrings, Url, configTree.ApplicationAlias);
@@ -72,23 +72,64 @@ namespace Umbraco.Web.Trees
return nodes;
}
protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
protected virtual MenuItemCollection GetMenuForRootNode(FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
//set the default to create
menu.DefaultMenuAlias = ActionNew.Instance.Alias;
//create action
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
//refresh action
menu.Items.Add<RefreshNode, ActionRefresh>(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
return menu;
}
protected virtual MenuItemCollection GetMenuForFolder(string path, FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
//set the default to create
menu.DefaultMenuAlias = ActionNew.Instance.Alias;
//create action
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
var hasChildren = FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
//We can only delete folders if it doesn't have any children (folders or files)
if (hasChildren == false)
{
//delete action
menu.Items.Add<ActionDelete>(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true);
}
//refresh action
menu.Items.Add<RefreshNode, ActionRefresh>(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
return menu;
}
protected virtual MenuItemCollection GetMenuForFile(string path, FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
//if it's not a directory then we only allow to delete the item
menu.Items.Add<ActionDelete>(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
return menu;
}
protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
{
//if root node no need to visit the filesystem so lets just create the menu and return it
if (id == Constants.System.Root.ToInvariantString())
{
//set the default to create
menu.DefaultMenuAlias = ActionNew.Instance.Alias;
//create action
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
//refresh action
menu.Items.Add<RefreshNode, ActionRefresh>(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
return menu;
return GetMenuForRootNode(queryStrings);
}
var menu = new MenuItemCollection();
var path = string.IsNullOrEmpty(id) == false && id != Constants.System.Root.ToInvariantString()
? HttpUtility.UrlDecode(id).TrimStart("/")
: "";
@@ -98,30 +139,10 @@ namespace Umbraco.Web.Trees
if (isDirectory)
{
//set the default to create
menu.DefaultMenuAlias = ActionNew.Instance.Alias;
//create action
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
var hasChildren = FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
//We can only delete folders if it doesn't have any children (folders or files)
if (hasChildren == false)
{
//delete action
menu.Items.Add<ActionDelete>(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true);
}
//refresh action
menu.Items.Add<RefreshNode, ActionRefresh>(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
}
else if (isFile)
{
//if it's not a directory then we only allow to delete the item
menu.Items.Add<ActionDelete>(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
return GetMenuForFolder(path, queryStrings);
}
return menu;
return isFile ? GetMenuForFile(path, queryStrings) : menu;
}
}
}
@@ -134,7 +134,7 @@ namespace Umbraco.Web.Trees
Attempt
.Try(GetUrlAndTitleFromLegacyAction(currentAction, xmlTreeNode.NodeID, xmlTreeNode.NodeType, xmlTreeNode.Text, currentSection),
action => menuItem.LaunchDialogUrl(action.Url, action.DialogTitle))
.OnFailure(() => GetLegacyConfirmView(currentAction, currentSection),
.OnFailure(() => GetLegacyConfirmView(currentAction),
view => menuItem.LaunchDialogView(
view,
ui.GetText("defaultdialogs", "confirmdelete") + " '" + xmlTreeNode.Text + "' ?"))
@@ -164,9 +164,8 @@ namespace Umbraco.Web.Trees
/// This will look at the legacy IAction's JsFunctionName and convert it to a confirmation dialog view if possible
/// </summary>
/// <param name="action"></param>
/// <param name="currentSection"></param>
/// <returns></returns>
internal static Attempt<string> GetLegacyConfirmView(IAction action, string currentSection)
internal static Attempt<string> GetLegacyConfirmView(IAction action)
{
if (action.JsFunctionName.IsNullOrWhiteSpace())
{
@@ -0,0 +1,73 @@
using System;
using System.Net.Http.Formatting;
using umbraco;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Mvc;
using System.Linq;
using umbraco.BusinessLogic.Actions;
using Umbraco.Web.WebApi.Filters;
using Umbraco.Core.Services;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Macros)]
[Tree(Constants.Applications.Developer, Constants.Trees.Macros, null, sortOrder: 2)]
[LegacyBaseTree(typeof(loadMacros))]
[PluginController("UmbracoTrees")]
[CoreTree]
public class MacroTreeController : TreeController
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
var intId = id.TryConvertTo<int>();
if (intId == false) throw new InvalidOperationException("Id must be an integer");
var nodes = new TreeNodeCollection();
nodes.AddRange(
Services.MacroService.GetAll()
.OrderBy(entity => entity.Name)
.Select(macro =>
{
var node = CreateTreeNode(macro.Id.ToInvariantString(), id, queryStrings, macro.Name, "icon-settings-alt", false);
node.Path = "-1," + macro.Id;
node.AssignLegacyJsCallback("javascript:UmbClientMgr.contentFrame('developer/macros/editMacro.aspx?macroID=" + macro.Id + "');");
return node;
}));
return nodes;
}
protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
if (id == Constants.System.Root.ToInvariantString())
{
//set the default to create
menu.DefaultMenuAlias = ActionNew.Instance.Alias;
// root actions
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)))
.ConvertLegacyMenuItem(null, Constants.Trees.Macros, queryStrings.GetValue<string>("application"));
menu.Items.Add<RefreshNode, ActionRefresh>(ui.Text("actions", ActionRefresh.Instance.Alias), true);
return menu;
}
//TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy dialogs
var menuItem = menu.Items.Add(ActionDelete.Instance, Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
var legacyConfirmView = LegacyTreeDataConverter.GetLegacyConfirmView(ActionDelete.Instance);
if (legacyConfirmView == false)
throw new InvalidOperationException("Could not resolve the confirmation view for the legacy action " + ActionDelete.Instance.Alias);
menuItem.LaunchDialogView(
legacyConfirmView.Result,
Services.TextService.Localize("general/delete"));
return menu;
}
}
}
@@ -8,7 +8,7 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Scripts)]
[Tree(Constants.Applications.Settings, "scripts", null, sortOrder: 4)]
[Tree(Constants.Applications.Settings, Constants.Trees.Scripts, null, sortOrder: 4)]
[LegacyBaseTree(typeof(loadScripts))]
[PluginController("UmbracoTrees")]
[CoreTree]
@@ -0,0 +1,80 @@
using System;
using System.Net.Http.Formatting;
using umbraco;
using umbraco.BusinessLogic.Actions;
using Umbraco.Core.IO;
using Umbraco.Core.Services;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Xslt)]
[Tree(Constants.Applications.Developer, Constants.Trees.Xslt, null, sortOrder: 5)]
[LegacyBaseTree(typeof(loadXslt))]
[PluginController("UmbracoTrees")]
[CoreTree]
public class XsltTreeController : FileSystemTreeController
{
protected override void OnRenderFileNode(ref TreeNode treeNode)
{
////TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy views
treeNode.AssignLegacyJsCallback("javascript:UmbClientMgr.contentFrame('developer/xslt/editXslt.aspx?file=" + treeNode.Id + "');");
}
protected override void OnRenderFolderNode(ref TreeNode treeNode)
{
//TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy views
treeNode.AssignLegacyJsCallback("javascript:void(0);");
}
protected override MenuItemCollection GetMenuForFile(string path, FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
//TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy dialogs
var menuItem = menu.Items.Add(ActionDelete.Instance, Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
var legacyConfirmView = LegacyTreeDataConverter.GetLegacyConfirmView(ActionDelete.Instance);
if (legacyConfirmView == false)
throw new InvalidOperationException("Could not resolve the confirmation view for the legacy action " + ActionDelete.Instance.Alias);
menuItem.LaunchDialogView(
legacyConfirmView.Result,
Services.TextService.Localize("general/delete"));
return menu;
}
protected override MenuItemCollection GetMenuForRootNode(FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
//set the default to create
menu.DefaultMenuAlias = ActionNew.Instance.Alias;
// root actions
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)))
.ConvertLegacyMenuItem(null, Constants.Trees.Xslt, queryStrings.GetValue<string>("application"));
menu.Items.Add<RefreshNode, ActionRefresh>(ui.Text("actions", ActionRefresh.Instance.Alias), true);
return menu;
}
protected override IFileSystem2 FileSystem
{
get { return FileSystemProviderManager.Current.XsltFileSystem; }
}
private static readonly string[] ExtensionsStatic = { "xslt" };
protected override string[] Extensions
{
get { return ExtensionsStatic; }
}
protected override string FileIcon
{
get { return "icon-code"; }
}
}
}
+10
View File
@@ -315,9 +315,11 @@
<Compile Include="Cache\TemplateCacheRefresher.cs" />
<Compile Include="Cache\UnpublishedPageCacheRefresher.cs" />
<Compile Include="Cache\UserCacheRefresher.cs" />
<Compile Include="Features\DisabledFeatures.cs" />
<Compile Include="Editors\BackOfficeNotificationsController.cs" />
<Compile Include="Editors\BackOfficeServerVariables.cs" />
<Compile Include="Editors\DashboardHelper.cs" />
<Compile Include="Editors\EditorModelEventArgs.cs" />
<Compile Include="Editors\EditorValidationResolver.cs" />
<Compile Include="Editors\EditorValidator.cs" />
<Compile Include="Editors\FromJsonPathAttribute.cs" />
@@ -328,6 +330,8 @@
<Compile Include="Editors\ParameterSwapControllerActionSelector.cs" />
<Compile Include="Editors\CodeFileController.cs" />
<Compile Include="Editors\TourController.cs" />
<Compile Include="Models\BackOfficeTourFilter.cs" />
<Compile Include="TourFilterResolver.cs" />
<Compile Include="Editors\UserEditorAuthorizationHelper.cs" />
<Compile Include="Editors\UserGroupAuthorizationAttribute.cs" />
<Compile Include="Editors\UserGroupEditorAuthorizationHelper.cs" />
@@ -335,6 +339,8 @@
<Compile Include="Editors\UserGroupValidateAttribute.cs" />
<Compile Include="ExamineStartup.cs" />
<Compile Include="Extensions\UdiExtensions.cs" />
<Compile Include="Features\UmbracoFeatures.cs" />
<Compile Include="Features\FeaturesResolver.cs" />
<Compile Include="HealthCheck\Checks\Config\AbstractConfigCheck.cs" />
<Compile Include="HealthCheck\Checks\Config\AcceptableConfiguration.cs" />
<Compile Include="HealthCheck\Checks\Config\ConfigurationService.cs" />
@@ -376,6 +382,7 @@
<Compile Include="Models\ContentEditing\AssignedContentPermissions.cs" />
<Compile Include="Models\ContentEditing\AssignedUserGroupPermissions.cs" />
<Compile Include="Models\ContentEditing\ContentRedirectUrl.cs" />
<Compile Include="Models\ContentEditing\EditorNavigation.cs" />
<Compile Include="Models\ContentEditing\GetAvailableCompositionsFilter.cs" />
<Compile Include="Editors\IEditorValidator.cs" />
<Compile Include="Editors\EditorModelEventManager.cs" />
@@ -496,6 +503,7 @@
<Compile Include="Models\Mapping\PropertyTypeGroupResolver.cs" />
<Compile Include="Security\Identity\PreviewAuthenticationMiddleware.cs" />
<Compile Include="SingletonHttpContextAccessor.cs" />
<Compile Include="Trees\MacroTreeController.cs" />
<Compile Include="Trees\UserTreeController.cs" />
<Compile Include="Suspendable.cs" />
<Compile Include="Trees\ContentBlueprintTreeController.cs" />
@@ -504,6 +512,7 @@
<Compile Include="Trees\MediaTypeTreeController.cs" />
<Compile Include="Trees\MemberTypeTreeController.cs" />
<Compile Include="Security\WebAuthExtensions.cs" />
<Compile Include="Trees\XsltTreeController.cs" />
<Compile Include="umbraco.presentation\SafeXmlReaderWriter.cs" />
<Compile Include="Trees\ScriptTreeController.cs" />
<Compile Include="umbraco.presentation\umbraco\developer\Packages\installer.aspx.cs">
@@ -794,6 +803,7 @@
<Compile Include="WebApi\Filters\ClearAngularAntiForgeryTokenAttribute.cs" />
<Compile Include="WebApi\Filters\DisableBrowserCacheAttribute.cs" />
<Compile Include="WebApi\Filters\EnableOverrideAuthorizationAttribute.cs" />
<Compile Include="WebApi\Filters\FeatureAuthorizeAttribute.cs" />
<Compile Include="WebApi\Filters\FilterGrouping.cs" />
<Compile Include="WebApi\Filters\LegacyTreeAuthorizeAttribute.cs" />
<Compile Include="WebApi\Filters\OutgoingEditorModelEventAttribute.cs" />
+1 -1
View File
@@ -20,7 +20,7 @@ namespace Umbraco.Web
/// <summary>
/// Class that encapsulates Umbraco information of a specific HTTP request
/// </summary>
public class UmbracoContext : DisposableObject, IDisposeOnRequestEnd
public class UmbracoContext : DisposableObjectSlim, IDisposeOnRequestEnd
{
internal const string HttpContextItemName = "Umbraco.Web.UmbracoContext";
private static readonly object Locker = new object();
@@ -0,0 +1,19 @@
using System.Web.Http;
using System.Web.Http.Controllers;
using Umbraco.Web.Features;
namespace Umbraco.Web.WebApi.Filters
{
/// <summary>
/// Ensures that the controller is an authorized feature.
/// </summary>
/// <remarks>Else returns unauthorized.</remarks>
public sealed class FeatureAuthorizeAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var controllerType = actionContext.ControllerContext.ControllerDescriptor.ControllerType;
return FeaturesResolver.Current.Features.IsEnabled(controllerType);
}
}
}
@@ -6,12 +6,14 @@ using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Services;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.WebApi
{
/// <summary>
/// The base class for API controllers that expose Umbraco services - THESE ARE NOT AUTO ROUTED
/// </summary>
[FeatureAuthorize]
public abstract class UmbracoApiControllerBase : ApiController
{
protected UmbracoApiControllerBase()
@@ -35,7 +37,7 @@ namespace Umbraco.Web.WebApi
InstanceId = Guid.NewGuid();
_umbraco = umbracoHelper;
}
private UmbracoHelper _umbraco;
/// <summary>
+5
View File
@@ -38,6 +38,7 @@ using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Web.Editors;
using Umbraco.Web.Features;
using Umbraco.Web.HealthCheck;
using Umbraco.Web.Profiling;
using Umbraco.Web.Search;
@@ -352,6 +353,10 @@ namespace Umbraco.Web
protected override void InitializeResolvers()
{
base.InitializeResolvers();
FeaturesResolver.Current = new FeaturesResolver(new UmbracoFeatures());
TourFilterResolver.Current = new TourFilterResolver(ServiceProvider, LoggerResolver.Current.Logger);
SearchableTreeResolver.Current = new SearchableTreeResolver(ServiceProvider, LoggerResolver.Current.Logger, ApplicationContext.Services.ApplicationTreeService, () => PluginManager.ResolveSearchableTrees());
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.IO;
using System.Text;
@@ -29,10 +30,8 @@ using Umbraco.Core;
namespace umbraco
{
/// <summary>
/// Handles loading of the cache application into the developer application tree
/// </summary>
[Tree(Constants.Applications.Developer, "macros", "Macros", sortOrder: 2)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This is no longer used and will be removed from the codebase in the future")]
public class loadMacros : BaseTree
{
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.IO;
using System.Text;
@@ -29,10 +30,8 @@ using Umbraco.Core;
namespace umbraco
{
/// <summary>
/// Handles loading of the xslt files into the application tree
/// </summary>
[Tree(Constants.Applications.Developer, "xslt", "XSLT Files", sortOrder: 5)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This is no longer used and will be removed from the codebase in the future")]
public class loadXslt : FileSystemTree
{
@@ -42,8 +42,8 @@ namespace umbraco.cms.presentation.developer
if (IsPostBack == false)
{
ClientTools
.SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree<loadMacros>().Tree.Alias)
.SyncTree("-1,init," + _macro.Id, false);
.SetActiveTreeType(Constants.Trees.Macros)
.SyncTree("-1," + _macro.Id, false);
string tempMacroAssembly = _macro.ControlAssembly ?? "";
string tempMacroType = _macro.ControlType ?? "";
@@ -321,8 +321,8 @@ namespace umbraco.cms.presentation.developer
Page.Validate();
ClientTools
.SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree<loadMacros>().Tree.Alias)
.SyncTree("-1,init," + _macro.Id.ToInvariantString(), true); //true forces the reload
.SetActiveTreeType(Constants.Trees.Macros)
.SyncTree("-1," + _macro.Id.ToInvariantString(), true); //true forces the reload
var tempMacroAssembly = macroAssembly.Text;
var tempMacroType = macroType.Text;
@@ -33,9 +33,9 @@ namespace umbraco.cms.presentation.developer
if (!IsPostBack)
{
string file = Request.QueryString["file"];
string path = DeepLink.GetTreePathFromFilePath(file);
string path = DeepLink.GetTreePathFromFilePath(file, false, true);
ClientTools
.SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree<loadXslt>().Tree.Alias)
.SetActiveTreeType(Constants.Trees.Xslt)
.SyncTree(path, false);
}
@@ -14,7 +14,7 @@ using Umbraco.Core.IO;
namespace umbraco.cms.businesslogic.packager.repositories
{
[Obsolete("This should not be used and will be removed in future Umbraco versions")]
public class Repository : DisposableObject
public class Repository : DisposableObjectSlim
{
public string Guid { get; private set; }
@@ -289,7 +289,7 @@ namespace umbraco.cms.businesslogic.packager.repositories
}
/// <summary>
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObject"/> which handles common required locking logic.
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObjectSlim"/> which handles common required locking logic.
/// </summary>
protected override void DisposeResources()
{
+12 -2
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using umbraco.BusinessLogic;
using System.Web;
@@ -11,15 +12,24 @@ namespace umbraco.cms.helpers
public class DeepLink
{
public static string GetTreePathFromFilePath(string filePath)
{
return GetTreePathFromFilePath(filePath, true, false);
}
internal static string GetTreePathFromFilePath(string filePath, bool includeInit, bool urlEncode)
{
List<string> treePath = new List<string>();
treePath.Add("-1");
treePath.Add("init");
if (includeInit)
treePath.Add("init");
string[] pathPaths = filePath.Split('/');
for (int p = 0; p < pathPaths.Length; p++)
{
treePath.Add(string.Join("/", pathPaths.Take(p + 1).ToArray()));
var s = string.Join("/", pathPaths.Take(p + 1).ToArray());
if (urlEncode)
s = WebUtility.UrlEncode(s);
treePath.Add(s);
}
string sPath = string.Join(",", treePath.ToArray());
return sPath;