diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index a0fa732c8c..46c5bf851c 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -36,6 +36,8 @@ namespace Umbraco.Core.IO public static string Scripts => IOHelper.ReturnPath("umbracoScriptsPath", "~/scripts"); + public static string StyleSheets => IOHelper.ReturnPath("umbracoStylesheetsPath", "~/css"); + public static string Umbraco => IOHelper.ReturnPath("umbracoPath", "~/umbraco"); //TODO: Consider removing this diff --git a/src/Umbraco.Core/Models/Stylesheet.cs b/src/Umbraco.Core/Models/Stylesheet.cs index a228b70105..87632fac27 100644 --- a/src/Umbraco.Core/Models/Stylesheet.cs +++ b/src/Umbraco.Core/Models/Stylesheet.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Models { } internal Stylesheet(string path, Func getFileContent) - : base(path.EnsureEndsWith(".css"), getFileContent) + : base(string.IsNullOrEmpty(path) ? path : path.EnsureEndsWith(".css"), getFileContent) { InitializeProperties(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.controller.js new file mode 100644 index 0000000000..229587cc63 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.controller.js @@ -0,0 +1,18 @@ +(function () { + "use strict"; + + function StyleSheetsCreateController($scope, $location, navigationService) { + + var vm = this; + var node = $scope.dialogOptions.currentNode; + + vm.createFile = createFile; + + function createFile() { + $location.path("/settings/stylesheets/edit/" + node.id).search("create", "true"); + navigationService.hideMenu(); + } + } + + angular.module("umbraco").controller("Umbraco.Editors.StyleSheets.CreateController", StyleSheetsCreateController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.html b/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.html new file mode 100644 index 0000000000..82854635f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.html @@ -0,0 +1,23 @@ + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/stylesheets/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/stylesheets/edit.controller.js new file mode 100644 index 0000000000..33aa0f979b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/stylesheets/edit.controller.js @@ -0,0 +1,205 @@ +(function () { + "use strict"; + + function StyleSheetsEditController($scope, $routeParams, $timeout, appState, editorState, navigationService, assetsService, codefileResource, contentEditingHelper, notificationsService, localizationService, templateHelper, angularHelper) { + + var vm = this; + var currentPosition = null; + + vm.page = {}; + vm.page.loading = true; + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; + vm.page.saveButtonState = "init"; + + //Used to toggle the keyboard shortcut modal + //From a custom keybinding in ace editor - that conflicts with our own to show the dialog + vm.showKeyboardShortcut = false; + + //Keyboard shortcuts for help dialog + vm.page.keyboardShortcutsOverview = []; + + templateHelper.getGeneralShortcuts().then(function(shortcuts){ + vm.page.keyboardShortcutsOverview.push(shortcuts); + }); + + templateHelper.getEditorShortcuts().then(function(shortcuts){ + vm.page.keyboardShortcutsOverview.push(shortcuts); + }); + + vm.stylesheet = {}; + + // bind functions to view model + vm.save = save; + + /* Function bound to view model */ + + function save() { + + vm.page.saveButtonState = "busy"; + + vm.stylesheet.content = vm.editor.getValue(); + + contentEditingHelper.contentEditorPerformSave({ + saveMethod: codefileResource.save, + scope: $scope, + content: vm.stylesheet, + // We do not redirect on failure for style sheets - this is because it is not possible to actually save the style sheet + // when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) {} + }).then(function (saved) { + + localizationService.localizeMany(["speechBubbles_fileSavedHeader", "speechBubbles_fileSavedText"]).then(function(data){ + var header = data[0]; + var message = data[1]; + notificationsService.success(header, message); + }); + + //check if the name changed, if so we need to redirect + if (vm.stylesheet.id !== saved.id) { + contentEditingHelper.redirectToRenamedContent(saved.id); + } + else { + vm.page.saveButtonState = "success"; + vm.stylesheet = saved; + + //sync state + editorState.set(vm.stylesheet); + + // sync tree + navigationService.syncTree({ tree: "stylesheets", path: vm.stylesheet.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + }, function (err) { + + vm.page.saveButtonState = "error"; + + localizationService.localizeMany(["speechBubbles_validationFailedHeader", "speechBubbles_validationFailedMessage"]).then(function(data){ + var header = data[0]; + var message = data[1]; + notificationsService.error(header, message); + }); + + }); + + + } + + /* Local functions */ + + function init() { + + //we need to load this somewhere, for now its here. + assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); + + if ($routeParams.create) { + codefileResource.getScaffold("stylesheets", $routeParams.id).then(function (stylesheet) { + ready(stylesheet, false); + }); + } else { + codefileResource.getByPath('stylesheets', $routeParams.id).then(function (stylesheet) { + ready(stylesheet, true); + }); + } + + } + + function ready(stylesheet, syncTree) { + + vm.page.loading = false; + + vm.stylesheet = stylesheet; + + //sync state + editorState.set(vm.stylesheet); + + if (syncTree) { + navigationService.syncTree({ tree: "stylesheets", path: vm.stylesheet.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + vm.aceOption = { + mode: "stylesheet", + theme: "chrome", + showPrintMargin: false, + advanced: { + fontSize: '14px', + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: false + }, + onLoad: function(_editor) { + + vm.editor = _editor; + + //Update the auto-complete method to use ctrl+alt+space + _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); + + //Unassigns the keybinding (That was previously auto-complete) + //As conflicts with our own tree search shortcut + _editor.commands.bindKey("ctrl-space", null); + + //TODO: Move all these keybinding config out into some helper/service + _editor.commands.addCommands([ + //Disable (alt+shift+K) + //Conflicts with our own show shortcuts dialog - this overrides it + { + name: 'unSelectOrFindPrevious', + bindKey: 'Alt-Shift-K', + exec: function() { + //Toggle the show keyboard shortcuts overlay + $scope.$apply(function(){ + vm.showKeyboardShortcut = !vm.showKeyboardShortcut; + }); + }, + readOnly: true + } + ]); + + // initial cursor placement + // Keep cursor in name field if we are create a new style sheet + // else set the cursor at the bottom of the code editor + if(!$routeParams.create) { + $timeout(function(){ + vm.editor.navigateFileEnd(); + vm.editor.focus(); + }); + } + + vm.editor.on("change", changeAceEditor); + + } + } + + function changeAceEditor() { + setFormState("dirty"); + } + + function setFormState(state) { + + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + + // set state + if(state === "dirty") { + currentForm.$setDirty(); + } else if(state === "pristine") { + currentForm.$setPristine(); + } + } + + + } + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.StyleSheets.EditController", StyleSheetsEditController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/stylesheets/edit.html b/src/Umbraco.Web.UI.Client/src/views/stylesheets/edit.html new file mode 100644 index 0000000000..61252f5a54 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/stylesheets/edit.html @@ -0,0 +1,58 @@ +
+ + + +
+ + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+ +
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 f2bf1c2c60..6dbcd0d870 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -315,6 +315,7 @@ New partial view from snippet New partial view macro from snippet New partial view macro (without macro) + New style sheet file Browse your website diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web/Editors/CodeFileController.cs index edcd71b2e8..6bd1030dc3 100644 --- a/src/Umbraco.Web/Editors/CodeFileController.cs +++ b/src/Umbraco.Web/Editors/CodeFileController.cs @@ -17,6 +17,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Umbraco.Web.Trees; +using Stylesheet = Umbraco.Core.Models.Stylesheet; namespace Umbraco.Web.Editors { @@ -168,6 +169,18 @@ namespace Umbraco.Web.Editors return display; } throw new HttpResponseException(HttpStatusCode.NotFound); + + case Core.Constants.Trees.Stylesheets: + var stylesheet = Services.FileService.GetStylesheetByName(virtualPath); + if (stylesheet != null) + { + var display = Mapper.Map(stylesheet); + display.FileType = Core.Constants.Trees.Stylesheets; + display.Path = Url.GetTreePathFromFilePath(stylesheet.Path); + display.Id = System.Web.HttpUtility.UrlEncode(stylesheet.Path); + return display; + } + throw new HttpResponseException(HttpStatusCode.NotFound); } throw new HttpResponseException(HttpStatusCode.NotFound); @@ -235,6 +248,10 @@ namespace Umbraco.Web.Editors codeFileDisplay = Mapper.Map(new Script(string.Empty)); codeFileDisplay.VirtualPath = SystemDirectories.Scripts; break; + case Core.Constants.Trees.Stylesheets: + codeFileDisplay = Mapper.Map(new Stylesheet(string.Empty)); + codeFileDisplay.VirtualPath = SystemDirectories.StyleSheets; + break; default: throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Unsupported editortype")); } @@ -369,13 +386,17 @@ namespace Umbraco.Web.Editors display.Id = System.Web.HttpUtility.UrlEncode(scriptResult.Path); return display; - //display.AddErrorNotification( - // Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), - // Services.TextService.Localize("speechBubbles/partialViewErrorText")); - - + //display.AddErrorNotification( + // Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), + // Services.TextService.Localize("speechBubbles/partialViewErrorText")); + case Core.Constants.Trees.Stylesheets: + var stylesheetResult = CreateOrUpdateStylesheet(display); + display = Mapper.Map(stylesheetResult, display); + display.Path = Url.GetTreePathFromFilePath(stylesheetResult.Path); + display.Id = System.Web.HttpUtility.UrlEncode(stylesheetResult.Path); + return display; default: throw new HttpResponseException(HttpStatusCode.NotFound); @@ -431,6 +452,54 @@ namespace Umbraco.Web.Editors return script; } + private Stylesheet CreateOrUpdateStylesheet(CodeFileDisplay display) + { + return CreateOrUpdateFile(display, ".css", Current.FileSystems.StylesheetsFileSystem, + name => Services.FileService.GetStylesheetByName(name), + (stylesheet, userId) => Services.FileService.SaveStylesheet(stylesheet, userId), + name => new Stylesheet(name) + ); + } + + private T CreateOrUpdateFile(CodeFileDisplay display, string extension, IFileSystem fileSystem, + Func getFileByName, Action saveFile, Func createFile) where T : Core.Models.File + { + //must always end with the correct extension + display.Name = EnsureCorrectFileExtension(display.Name, extension); + + var virtualPath = display.VirtualPath ?? string.Empty; + // this is all weird, should be using relative paths everywhere! + var relPath = fileSystem.GetRelativePath(virtualPath); + + if (relPath.EndsWith(extension) == false) + { + //this would typically mean it's new + relPath = relPath.IsNullOrWhiteSpace() + ? relPath + display.Name + : relPath.EnsureEndsWith('/') + display.Name; + } + + var file = getFileByName(relPath); + if (file != null) + { + // might need to find the path + var orgPath = file.OriginalPath.Substring(0, file.OriginalPath.IndexOf(file.Name)); + file.Path = orgPath + display.Name; + + file.Content = display.Content; + //try/catch? since this doesn't return an Attempt? + saveFile(file, Security.CurrentUser.Id); + } + else + { + file = createFile(relPath); + file.Content = display.Content; + saveFile(file, Security.CurrentUser.Id); + } + + return file; + } + private Attempt CreateOrUpdatePartialView(CodeFileDisplay display) { return CreateOrUpdatePartialView(display, SystemDirectories.PartialViews, diff --git a/src/Umbraco.Web/Models/Mapping/CodeFileMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/CodeFileMapperProfile.cs index 082abfdace..94c43f8f11 100644 --- a/src/Umbraco.Web/Models/Mapping/CodeFileMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/CodeFileMapperProfile.cs @@ -1,6 +1,7 @@ using AutoMapper; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; +using Stylesheet = Umbraco.Core.Models.Stylesheet; namespace Umbraco.Web.Models.Mapping { @@ -20,6 +21,12 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.Path, opt => opt.Ignore()) .ForMember(dest => dest.Snippet, opt => opt.Ignore()); + CreateMap() + .ForMember(dest => dest.FileType, opt => opt.Ignore()) + .ForMember(dest => dest.Notifications, opt => opt.Ignore()) + .ForMember(dest => dest.Path, opt => opt.Ignore()) + .ForMember(dest => dest.Snippet, opt => opt.Ignore()); + CreateMap() .IgnoreEntityCommonProperties() .ForMember(dest => dest.Id, opt => opt.Ignore())