From f4d0de6665d3389d789d47642184d27db654844c Mon Sep 17 00:00:00 2001 From: Shannon Date: Sun, 4 Jun 2017 14:43:28 +0200 Subject: [PATCH 01/41] get services/repos working for blueprints along with tests --- src/Umbraco.Core/Constants-ObjectTypes.cs | 12 +- src/Umbraco.Core/Models/UmbracoObjectTypes.cs | 8 ++ .../Repositories/ContentRepository.cs | 37 +++++- .../Interfaces/IContentRepository.cs | 2 +- .../Repositories/VersionableRepositoryBase.cs | 20 +--- .../Persistence/RepositoryFactory.cs | 16 +++ src/Umbraco.Core/Services/ContentService.cs | 88 +++++++++++++- src/Umbraco.Core/Services/IContentService.cs | 13 ++- src/Umbraco.Core/UdiEntityType.cs | 4 + .../Services/ContentServiceTests.cs | 107 +++++++++++++++++- 10 files changed, 273 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index adc154174a..bb12ec9efd 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -68,12 +68,22 @@ namespace Umbraco.Core /// Guid for a Document object. /// public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; - + /// /// Guid for a Document object. /// public static readonly Guid DocumentGuid = new Guid(Document); + /// + /// Guid for a Document Blueprint object. + /// + public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA"; + + /// + /// Guid for a Document object. + /// + public static readonly Guid DocumentBlueprintGuid = new Guid(DocumentBlueprint); + /// /// Guid for a Document Type object. /// diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 0df7a21e57..3e633fc020 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -38,6 +38,14 @@ namespace Umbraco.Core.Models [UmbracoUdiType(Constants.UdiEntityType.Document)] Document, + /// + /// Document + /// + [UmbracoObjectType(Constants.ObjectTypes.DocumentBlueprint, typeof(IContent))] + [FriendlyName("DocumentBlueprint")] + [UmbracoUdiType(Constants.UdiEntityType.Document)] + DocumentBlueprint, + /// /// Media /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index a34a77d3b4..cd763f4e32 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -20,6 +20,27 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { + /// + /// Override the base content repository so we can change the node object type + /// + /// + /// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, however to do that + /// requires quite a lot of work since we'd need to re-organize the interhitance quite a lot or create a helper class to perform a lot of the underlying logic. + /// + /// TODO: Create a helper method to conain most of the underlying logic for the ContentRepository + /// + internal class ContentBlueprintRepository : ContentRepository + { + public ContentBlueprintRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cacheHelper, logger, syntaxProvider, contentTypeRepository, templateRepository, tagRepository, contentSection) + { + } + + protected override Guid NodeObjectTypeId + { + get { return Constants.ObjectTypes.DocumentBlueprintGuid; } + } + } + /// /// Represents a repository for doing CRUD operations for /// @@ -201,7 +222,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Guid NodeObjectTypeId { - get { return new Guid(Constants.ObjectTypes.Document); } + get { return Constants.ObjectTypes.DocumentGuid; } } #endregion @@ -742,7 +763,19 @@ namespace Umbraco.Core.Persistence.Repositories return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); } - + + public IEnumerable GetBlueprints(IQuery query) + { + Func, Sql> translate = t => t.Translate(); + + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); + var translatorFull = new SqlTranslator(sqlFull, query); + var sqlIds = GetBaseQuery(BaseQueryType.Ids); + var translatorIds = new SqlTranslator(sqlIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); + } + /// /// This builds the Xml document used for the XML cache /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index e04608e9c6..00e0868ef0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories -{ +{ public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository { diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 98743a6a63..96db4428d0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -52,25 +52,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Id of the to retrieve versions from /// An enumerable list of the same object with different versions - public virtual IEnumerable GetAllVersions(int id) - { - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.NodeId == id) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - - var dtos = Database.Fetch(sql); - foreach (var dto in dtos) - { - yield return GetByVersion(dto.VersionId); - } - } + public abstract IEnumerable GetAllVersions(int id); /// /// Gets a list of all version Ids for the given content item ordered so latest is first diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index afb93f620f..3b8a2be53a 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -145,6 +145,22 @@ namespace Umbraco.Core.Persistence }; } + public virtual IContentRepository CreateContentBlueprintRepository(IScopeUnitOfWork uow) + { + return new ContentBlueprintRepository( + uow, + _cacheHelper, + _logger, + _sqlSyntax, + CreateContentTypeRepository(uow), + CreateTemplateRepository(uow), + CreateTagRepository(uow), + _settings.Content) + { + EnsureUniqueNaming = _settings.Content.EnsureUniqueNaming + }; + } + public virtual IContentTypeRepository CreateContentTypeRepository(IScopeUnitOfWork uow) { return new ContentTypeRepository( diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index c00b226d3e..ab15f6acc4 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -174,8 +174,7 @@ namespace Umbraco.Core.Services content.WriterId = userId; uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parentId)); - - Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); + uow.Commit(); } @@ -217,8 +216,7 @@ namespace Umbraco.Core.Services content.WriterId = userId; uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parent)); - - Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); + uow.Commit(); } @@ -1155,6 +1153,73 @@ namespace Umbraco.Core.Services return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); } + public void SaveBlueprint(IContent content, int userId = 0) + { + //always ensure the blueprint is at the root + content.ParentId = -1; + + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + if (string.IsNullOrWhiteSpace(content.Name)) + { + throw new ArgumentException("Cannot save content blueprint with empty name."); + } + + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + + if (content.HasIdentity == false) + { + content.CreatorId = userId; + } + content.WriterId = userId; + + repository.AddOrUpdate(content); + + uow.Commit(); + } + } + } + + public void DeleteBlueprint(IContent content, int userId = 0) + { + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + repository.Delete(content); + uow.Commit(); + } + } + } + + public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0) + { + if (blueprint == null) throw new ArgumentNullException("blueprint"); + + var contentType = blueprint.ContentType; + var content = new Content(name, -1, contentType); + content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id); + + using (var uow = UowProvider.GetUnitOfWork()) + { + content.CreatorId = userId; + content.WriterId = userId; + + //Now we need to map all of the properties over! + foreach (var property in blueprint.Properties) + { + content.SetValue(property.Alias, property.Value); + } + + uow.Commit(); + } + + return content; + } + /// /// Saves a single object /// @@ -1787,6 +1852,21 @@ namespace Umbraco.Core.Services return true; } + public IEnumerable GetDocumentBlueprints(params int[] documentTypeIds) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + + var query = new Query(); + if (documentTypeIds.Length > 0) + { + query.Where(x => documentTypeIds.Contains(x.ContentTypeId)); + } + return repository.GetByQuery(query); + } + } + /// /// Gets paged content descendants as XML by path /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 4af87d2b58..f497eea5bc 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -16,10 +16,10 @@ namespace Umbraco.Core.Services /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. /// public interface IContentServiceOperations - { - //TODO: Remove this class in v8 - - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... + { + //TODO: Remove this class in v8 + + //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... /// /// Saves a single object @@ -96,6 +96,11 @@ namespace Umbraco.Core.Services /// public interface IContentService : IContentServiceBase { + IEnumerable GetDocumentBlueprints(params int[] documentTypeIds); + void SaveBlueprint(IContent content, int userId = 0); + void DeleteBlueprint(IContent content, int userId = 0); + IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0); + /// /// Gets all XML entries found in the cmsContentXml table based on the given path /// diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs index f6b9b1e3b0..926489f02d 100644 --- a/src/Umbraco.Core/UdiEntityType.cs +++ b/src/Umbraco.Core/UdiEntityType.cs @@ -24,6 +24,10 @@ namespace Umbraco.Core [UdiType(UdiType.GuidUdi)] public const string Document = "document"; + + [UdiType(UdiType.GuidUdi)] + public const string DocumentBluePrint = "document-blueprint"; + [UdiType(UdiType.GuidUdi)] public const string Media = "media"; [UdiType(UdiType.GuidUdi)] diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index c598b272d7..71eacdcf04 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -46,11 +46,112 @@ namespace Umbraco.Tests.Services VersionableRepositoryBase.ThrowOnWarning = false; base.TearDown(); - } - - //TODO Add test to verify there is only ONE newest document/content in cmsDocument table after updating. + } + + //TODO Add test to verify there is only ONE newest document/content in cmsDocument table after updating. //TODO Add test to delete specific version (with and without deleting prior versions) and versions by date. + [Test] + public void Create_Blueprint() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentTypeService.Save(contentType); + + var blueprint = MockedContent.CreateTextpageContent(contentType, "hello", -1); + blueprint.SetValue("title", "blueprint 1"); + blueprint.SetValue("bodyText", "blueprint 2"); + blueprint.SetValue("keywords", "blueprint 3"); + blueprint.SetValue("description", "blueprint 4"); + + contentService.SaveBlueprint(blueprint); + + var found = contentService.GetDocumentBlueprints().ToArray(); + Assert.AreEqual(1, found.Length); + + //ensures it's not found by normal content + var contentFound = contentService.GetById(found[0].Id); + Assert.IsNull(contentFound); + } + + [Test] + public void Delete_Blueprint() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentTypeService.Save(contentType); + + var blueprint = MockedContent.CreateTextpageContent(contentType, "hello", -1); + blueprint.SetValue("title", "blueprint 1"); + blueprint.SetValue("bodyText", "blueprint 2"); + blueprint.SetValue("keywords", "blueprint 3"); + blueprint.SetValue("description", "blueprint 4"); + + contentService.SaveBlueprint(blueprint); + + contentService.DeleteBlueprint(blueprint); + + var found = contentService.GetDocumentBlueprints().ToArray(); + Assert.AreEqual(0, found.Length); + } + + [Test] + public void Create_Content_From_Blueprint() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentTypeService.Save(contentType); + + var blueprint = MockedContent.CreateTextpageContent(contentType, "hello", -1); + blueprint.SetValue("title", "blueprint 1"); + blueprint.SetValue("bodyText", "blueprint 2"); + blueprint.SetValue("keywords", "blueprint 3"); + blueprint.SetValue("description", "blueprint 4"); + + contentService.SaveBlueprint(blueprint); + + var fromBlueprint = contentService.CreateContentFromBlueprint(blueprint, "hello world"); + contentService.Save(fromBlueprint); + + Assert.IsTrue(fromBlueprint.HasIdentity); + Assert.AreEqual("blueprint 1", fromBlueprint.Properties["title"].Value); + Assert.AreEqual("blueprint 2", fromBlueprint.Properties["bodyText"].Value); + Assert.AreEqual("blueprint 3", fromBlueprint.Properties["keywords"].Value); + Assert.AreEqual("blueprint 4", fromBlueprint.Properties["description"].Value); + } + + [Test] + public void Get_All_Blueprints() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + var ct1 = MockedContentTypes.CreateTextpageContentType("ct1"); + contentTypeService.Save(ct1); + var ct2 = MockedContentTypes.CreateTextpageContentType("ct2"); + contentTypeService.Save(ct2); + + for (int i = 0; i < 10; i++) + { + var blueprint = MockedContent.CreateTextpageContent(i % 2 == 0 ? ct1 : ct2, "hello" + i, -1); + contentService.SaveBlueprint(blueprint); + } + + var found = contentService.GetDocumentBlueprints().ToArray(); + Assert.AreEqual(10, found.Length); + + found = contentService.GetDocumentBlueprints(ct1.Id).ToArray(); + Assert.AreEqual(5, found.Length); + + found = contentService.GetDocumentBlueprints(ct2.Id).ToArray(); + Assert.AreEqual(5, found.Length); + } /// /// Ensures that we don't unpublish all nodes when a node is deleted that has an invalid path of -1 From adb02c96a76d852fde6ed1290182ec45cd954240 Mon Sep 17 00:00:00 2001 From: Shannon Date: Sun, 4 Jun 2017 15:54:51 +0200 Subject: [PATCH 02/41] Gets blueprint tree controller working, makes the content editor a directive :O --- src/Umbraco.Core/Constants-Applications.cs | 7 +- .../Repositories/EntityRepository.cs | 20 +- .../components/content/edit.controller.js | 243 ++++++++++++++++++ .../src/views/components/content/edit.html | 86 +++++++ .../views/content/content.edit.controller.js | 232 ----------------- .../src/views/content/edit.html | 87 +------ .../contentblueprints/delete.controller.js | 32 +++ .../src/views/contentblueprints/delete.html | 12 + .../src/views/contentblueprints/edit.html | 3 + src/Umbraco.Web.UI/config/trees.config | 3 +- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 1 + .../umbraco/config/lang/en_us.xml | 1 + .../Trees/ContentBlueprintTreeController.cs | 81 ++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 14 files changed, 480 insertions(+), 329 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/content/edit.html delete mode 100644 src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html create mode 100644 src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index fd9476a8ab..d98c12aeb1 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -56,7 +56,12 @@ /// /// alias for the content tree. /// - public const string Content = "content"; + public const string Content = "content"; + + /// + /// alias for the content blueprint tree. + /// + public const string ContentBlueprints = "contentBlueprints"; /// /// alias for the member tree. diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 4efa8a63ac..4e6bf6ee70 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -48,8 +48,8 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, IQuery filter = null) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var factory = new UmbracoEntityFactory(); var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, sql => @@ -178,8 +178,8 @@ namespace Umbraco.Core.Persistence.Repositories public IUmbracoEntity GetByKey(Guid key, Guid objectTypeId) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var sql = GetFullSqlForEntityType(key, isContent, isMedia, objectTypeId); @@ -225,8 +225,8 @@ namespace Umbraco.Core.Persistence.Repositories public virtual IUmbracoEntity Get(int id, Guid objectTypeId) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var sql = GetFullSqlForEntityType(id, isContent, isMedia, objectTypeId); @@ -280,8 +280,8 @@ namespace Umbraco.Core.Persistence.Repositories private IEnumerable PerformGetAll(Guid objectTypeId, Action filter = null) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, filter); var factory = new UmbracoEntityFactory(); @@ -324,8 +324,8 @@ namespace Umbraco.Core.Persistence.Repositories public virtual IEnumerable GetByQuery(IQuery query, Guid objectTypeId) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, null, objectTypeId); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js new file mode 100644 index 0000000000..2633b0854b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -0,0 +1,243 @@ +(function () { + 'use strict'; + + function ContentEditController($rootScope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { + + function link(scope, element, attrs, ctrl) { + //setup scope vars + scope.defaultButton = null; + scope.subButtons = []; + + scope.page = {}; + scope.page.loading = false; + scope.page.menu = {}; + scope.page.menu.currentNode = null; + scope.page.menu.currentSection = appState.getSectionState("currentSection"); + scope.page.listViewPath = null; + scope.page.isNew = $routeParams.create; + scope.page.buttonGroupState = "init"; + + function init(content) { + + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $routeParams.create, + content: content, + methods: { + saveAndPublish: scope.saveAndPublish, + sendToPublish: scope.sendToPublish, + save: scope.save, + unPublish: scope.unPublish + } + }); + scope.defaultButton = buttons.defaultButton; + scope.subButtons = buttons.subButtons; + + editorState.set(scope.content); + + //We fetch all ancestors of the node to generate the footer breadcrumb navigation + if (!$routeParams.create) { + if (content.parentId && content.parentId !== -1) { + entityResource.getAncestors(content.id, "document") + .then(function (anc) { + scope.ancestors = anc; + }); + } + } + } + + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { + + if (!scope.content.isChildOfListView) { + navigationService.syncTree({ tree: "content", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + scope.page.menu.currentNode = syncArgs.node; + }); + } + else if (initialLoad === true) { + + //it's a child item, just sync the ui node to the parent + navigationService.syncTree({ tree: "content", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); + + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise( + $http.get(content.treeNodeUrl), + 'Failed to retrieve data for child node ' + content.id).then(function (node) { + scope.page.menu.currentNode = node; + }); + } + } + + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish + function performSave(args) { + var deferred = $q.defer(); + + scope.page.buttonGroupState = "busy"; + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: scope, + content: scope.content, + action: args.action + }).then(function (data) { + //success + init(scope.content); + syncTreeNode(scope.content, data.path); + + scope.page.buttonGroupState = "success"; + + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set(scope.content); + } + + scope.page.buttonGroupState = "error"; + + deferred.reject(err); + }); + + return deferred.promise; + } + + function resetLastListPageNumber(content) { + // We're using rootScope to store the page number for list views, so if returning to the list + // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children + // we should remove this so as not to confuse if navigating to a different list + if (!content.isChildOfListView && !content.isContainer) { + $rootScope.lastListViewPageViewed = null; + } + } + + if ($routeParams.create) { + + scope.page.loading = true; + + //we are creating so get an empty content item + contentResource.getScaffold($routeParams.id, $routeParams.doctype) + .then(function (data) { + + scope.content = data; + + init(scope.content); + + resetLastListPageNumber(scope.content); + + scope.page.loading = false; + + }); + } + else { + + scope.page.loading = true; + + //we are editing so get the content item from the server + contentResource.getById($routeParams.id) + .then(function (data) { + + scope.content = data; + + if (data.isChildOfListView && data.trashed === false) { + scope.page.listViewPath = ($routeParams.page) ? + "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page : + "/content/content/edit/" + data.parentId; + } + + init(scope.content); + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + + syncTreeNode(scope.content, data.path, true); + + resetLastListPageNumber(scope.content); + + scope.page.loading = false; + + }); + } + + + scope.unPublish = function () { + + if (formHelper.submitForm({ scope: scope, statusMessage: "Unpublishing...", skipValidation: true })) { + + scope.page.buttonGroupState = "busy"; + + contentResource.unPublish(scope.content.id) + .then(function (data) { + + formHelper.resetForm({ scope: scope, notifications: data.notifications }); + + contentEditingHelper.handleSuccessfulSave({ + scope: scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties(scope.content, data) + }); + + init(scope.content); + + syncTreeNode(scope.content, data.path); + + scope.page.buttonGroupState = "success"; + + }); + } + + }; + + scope.sendToPublish = function () { + return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); + }; + + scope.saveAndPublish = function () { + return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); + }; + + scope.save = function () { + return performSave({ saveMethod: contentResource.save, statusMessage: "Saving...", action: "save" }); + }; + + scope.preview = function (content) { + + + if (!scope.busy) { + + // Chromes popup blocker will kick in if a window is opened + // outwith the initial scoped request. This trick will fix that. + // + var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); + scope.save().then(function (data) { + // Build the correct path so both /#/ and #/ work. + var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; + previewWindow.location.href = redirect; + }); + + + } + + }; + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/content/edit.html', + scope: { + + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('contentEditor', ContentEditController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html new file mode 100644 index 0000000000..b97676fb7e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html @@ -0,0 +1,86 @@ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js deleted file mode 100644 index e8e729fd72..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ /dev/null @@ -1,232 +0,0 @@ -/** - * @ngdoc controller - * @name Umbraco.Editors.Content.EditController - * @function - * - * @description - * The controller for the content editor - */ -function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { - - //setup scope vars - $scope.defaultButton = null; - $scope.subButtons = []; - - $scope.page = {}; - $scope.page.loading = false; - $scope.page.menu = {}; - $scope.page.menu.currentNode = null; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.listViewPath = null; - $scope.page.isNew = $routeParams.create; - $scope.page.buttonGroupState = "init"; - - function init(content) { - - var buttons = contentEditingHelper.configureContentEditorButtons({ - create: $routeParams.create, - content: content, - methods: { - saveAndPublish: $scope.saveAndPublish, - sendToPublish: $scope.sendToPublish, - save: $scope.save, - unPublish: $scope.unPublish - } - }); - $scope.defaultButton = buttons.defaultButton; - $scope.subButtons = buttons.subButtons; - - editorState.set($scope.content); - - //We fetch all ancestors of the node to generate the footer breadcrumb navigation - if (!$routeParams.create) { - if (content.parentId && content.parentId != -1) { - entityResource.getAncestors(content.id, "document") - .then(function (anc) { - $scope.ancestors = anc; - }); - } - } - } - - /** Syncs the content item to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(content, path, initialLoad) { - - if (!$scope.content.isChildOfListView) { - navigationService.syncTree({ tree: "content", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); - } - else if (initialLoad === true) { - - //it's a child item, just sync the ui node to the parent - navigationService.syncTree({ tree: "content", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node - // from the server so that we can load in the actions menu. - umbRequestHelper.resourcePromise( - $http.get(content.treeNodeUrl), - 'Failed to retrieve data for child node ' + content.id).then(function (node) { - $scope.page.menu.currentNode = node; - }); - } - } - - // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish - function performSave(args) { - var deferred = $q.defer(); - - $scope.page.buttonGroupState = "busy"; - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: args.statusMessage, - saveMethod: args.saveMethod, - scope: $scope, - content: $scope.content, - action: args.action - }).then(function (data) { - //success - init($scope.content); - syncTreeNode($scope.content, data.path); - - $scope.page.buttonGroupState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - - $scope.page.buttonGroupState = "error"; - - deferred.reject(err); - }); - - return deferred.promise; - } - - function resetLastListPageNumber(content) { - // We're using rootScope to store the page number for list views, so if returning to the list - // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children - // we should remove this so as not to confuse if navigating to a different list - if (!content.isChildOfListView && !content.isContainer) { - $rootScope.lastListViewPageViewed = null; - } - } - - if ($routeParams.create) { - - $scope.page.loading = true; - - //we are creating so get an empty content item - contentResource.getScaffold($routeParams.id, $routeParams.doctype) - .then(function (data) { - - $scope.content = data; - - init($scope.content); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); - } - else { - - $scope.page.loading = true; - - //we are editing so get the content item from the server - contentResource.getById($routeParams.id) - .then(function (data) { - - $scope.content = data; - - if (data.isChildOfListView && data.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) - ? "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page - : "/content/content/edit/" + data.parentId; - } - - init($scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - syncTreeNode($scope.content, data.path, true); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); - } - - - $scope.unPublish = function () { - - if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { - - $scope.page.buttonGroupState = "busy"; - - contentResource.unPublish($scope.content.id) - .then(function (data) { - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - init($scope.content); - - syncTreeNode($scope.content, data.path); - - $scope.page.buttonGroupState = "success"; - - }); - } - - }; - - $scope.sendToPublish = function () { - return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); - }; - - $scope.saveAndPublish = function () { - return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); - }; - - $scope.save = function () { - return performSave({ saveMethod: contentResource.save, statusMessage: "Saving...", action: "save" }); - }; - - $scope.preview = function (content) { - - - if (!$scope.busy) { - - // Chromes popup blocker will kick in if a window is opened - // outwith the initial scoped request. This trick will fix that. - // - var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); - $scope.save().then(function (data) { - // Build the correct path so both /#/ and #/ work. - var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; - previewWindow.location.href = redirect; - }); - - - } - - }; - -} - -angular.module("umbraco").controller("Umbraco.Editors.Content.EditController", ContentEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index 0ef68fe07b..04d4259e09 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -1,86 +1,3 @@ -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.controller.js new file mode 100644 index 0000000000..feae545a3e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.controller.js @@ -0,0 +1,32 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.ContentBlueprint.DeleteController + * @function + * + * @description + * The controller for deleting content blueprints + */ +function ContentBlueprintDeleteController($scope, codefileResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + + codefileResource.deleteByPath('partialViews', $scope.currentNode.id) + .then(function() { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.ContentBlueprint.DeleteController", ContentBlueprintDeleteController); diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.html new file mode 100644 index 0000000000..44a31254d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.html @@ -0,0 +1,12 @@ +
+
+ +

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

+ + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html new file mode 100644 index 0000000000..04d4259e09 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 13da9246b7..d32e58f093 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -16,6 +16,7 @@ + @@ -40,5 +41,5 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index d99cd8f43f..85d3aa79e6 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1340,6 +1340,7 @@ To manage your website, simply open the Umbraco back office and start adding con Upload translation XML + Content Blueprints Cache Browser Recycle Bin Created packages diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 63fc12101f..8e176246f8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1337,6 +1337,7 @@ To manage your website, simply open the Umbraco back office and start adding con Upload translation XML + Content Blueprints Cache Browser Recycle Bin Created packages diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs new file mode 100644 index 0000000000..7b18b015ea --- /dev/null +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Net.Http.Formatting; +using System.Web.Http; +using umbraco; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; +using Umbraco.Core.Services; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Trees +{ + /// + /// The content blueprint tree controller + /// + /// + /// This authorizes based on access to the content section even though it exists in the settings + /// + [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, null, sortOrder: 8)] + [PluginController("UmbracoTrees")] + [CoreTree] + public class ContentBlueprintTreeController : TreeController + { + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var nodes = new TreeNodeCollection(); + + //check if we're rendering the root + if (id == Constants.System.Root.ToInvariantString()) + { + var altStartId = string.Empty; + + if (queryStrings.HasKey(TreeQueryStringParameters.StartNodeId)) + altStartId = queryStrings.GetValue(TreeQueryStringParameters.StartNodeId); + + //check if a request has been made to render from a specific start node + if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != Constants.System.Root.ToString(CultureInfo.InvariantCulture)) + { + id = altStartId; + } + + var entities = Services.EntityService.GetChildren(Constants.System.Root, UmbracoObjectTypes.DocumentBlueprint).ToArray(); + + nodes.AddRange(entities + .Select(entity => CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprintGuid, id, queryStrings, "icon-blueprint", false)) + .Where(node => node != null)); + + return nodes; + } + + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + if (id == Constants.System.Root.ToInvariantString()) + { + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + return menu; + } + + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + + return menu; + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0b5652e509..77ab8d55c9 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -434,6 +434,7 @@ + From 028903c306ef0ac69cbc0657b8e1fbbf2c7a2408 Mon Sep 17 00:00:00 2001 From: Shannon Date: Sun, 4 Jun 2017 16:00:25 +0200 Subject: [PATCH 03/41] Creates endpoint to create a blueprint --- src/Umbraco.Web/Editors/ContentController.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index da5e0c3a3b..0e59124dc9 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -275,6 +275,24 @@ namespace Umbraco.Web.Editors return false; } + /// + /// Creates a blueprint from a content item + /// + /// The content id to copy + /// The name of the blueprint + /// + public IHttpActionResult CreateBlueprintFromContent(int contentId, string name) + { + var content = Services.ContentService.GetById(contentId); + if (content == null) return NotFound(); + + var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.GetUserId()); + + Services.ContentService.SaveBlueprint(blueprint, Security.GetUserId()); + + return Ok(); + } + /// /// Saves content /// From 9f0639ba242049579e5d0ea87950933e6ebdb327 Mon Sep 17 00:00:00 2001 From: Lars-Erik Aabech Date: Sun, 4 Jun 2017 16:07:11 +0200 Subject: [PATCH 04/41] Create blueprint from content action In process of adding blueprints to create content dialog --- .../Migrations/Initial/BaseDataCreation.cs | 4 +- .../src/common/resources/content.resource.js | 11 ++++++ .../content/content.create.controller.js | 26 +++++++++++-- .../content.createblueprint.controller.js | 35 ++++++++++++++++++ .../src/views/content/create.html | 37 ++++++------------- .../src/views/content/createblueprint.html | 17 +++++++++ .../Views/Partials/Grid/Bootstrap2.cshtml | 32 +++++----------- .../Views/Partials/Grid/Bootstrap3.cshtml | 32 +++++----------- .../Views/Partials/Grid/Editors/Base.cshtml | 1 + .../Views/Partials/Grid/Editors/Embed.cshtml | 7 +++- .../Views/Partials/Grid/Editors/Macro.cshtml | 2 + .../Views/Partials/Grid/Editors/Media.cshtml | 3 +- .../Partials/Grid/Editors/TextString.cshtml | 5 +-- src/Umbraco.Web.UI/js/bootstrap.min.js | 7 ++++ src/Umbraco.Web.UI/js/jquery.min.js | 4 ++ .../Trees/ContentTreeController.cs | 5 ++- .../ActionCreateBlueprintFromContent.cs | 35 ++++++++++++++++++ src/umbraco.cms/umbraco.cms.csproj | 1 + 18 files changed, 183 insertions(+), 81 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/content/createblueprint.html create mode 100644 src/Umbraco.Web.UI/js/bootstrap.min.js create mode 100644 src/Umbraco.Web.UI/js/jquery.min.js create mode 100644 src/umbraco.cms/Actions/ActionCreateBlueprintFromContent.cs diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 002af11611..2e32adcb54 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -170,9 +170,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial private void CreateUmbracoUserTypeData() { - _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 1, Alias = "admin", Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7" }); + _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 1, Alias = "admin", Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7ï" }); _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 2, Alias = "writer", Name = "Writer", DefaultPermissions = "CAH:F" }); - _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 3, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5F" }); + _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 3, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5Fï" }); _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 4, Alias = "translator", Name = "Translator", DefaultPermissions = "AF" }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index f44401f5c5..f98937759e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -665,6 +665,17 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { [{ id: id }])), 'Failed to publish content with id ' + id); + }, + + createBlueprintFromContent: function(id, name) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "CreateBlueprintFromContent", { + id: id, name: name + }) + ), + "Failed to create blueprint from content with id " + id + ); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 503caeb82d..e0e3bcee37 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -6,11 +6,29 @@ * @description * The controller for the content creation dialog */ -function contentCreateController($scope, $routeParams, contentTypeResource, iconHelper) { - - contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function(data) { +function contentCreateController($scope, $routeParams, contentTypeResource, iconHelper, $location) { + + contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); - }); + }); + + $scope.goToAction = function(docType) { + if (docType.blueprints && docType.blueprints.length) { + // Show dialog + } else { + $location.path( "/content/edit/" + + $scope.currentNode.id + + "?doctype=" + + docType.alias + + "&create=true" + ); + } + navigationService.hideNavigation(); + } + + $scope.createLinkAttributes = function(docType) { + return linkAttributes; + } } angular.module('umbraco').controller("Umbraco.Editors.Content.CreateController", contentCreateController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js new file mode 100644 index 0000000000..0ae435ddf1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js @@ -0,0 +1,35 @@ +(function() { + + function CreateBlueprintController($scope, + contentResource, + appState, + navigationService, + editorState, + notificationService) { + + $scope.name = editorState.current.name; + + $scope.create = function() { + contentResource.createBlueprintFromContent(editorState.current.id, $scope.name) + .then(function() { + notificationService.showNotification({ + type: 3, + header: "Created blueprint", + message: "Blueprint was created based on " + $scope.name + }); + $scope.closeDialogs(); + }); + }; + } + + angular.module("umbraco").controller("Umbraco.Editors.Content.CreateBlueprintController", + [ + "$scope", + "contentResource", + "navigationService", + "editorState", + "notificationsService", + CreateBlueprintController + ]); + +}()); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index 73bdc5c188..b4b63d949e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -9,31 +9,18 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/createblueprint.html b/src/Umbraco.Web.UI.Client/src/views/content/createblueprint.html new file mode 100644 index 0000000000..08fc36887a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/createblueprint.html @@ -0,0 +1,17 @@ +
+ + + +
diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml index c8f9ab7cd1..8b189ae1a0 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml @@ -2,10 +2,6 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* - Razor helpers located at the bottom of this file -*@ - @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; @@ -64,29 +60,21 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) - { - var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); - attrs.Add(property.Name + "=\"" + propertyValue + "\""); + foreach (JProperty property in cfg.Properties()) { + attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - { - var propertyValue = property.Value.ToString(); - if (string.IsNullOrWhiteSpace(propertyValue) == false) - { - cssVals.Add(property.Name + ":" + propertyValue + ";"); - } - } + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); - if (cssVals.Any()) - attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); + if (cssVals.Any()) + attrs.Add("style='" + string.Join(" ", cssVals) + "'"); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index 6ab5c1355a..e672aa2a11 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -2,10 +2,6 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* - Razor helpers located at the bottom of this file -*@ - @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; @@ -64,29 +60,21 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) - { - var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); - attrs.Add(property.Name + "=\"" + propertyValue + "\""); + foreach (JProperty property in cfg.Properties()) { + attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - { - var propertyValue = property.Value.ToString(); - if (string.IsNullOrWhiteSpace(propertyValue) == false) - { - cssVals.Add(property.Name + ":" + propertyValue + ";"); - } - } + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); - if (cssVals.Any()) - attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); + if (cssVals.Any()) + attrs.Add("style='" + string.Join(" ", cssVals) + "'"); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml index ffb7603048..a86c04819a 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml @@ -1,4 +1,5 @@ @model dynamic +@using Umbraco.Web.Templates @functions { public static string EditorView(dynamic contentItem) diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml index c27be6bcdf..393157bcf8 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml @@ -1,2 +1,7 @@ @model dynamic -@Html.Raw(Model.value) +@using Umbraco.Web.Templates + + +
+ @Html.Raw(Model.value) +
diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml index ed08bb2484..e0822808d8 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml @@ -1,4 +1,6 @@ @inherits UmbracoViewPage +@using Umbraco.Web.Templates + @if (Model.value != null) { diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml index 5b5adbdc7d..09d04219f2 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml @@ -1,4 +1,5 @@ @model dynamic +@using Umbraco.Web.Templates @if (Model.value != null) { @@ -13,7 +14,7 @@ } } - @Model.value.altText + @Model.value.caption if (Model.value.caption != null) { diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml index 8c92ca0d83..0cac4eb1ff 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml @@ -4,9 +4,8 @@ @if (Model.editor.config.markup != null) { string markup = Model.editor.config.markup.ToString(); - var umbracoHelper = new UmbracoHelper(UmbracoContext.Current); - - markup = markup.Replace("#value#", umbracoHelper.ReplaceLineBreaksForHtml(HttpUtility.HtmlEncode(Model.value.ToString()))); + + markup = markup.Replace("#value#", Model.value.ToString()); markup = markup.Replace("#style#", Model.editor.config.style.ToString()); diff --git a/src/Umbraco.Web.UI/js/bootstrap.min.js b/src/Umbraco.Web.UI/js/bootstrap.min.js new file mode 100644 index 0000000000..d839865900 --- /dev/null +++ b/src/Umbraco.Web.UI/js/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.3.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.1",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.1",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.1",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c="prev"==a?-1:1,d=this.getItemIndex(b),e=(d+c)%this.$items.length;return this.$items.eq(e)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i="next"==b?"first":"last",j=this;if(!f.length){if(!this.options.wrap)return;f=this.$element.find(".item")[i]()}if(f.hasClass("active"))return this.sliding=!1;var k=f[0],l=a.Event("slide.bs.carousel",{relatedTarget:k,direction:h});if(this.$element.trigger(l),!l.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var m=a(this.$indicators.children()[this.getItemIndex(f)]);m&&m.addClass("active")}var n=a.Event("slid.bs.carousel",{relatedTarget:k,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),j.sliding=!1,setTimeout(function(){j.$element.trigger(n)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(n)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.1",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.find("> .panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.1",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('