diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index b9d9447343..cb177539be 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -69,6 +69,11 @@ namespace Umbraco.Core.Models.Membership /// /// Will hold the media file system relative path of the users custom avatar if they uploaded one /// - string Avatar { get; set; } + string Avatar { get; set; } + + /// + /// A Json blob stored for recording tour data for a user + /// + string TourData { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index e01b070d18..892de4a7e1 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -99,6 +99,7 @@ namespace Umbraco.Core.Models.Membership private string _name; private string _securityStamp; private string _avatar; + private string _tourData; private int _sessionTimeout; private int[] _startContentIds; private int[] _startMediaIds; @@ -132,6 +133,7 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo SecurityStampSelector = ExpressionHelper.GetPropertyInfo(x => x.SecurityStamp); public readonly PropertyInfo AvatarSelector = ExpressionHelper.GetPropertyInfo(x => x.Avatar); + public readonly PropertyInfo TourDataSelector = ExpressionHelper.GetPropertyInfo(x => x.TourData); public readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentIds); public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaIds); @@ -464,8 +466,18 @@ namespace Umbraco.Core.Models.Membership { get { return _avatar; } set { SetPropertyValueAndDetectChanges(value, ref _avatar, Ps.Value.AvatarSelector); } - } + } + /// + /// A Json blob stored for recording tour data for a user + /// + [DataMember] + public string TourData + { + get { return _tourData; } + set { SetPropertyValueAndDetectChanges(value, ref _tourData, Ps.Value.TourDataSelector); } + } + /// /// Gets or sets the session timeout. /// diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 1507b4adf8..d147f79eb9 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -103,6 +103,14 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] [Length(500)] public string Avatar { get; set; } + + /// + /// A Json blob stored for recording tour data for a user + /// + [Column("tourData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string TourData { get; set; } [ResultColumn] public List UserGroupDtos { get; set; } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenEightZero/AddTourDataUserColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenEightZero/AddTourDataUserColumn.cs new file mode 100644 index 0000000000..ea39c20a2e --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenEightZero/AddTourDataUserColumn.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenEightZero +{ + [Migration("7.8.0", 1, Constants.System.UmbracoMigrationName)] + public class AddTourDataUserColumn : MigrationBase + { + public AddTourDataUserColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { + } + + public override void Up() + { + //Don't exeucte if the column is already there + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("tourData")) == false) + { + var textType = SqlSyntax.GetSpecialDbType(SpecialDbTypes.NTEXT); + Create.Column("tourData").OnTable("umbracoUser").AsCustom(textType).Nullable(); + } + + } + + public override void Down() + { + } + } + +} diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 408dfb68c8..e7a846f933 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -401,7 +401,8 @@ ORDER BY colName"; {"updateDate", "UpdateDate"}, {"avatar", "Avatar"}, {"emailConfirmedDate", "EmailConfirmedDate"}, - {"invitedDate", "InvitedDate"} + {"invitedDate", "InvitedDate"}, + {"tourData", "TourData"} }; //create list of properties that have changed diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c4c1d58fdc..2c2bea9c1e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -546,6 +546,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index c79812ccbb..412a009af5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -22,14 +22,16 @@ scope.model.completeTour = function() { unbindEvent(); - tourService.completeTour(scope.model); - backdropService.close(); + tourService.completeTour(scope.model).then(function() { + backdropService.close(); + }); }; scope.model.disableTour = function() { unbindEvent(); - tourService.disableTour(scope.model); - backdropService.close(); + tourService.disableTour(scope.model).then(function() { + backdropService.close(); + }); } function onInit() { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js index 233d510c3e..0a6b3cd6bc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js @@ -10,6 +10,30 @@ function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) { //the factory object returned return { + saveTourStatus: function (tourStatus) { + + if (!tourStatus) { + return angularHelper.rejectedPromise({ errorMsg: 'tourStatus cannot be empty' }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "PostSetUserTour"), + tourStatus), + 'Failed to save tour status'); + }, + + getTours: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "GetUserTours")), 'Failed to get tours'); + }, + performSetInvitedUserPassword: function (newPassword) { if (!newPassword) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 65c788be49..f160a6d343 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -1,419 +1,424 @@ (function () { 'use strict'; - function tourService(eventsService, localStorageService) { - - var localStorageKey = "umbTours"; + function tourService(eventsService, currentUserResource, $q) { + var currentTour = null; - var tours = [ - { - "name": "Introduction", - "alias": "umbIntroIntroduction", - "group": "Getting Started", - "allowDisable": true, - "steps": [ - { - title: "Welcome to Umbraco - The Friendly CMS", - content: "

Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible.

In this quick tour we will introduce you to the main areas of Umbraco and show you how to best get started.

", - type: "intro" - }, - { - element: "#applications", - elementPreventClick: true, - title: "Sections", - content: "These are the Sections and allows you to navigate the different areas of Umbraco.", - backdropOpacity: 0.6 - }, - - { - element: "#tree", - elementPreventClick: true, - title: "The Tree", - content: "This is the Tree and will contain all the content of your website." - }, - { - element: "[data-element='editor-content']", - elementPreventClick: true, - title: "Dashboards", - content: "A dashboard is the main view you are presented with when entering a section within the backoffice, and can be used to show valuable information to the users of the system." - }, - { - element: "[data-element='global-search-field']", - title: "Search", - content: "The search allows you to quickly find content across sections within Umbraco." - }, - { - element: "#applications [data-element='section-user']", - title: "User profile", - content: "Click on the user photo to open the user profile dialog.", - event: "click", - backdropOpacity: 0.6 - }, - { - element: "[data-element~='overlay-user']", - elementPreventClick: true, - title: "User profile", - content: "

This is where you can see details about your user, change your password and log out of Umbraco.

In the User section you will be able to do more advaned user management.

" - }, - { - element: "[data-element~='overlay-user'] [data-element='button-overlayClose']", - title: "User profile", - content: "Let's close the user profile again", - event: "click" - }, - { - element: "#applications [data-element='section-help']", - title: "Help", - content: "If you ever find yourself in trouble click here to open the help drawer.", - event: "click", - backdropOpacity: 0.6 - }, - { - element: "[data-element='drawer']", - elementPreventClick: true, - title: "Help", - content: "

In the help drawer you will find articles and videos related to the section you are using.

This is also where you will find the next tour on how to get started with Umbraco.

", - backdropOpacity: 0.6 - }, - { - element: "[data-element='drawer'] [data-element='help-tours']", - title: "Tours", - content: "To continue your journey on getting started with Umbraco, you can find more tours right here." - } - ] - }, - { - "name": "Create document type", - "alias": "umbIntroCreateDocType", - "group": "Getting Started", - "steps": [ - { - title: "Create your first Document Type", - content: "

Step 1 of any site is to create a Document Type. A Document Type is a data container where you can add data fields. The editor can then input data and Umbraco can use it to output it in the relevant parts of a template.

In this tour you will learn how to set up a basic Document Type with a data field to enter a short text.

", - type: "intro" - }, - { - element: "#applications [data-element='section-settings']", - title: "Navigate to the settings sections", - content: "In the Settings section we will find the document types.", - event: "click", - backdropOpacity: 0.6 - }, - { - element: "#tree [data-element='tree-item-documentTypes']", - title: "Create document type", - content: "

Hover the document types tree and click the three small dots to open the context menu.

", - 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: "

Click Document Type to create a new document type with a template.

We will use the template in a later tour when we need to render our content.

", - event: "click" - }, - { - element: "[data-element='editor-name-field']", - title: "Enter a name", - content: "

Our document type needs a name. Enter Home in the field and click Next.", - view: "doctypename" - }, - { - element: "[data-element='editor-description']", - title: "Enter a description", - content: "

A description helps to pick the right document type when creating content.

Write a description to our Home page. It could be:

The home to our website

" - }, - { - element: "[data-element='group-add']", - title: "Add tab", - content: "Tabs help us organize the content on a content page. Click Add new tab to add a tab.", - event: "click" - }, - { - element: "[data-element='group-name-field']", - title: "Enter a name", - content: "Enter Content in the tab name.", - view: "tabName" - }, - { - element: "[data-element='property-add']", - title: "Add a property", - content: "

Properties are the different types of data on our content page.

On our Home page we wan't to add a welcome text.

Click Add property to open the property dialog.

", - event: "click" - }, - { - element: "[data-element~='overlay-property-settings'] [data-element='property-name']", - title: "Enter a name", - content: "Enter Welcome Text as name for the property.", - view: "propertyname" - }, - { - element: "[data-element~='overlay-property-settings'] [data-element='property-description']", - title: "Enter a description", - content: "

A description will help to fill in the right content.

Enter a description for the property editor. It could be:

Write a nice introduction text so the visitors feel welcome

" - }, - { - element: "[data-element~='overlay-property-settings'] [data-element='editor-add']", - title: "Add editor", - content: "The editor defines what data type the property is. Click Add editor to open the editor picker dialog.", - event: "click" - }, - { - element: "[data-element~='overlay-editor-picker']", - elementPreventClick: true, - title: "Editor picker", - content: "

In the editor picker dialog we can pick one of the many build in editor.

" - }, - { - element: "[data-element~='overlay-editor-picker'] [data-element='editor-Textarea']", - title: "Select editor", - content: "Select the Textarea editor which allows us to enter long texts.", - event: "click" - }, - { - element: "[data-element~='overlay-editor-settings']", - elementPreventClick: true, - title: "Editor settings", - content: "Each property editor can have individual settings. We don't want to change any of these now." - }, - { - element: "[data-element~='overlay-editor-settings'] [data-element='button-overlaySubmit']", - title: "Save editor", - content: "Click Submit to save the editor.", - event: "click" - }, - { - element: "[data-element~='overlay-property-settings'] [data-element='button-overlaySubmit']", - title: "Add property to document type", - content: "Click Submit 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 Save to create and save your new document type.", - event: "click" - } - ] - }, - { - "name": "Create Content", - "alias": "umbIntroCreateContent", - "group": "Getting Started", - "steps": [ - { - title: "Creating your first content node", - content: "

The Content section contains the content of the website. Content is displayed as nodes in the content tree.

In this tour we will learn how to create our Home page for our website.

", - type: "intro" - }, - { - element: "#applications [data-element='section-content']", - title: "Navigate to the content sections", - content: "In the Content section we will find the content of our website.", - event: "click", - backdropOpacity: 0.6 - }, - { - element: "[data-element='tree-root']", - title: "Open context menu", - content: "

Open the context menu by hovering the root of the content section.

Now click the three small dots to the right.

", - event: "click", - eventElement: "[data-element='tree-root'] [data-element='tree-item-options']" - }, - { - element: "[data-element='action-create-home']", - title: "Create Home page", - content: "

Click on Home to create a new page of type Home.

", - event: "click" - }, - { - element: "[data-element='editor-content'] [data-element='editor-name-field']", - title: "Give your new page a name", - content: "

Our new page needs a name. Enter Home in the field and click Next.

", - view: "nodename" - }, - { - element: "[data-element='editor-content'] [data-element='property-welcomeText']", - title: "Add a welcome text", - content: "

Add content to the Welcome Text field

If you don't have any ideas here is a start:

I am learning Umbraco. High Five I Rock #H5IR
.

" - }, - { - element: "[data-element='editor-content'] [data-element='button-saveAndPublish']", - title: "Save and publish", - content: "

Now click the Save and publish button to save and publish your changes.

", - event: "click" - } - ] - }, - { - "name": "Render in template", - "alias": "umbIntroRenderInTemplate", - "group": "Getting Started", - "steps": [ - { - title: "Render your content in a template", - content: "

Templating in Umbraco builds on the concept of Razor Views from asp.net MVC. - This tour is a sneak peak on how to write templates in Umbraco.

In this tour we will learn how to render content from our Home document type so we can see the content added to our Home page.

", - type: "intro" - }, - { - element: "#applications [data-element='section-settings']", - title: "Navigate to the Settings section", - content: "

In the Settings section you will find all the templates

It is of course also possible to edit all your code files in your favorite code editor.

", - event: "click", - backdropOpacity: 0.6 - }, - { - element: "#tree [data-element='tree-item-templates']", - title: "Expand the Templates node", - content: "

To see all our templates click the small triangle to the left of the templates node.

", - 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']", - title: "Open Home template", - content: "

Click the Home template to open and edit it.

", - eventElement: "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home'] a.umb-tree-item__label", - event: "click" - }, - { - element: "[data-element='editor-templates'] [data-element='code-editor']", - title: "Edit template", - content: '

The template can be edited here or in your favorite code editor.

To render the field from the document type add the following to the template:

@Model.Content.GetPropertyValue("welcomeText")

' - }, - { - element: "[data-element='editor-templates'] [data-element='button-save']", - title: "Save the template", - content: "Click the Save button and your template will be saved.", - event: "click" - } - ] - }, - { - "name": "View Home page", - "alias": "umbIntroViewHomePage", - "group": "Getting Started", - "steps": [ - { - title: "View your Umbraco site", - content: "

Our three main components to a page is done: Document type, Template, and Content - it is now time to see the result.

In this tour we will learn how to see our published website.

", - type: "intro" - }, - { - element: "#applications [data-element='section-content']", - title: "Navigate to the content sections", - content: "In the Content section we 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: "

Click the Home page to open it

", - event: "click", - eventElement: "#tree [data-element='tree-item-Home'] a.umb-tree-item__label" - }, - { - element: "[data-element='editor-content'] [data-element='tab-Generic properties']", - title: "Properties", - content: "

Under the properties tab you will find the default information about a content item.

", - event: "click" - }, - { - element: "[data-element='editor-content'] [data-element='property-_umb_urls']", - title: "Open page", - content: "

Click the Link to document to view your page.

Tip: Click the preview button in the bottom right corner to preview changes without publishing them.

", - event: "click", - eventElement: "[data-element='editor-content'] [data-element='property-_umb_urls'] a[target='_blank']" - } - ] - }, - { - "name": "The media library", - "alias": "umbIntroMediaSection", - "group": "Getting Started", - "steps": [ - { - title: "How to use the media library", - content: "

A website would be boring without media content. In Umbraco you can manage all your images, documents, videos etc. in the Media section. Here you can upload and organise your media items and see details about each item.

In this tour we will learn how to upload and orginise your Media library in Umbraco. It will also show you how to view details about a specific media item.

", - type: "intro" - }, - { - element: "#applications [data-element='section-media']", - title: "Navigate to the media section", - content: "The media section is where you will manage all your media items.", - event: "click", - backdropOpacity: 0.6 - }, - { - element: "#tree [data-element='tree-root']", - title: "Create a new folder", - content: "

Let's first create a folder for our images. Hover the media root and click the three small dots on the right side of the item.

", - 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: "

Select the Folder options to select the type folder.

", - event: "click" - }, - { - element: "[data-element='editor-media'] [data-element='editor-name-field']", - title: "Enter a name", - content: "

Enter My folder in the field.

", - view: "foldername" - }, - { - element: "[data-element='editor-media'] [data-element='button-save']", - title: "Save the folder", - content: "

Click the Save button to create the new folder

", - event: "click" - }, - { - element: "[data-element='editor-media'] [data-element='dropzone']", - title: "Upload images", - content: "

In the upload area you can upload your media items.

Click the Upload button and select a couple of images on your computer and upload them.

", - view: "uploadimages" - }, - { - element: "[data-element='editor-media'] [data-element='media-grid-item-0']", - title: "View media item details", - content: "Hover the media item and Click the purple bar 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: "

Here you can see the image you have uploaded.

You can use the dot in the center of the image to set a focal point on the image.

" - }, - { - element: "[data-element='editor-media'] [data-element='property-umbracoBytes']", - title: "Image size", - content: "

You will also find other details about the image, like the size.

You can add extra properties to an image by creating or editing the Media types

" - }, - { - element: "[data-element='editor-media'] [data-element='tab-Generic properties']", - title: "Properties", - content: "Like the content section you can also find default properties about the media item. You will find these under the properties tab.", - event: "click" - }, - { - element: "[data-element='editor-media'] [data-element='property-_umb_urls']", - title: "Link to media", - content: "The path to the media item..." - }, - { - element: "[data-element='editor-media'] [data-element='property-_umb_updatedate']", - title: "Last edited", - content: "...and information about when the media item has been created and edited." - } - ] - } - ]; + /** + * Method to return all of the tours as a new instance + */ + function getTours() { + var tours = [ + { + "name": "Introduction", + "alias": "umbIntroIntroduction", + "group": "Getting Started", + "allowDisable": true, + "steps": [ + { + title: "Welcome to Umbraco - The Friendly CMS", + content: "

Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible.

In this quick tour we will introduce you to the main areas of Umbraco and show you how to best get started.

", + type: "intro" + }, + { + element: "#applications", + elementPreventClick: true, + title: "Sections", + content: "These are the Sections and allows you to navigate the different areas of Umbraco.", + backdropOpacity: 0.6 + }, + + { + element: "#tree", + elementPreventClick: true, + title: "The Tree", + content: "This is the Tree and will contain all the content of your website." + }, + { + element: "[data-element='editor-content']", + elementPreventClick: true, + title: "Dashboards", + content: "A dashboard is the main view you are presented with when entering a section within the backoffice, and can be used to show valuable information to the users of the system." + }, + { + element: "[data-element='global-search-field']", + title: "Search", + content: "The search allows you to quickly find content across sections within Umbraco." + }, + { + element: "#applications [data-element='section-user']", + title: "User profile", + content: "Click on the user photo to open the user profile dialog.", + event: "click", + backdropOpacity: 0.6 + }, + { + element: "[data-element~='overlay-user']", + elementPreventClick: true, + title: "User profile", + content: "

This is where you can see details about your user, change your password and log out of Umbraco.

In the User section you will be able to do more advaned user management.

" + }, + { + element: "[data-element~='overlay-user'] [data-element='button-overlayClose']", + title: "User profile", + content: "Let's close the user profile again", + event: "click" + }, + { + element: "#applications [data-element='section-help']", + title: "Help", + content: "If you ever find yourself in trouble click here to open the help drawer.", + event: "click", + backdropOpacity: 0.6 + }, + { + element: "[data-element='drawer']", + elementPreventClick: true, + title: "Help", + content: "

In the help drawer you will find articles and videos related to the section you are using.

This is also where you will find the next tour on how to get started with Umbraco.

", + backdropOpacity: 0.6 + }, + { + element: "[data-element='drawer'] [data-element='help-tours']", + title: "Tours", + content: "To continue your journey on getting started with Umbraco, you can find more tours right here." + } + ] + }, + { + "name": "Create document type", + "alias": "umbIntroCreateDocType", + "group": "Getting Started", + "steps": [ + { + title: "Create your first Document Type", + content: "

Step 1 of any site is to create a Document Type. A Document Type is a data container where you can add data fields. The editor can then input data and Umbraco can use it to output it in the relevant parts of a template.

In this tour you will learn how to set up a basic Document Type with a data field to enter a short text.

", + type: "intro" + }, + { + element: "#applications [data-element='section-settings']", + title: "Navigate to the settings sections", + content: "In the Settings section we will find the document types.", + event: "click", + backdropOpacity: 0.6 + }, + { + element: "#tree [data-element='tree-item-documentTypes']", + title: "Create document type", + content: "

Hover the document types tree and click the three small dots to open the context menu.

", + 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: "

Click Document Type to create a new document type with a template.

We will use the template in a later tour when we need to render our content.

", + event: "click" + }, + { + element: "[data-element='editor-name-field']", + title: "Enter a name", + content: "

Our document type needs a name. Enter Home in the field and click Next.", + view: "doctypename" + }, + { + element: "[data-element='editor-description']", + title: "Enter a description", + content: "

A description helps to pick the right document type when creating content.

Write a description to our Home page. It could be:

The home to our website

" + }, + { + element: "[data-element='group-add']", + title: "Add tab", + content: "Tabs help us organize the content on a content page. Click Add new tab to add a tab.", + event: "click" + }, + { + element: "[data-element='group-name-field']", + title: "Enter a name", + content: "Enter Content in the tab name.", + view: "tabName" + }, + { + element: "[data-element='property-add']", + title: "Add a property", + content: "

Properties are the different types of data on our content page.

On our Home page we wan't to add a welcome text.

Click Add property to open the property dialog.

", + event: "click" + }, + { + element: "[data-element~='overlay-property-settings'] [data-element='property-name']", + title: "Enter a name", + content: "Enter Welcome Text as name for the property.", + view: "propertyname" + }, + { + element: "[data-element~='overlay-property-settings'] [data-element='property-description']", + title: "Enter a description", + content: "

A description will help to fill in the right content.

Enter a description for the property editor. It could be:

Write a nice introduction text so the visitors feel welcome

" + }, + { + element: "[data-element~='overlay-property-settings'] [data-element='editor-add']", + title: "Add editor", + content: "The editor defines what data type the property is. Click Add editor to open the editor picker dialog.", + event: "click" + }, + { + element: "[data-element~='overlay-editor-picker']", + elementPreventClick: true, + title: "Editor picker", + content: "

In the editor picker dialog we can pick one of the many build in editor.

" + }, + { + element: "[data-element~='overlay-editor-picker'] [data-element='editor-Textarea']", + title: "Select editor", + content: "Select the Textarea editor which allows us to enter long texts.", + event: "click" + }, + { + element: "[data-element~='overlay-editor-settings']", + elementPreventClick: true, + title: "Editor settings", + content: "Each property editor can have individual settings. We don't want to change any of these now." + }, + { + element: "[data-element~='overlay-editor-settings'] [data-element='button-overlaySubmit']", + title: "Save editor", + content: "Click Submit to save the editor.", + event: "click" + }, + { + element: "[data-element~='overlay-property-settings'] [data-element='button-overlaySubmit']", + title: "Add property to document type", + content: "Click Submit 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 Save to create and save your new document type.", + event: "click" + } + ] + }, + { + "name": "Create Content", + "alias": "umbIntroCreateContent", + "group": "Getting Started", + "steps": [ + { + title: "Creating your first content node", + content: "

The Content section contains the content of the website. Content is displayed as nodes in the content tree.

In this tour we will learn how to create our Home page for our website.

", + type: "intro" + }, + { + element: "#applications [data-element='section-content']", + title: "Navigate to the content sections", + content: "In the Content section we will find the content of our website.", + event: "click", + backdropOpacity: 0.6 + }, + { + element: "[data-element='tree-root']", + title: "Open context menu", + content: "

Open the context menu by hovering the root of the content section.

Now click the three small dots to the right.

", + event: "click", + eventElement: "[data-element='tree-root'] [data-element='tree-item-options']" + }, + { + element: "[data-element='action-create-home']", + title: "Create Home page", + content: "

Click on Home to create a new page of type Home.

", + event: "click" + }, + { + element: "[data-element='editor-content'] [data-element='editor-name-field']", + title: "Give your new page a name", + content: "

Our new page needs a name. Enter Home in the field and click Next.

", + view: "nodename" + }, + { + element: "[data-element='editor-content'] [data-element='property-welcomeText']", + title: "Add a welcome text", + content: "

Add content to the Welcome Text field

If you don't have any ideas here is a start:

I am learning Umbraco. High Five I Rock #H5IR
.

" + }, + { + element: "[data-element='editor-content'] [data-element='button-saveAndPublish']", + title: "Save and publish", + content: "

Now click the Save and publish button to save and publish your changes.

", + event: "click" + } + ] + }, + { + "name": "Render in template", + "alias": "umbIntroRenderInTemplate", + "group": "Getting Started", + "steps": [ + { + title: "Render your content in a template", + content: "

Templating in Umbraco builds on the concept of Razor Views from asp.net MVC. - This tour is a sneak peak on how to write templates in Umbraco.

In this tour we will learn how to render content from our Home document type so we can see the content added to our Home page.

", + type: "intro" + }, + { + element: "#applications [data-element='section-settings']", + title: "Navigate to the Settings section", + content: "

In the Settings section you will find all the templates

It is of course also possible to edit all your code files in your favorite code editor.

", + event: "click", + backdropOpacity: 0.6 + }, + { + element: "#tree [data-element='tree-item-templates']", + title: "Expand the Templates node", + content: "

To see all our templates click the small triangle to the left of the templates node.

", + 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']", + title: "Open Home template", + content: "

Click the Home template to open and edit it.

", + eventElement: "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home'] a.umb-tree-item__label", + event: "click" + }, + { + element: "[data-element='editor-templates'] [data-element='code-editor']", + title: "Edit template", + content: '

The template can be edited here or in your favorite code editor.

To render the field from the document type add the following to the template:

@Model.Content.GetPropertyValue("welcomeText")

' + }, + { + element: "[data-element='editor-templates'] [data-element='button-save']", + title: "Save the template", + content: "Click the Save button and your template will be saved.", + event: "click" + } + ] + }, + { + "name": "View Home page", + "alias": "umbIntroViewHomePage", + "group": "Getting Started", + "steps": [ + { + title: "View your Umbraco site", + content: "

Our three main components to a page is done: Document type, Template, and Content - it is now time to see the result.

In this tour we will learn how to see our published website.

", + type: "intro" + }, + { + element: "#applications [data-element='section-content']", + title: "Navigate to the content sections", + content: "In the Content section we 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: "

Click the Home page to open it

", + event: "click", + eventElement: "#tree [data-element='tree-item-Home'] a.umb-tree-item__label" + }, + { + element: "[data-element='editor-content'] [data-element='tab-Generic properties']", + title: "Properties", + content: "

Under the properties tab you will find the default information about a content item.

", + event: "click" + }, + { + element: "[data-element='editor-content'] [data-element='property-_umb_urls']", + title: "Open page", + content: "

Click the Link to document to view your page.

Tip: Click the preview button in the bottom right corner to preview changes without publishing them.

", + event: "click", + eventElement: "[data-element='editor-content'] [data-element='property-_umb_urls'] a[target='_blank']" + } + ] + }, + { + "name": "The media library", + "alias": "umbIntroMediaSection", + "group": "Getting Started", + "steps": [ + { + title: "How to use the media library", + content: "

A website would be boring without media content. In Umbraco you can manage all your images, documents, videos etc. in the Media section. Here you can upload and organise your media items and see details about each item.

In this tour we will learn how to upload and orginise your Media library in Umbraco. It will also show you how to view details about a specific media item.

", + type: "intro" + }, + { + element: "#applications [data-element='section-media']", + title: "Navigate to the media section", + content: "The media section is where you will manage all your media items.", + event: "click", + backdropOpacity: 0.6 + }, + { + element: "#tree [data-element='tree-root']", + title: "Create a new folder", + content: "

Let's first create a folder for our images. Hover the media root and click the three small dots on the right side of the item.

", + 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: "

Select the Folder options to select the type folder.

", + event: "click" + }, + { + element: "[data-element='editor-media'] [data-element='editor-name-field']", + title: "Enter a name", + content: "

Enter My folder in the field.

", + view: "foldername" + }, + { + element: "[data-element='editor-media'] [data-element='button-save']", + title: "Save the folder", + content: "

Click the Save button to create the new folder

", + event: "click" + }, + { + element: "[data-element='editor-media'] [data-element='dropzone']", + title: "Upload images", + content: "

In the upload area you can upload your media items.

Click the Upload button and select a couple of images on your computer and upload them.

", + view: "uploadimages" + }, + { + element: "[data-element='editor-media'] [data-element='media-grid-item-0']", + title: "View media item details", + content: "Hover the media item and Click the purple bar 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: "

Here you can see the image you have uploaded.

You can use the dot in the center of the image to set a focal point on the image.

" + }, + { + element: "[data-element='editor-media'] [data-element='property-umbracoBytes']", + title: "Image size", + content: "

You will also find other details about the image, like the size.

You can add extra properties to an image by creating or editing the Media types

" + }, + { + element: "[data-element='editor-media'] [data-element='tab-Generic properties']", + title: "Properties", + content: "Like the content section you can also find default properties about the media item. You will find these under the properties tab.", + event: "click" + }, + { + element: "[data-element='editor-media'] [data-element='property-_umb_urls']", + title: "Link to media", + content: "The path to the media item..." + }, + { + element: "[data-element='editor-media'] [data-element='property-_umb_updatedate']", + title: "Last edited", + content: "...and information about when the media item has been created and edited." + } + ] + } + ]; + return tours; + } function startTour(tour) { eventsService.emit("appState.tour.start", tour); @@ -425,74 +430,140 @@ currentTour = null; } + /** + * Disables a tour for the user, raises an event and returns a promise + * @param {any} tour + */ function disableTour(tour) { + var deferred = $q.defer(); tour.disabled = true; - saveInLocalStorage(tour); - eventsService.emit("appState.tour.end", tour); - currentTour = null; + currentUserResource + .saveTourStatus({ alias: tour.alias, disabled: tour.disabled, completed: tour.completed }).then( + function() { + eventsService.emit("appState.tour.end", tour); + currentTour = null; + deferred.resolve(tour); + }); + return deferred.promise; } + /** + * Completes a tour for the user, raises an event and returns a promise + * @param {any} tour + */ function completeTour(tour) { + var deferred = $q.defer(); tour.completed = true; - saveInLocalStorage(tour); - eventsService.emit("appState.tour.complete", tour); - currentTour = null; + currentUserResource + .saveTourStatus({ alias: tour.alias, disabled: tour.disabled, completed: tour.completed }).then( + function() { + eventsService.emit("appState.tour.complete", tour); + currentTour = null; + deferred.resolve(tour); + }); + return deferred.promise; } + /** + * Returns the current tour + */ function getCurrentTour() { + //TODO: This should be reset if a new user logs in return currentTour; } - + + /** + * Returns a promise of all tours with the current user statuses + */ function getAllTours() { - setCompletedTours(); - return tours; + var tours = getTours(); + return setTourStatuses(tours); } + /** + * Returns a promise of grouped tours with the current user statuses + */ function getGroupedTours() { - setCompletedTours(); - var groupedTours = _.groupBy(tours, "group"); - return groupedTours; + var deferred = $q.defer(); + var tours = getTours(); + setTourStatuses(tours).then(function() { + var groupedTours = _.groupBy(tours, "group"); + deferred.resolve(groupedTours); + }); + return deferred.promise; } + /** + * Returns a promise of the tour found by alias with the current user statuses + * @param {any} tourAlias + */ function getTourByAlias(tourAlias) { - var tour = _.findWhere(tours, {alias: tourAlias}); - return tour; + var deferred = $q.defer(); + var tours = getTours(); + setTourStatuses(tours).then(function () { + var tour = _.findWhere(tours, { alias: tourAlias }); + deferred.resolve(tour); + }); + return deferred.promise; } + /** + * Returns a promise of completed tours for the user + */ function getCompletedTours() { - var storedTours = localStorageService.get(localStorageKey); - var completedTours = _.where(storedTours, {completed: true}); - var aliases = _.pluck(completedTours, "alias"); - return aliases; + var deferred = $q.defer(); + currentUserResource.getTours().then(function (storedTours) { + var completedTours = _.where(storedTours, { completed: true }); + var aliases = _.pluck(completedTours, "alias"); + deferred.resolve(aliases); + }); + return deferred.promise; } + /** + * Returns a promise of disabled tours for the user + */ function getDisabledTours() { - var storedTours = localStorageService.get(localStorageKey); - var disabledTours = _.where(storedTours, {disabled: true}); - var aliases = _.pluck(disabledTours, "alias"); - return aliases; + var deferred = $q.defer(); + currentUserResource.getTours().then(function (storedTours) { + var disabledTours = _.where(storedTours, { disabled: true }); + var aliases = _.pluck(disabledTours, "alias"); + deferred.resolve(aliases); + }); + return deferred.promise; } /////////// - function setCompletedTours() { + /** + * Based on the tours given, this will set each of the tour statuses (disabled/completed) based on what is stored against the current user + * @param {any} tours + */ + function setTourStatuses(tours) { - var storedTours = []; + var deferred = $q.defer(); + currentUserResource.getTours().then(function (storedTours) { - if (localStorageService.get(localStorageKey)) { - storedTours = localStorageService.get(localStorageKey); - } + angular.forEach(storedTours, function (storedTour) { + if (storedTour.completed === true) { + angular.forEach(tours, function (tour) { + if (storedTour.alias === tour.alias) { + tour.completed = true; + } + }); + } + if (storedTour.disabled === true) { + angular.forEach(tours, function (tour) { + if (storedTour.alias === tour.alias) { + tour.disabled = true; + } + }); + } + }); - angular.forEach(storedTours, function (storedTour) { - if (storedTour.completed === true) { - angular.forEach(tours, function (tour) { - if (storedTour.alias === tour.alias) { - tour.completed = true; - } - }); - } + deferred.resolve(tours); }); - + return deferred.promise; } function saveInLocalStorage(tour) { @@ -535,10 +606,13 @@ disableTour: disableTour, completeTour: completeTour, getCurrentTour: getCurrentTour, + //TODO: Not used getAllTours: getAllTours, getGroupedTours: getGroupedTours, getTourByAlias: getTourByAlias, + //TODO: Not used getCompletedTours: getCompletedTours, + //TODO: Not used getDisabledTours: getDisabledTours, }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js index b4773cdaad..286100f830 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js @@ -26,7 +26,9 @@ function oninit() { - vm.tours = tourService.getGroupedTours(); + tourService.getGroupedTours().then(function(groupedTours) { + vm.tours = groupedTours; + }); // load custom help dashboard dashboardResource.getDashboard("user-help").then(function (dashboard) { @@ -156,9 +158,11 @@ } evts.push(eventsService.on("appState.tour.complete", function (event, tour) { - vm.tours = tourService.getGroupedTours(); - openTourGroup(tour.alias); - getTourGroupCompletedPercentage(); + tourService.getGroupedTours().then(function(groupedTours) { + vm.tours = groupedTours; + openTourGroup(tour.alias); + getTourGroupCompletedPercentage(); + }); })); $scope.$on('$destroy', function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js index 67a69fef2c..f187ec15cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js @@ -30,20 +30,17 @@ function startUpDynamicContentController($timeout, dashboardResource, assetsServ function onInit() { // load tours - vm.tours = tourService.getGroupedTours(); + tourService.getGroupedTours().then(function(groupedTours) { + vm.tours = groupedTours; + }); - // get list of completed tours and disabled tours - var completedTours = tourService.getCompletedTours(); - var disabledTours = tourService.getDisabledTours(); - // get intro tour - var introTour = tourService.getTourByAlias("umbIntroIntroduction"); - - // start intro tour if it hasn't been completed or disabled - if(completedTours.indexOf(introTour.alias) === -1 && disabledTours.indexOf(introTour.alias) === -1) { - tourService.startTour(introTour); - } - + tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) { + // start intro tour if it hasn't been completed or disabled + if (introTour.disabled !== true && introTour.completed !== true) { + tourService.startTour(introTour); + } + }); } function startTour(tour) { diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 3d8ec01937..8a348fd23e 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; using System.Net; using System.Text; using System.Threading.Tasks; @@ -17,6 +19,9 @@ using umbraco; using legacyUser = umbraco.BusinessLogic.User; using System.Net.Http; using System.Collections.Specialized; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Security; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -30,6 +35,48 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class CurrentUserController : UmbracoAuthorizedJsonController { + /// + /// Saves a tour status for the current user + /// + /// + /// + public UserTours PostSetUserTour(UserTourStatus status) + { + if (status == null) throw new ArgumentNullException("status"); + + if (Security.CurrentUser.TourData.IsNullOrWhiteSpace()) + { + var userTours = new UserTours(Security.CurrentUser.Id, new[] {status}); + Security.CurrentUser.TourData = JsonConvert.SerializeObject(userTours.Tours); + Services.UserService.Save(Security.CurrentUser); + return userTours; + } + + var userTourStatuses = JsonConvert.DeserializeObject>(Security.CurrentUser.TourData).ToList(); + var found = userTourStatuses.FirstOrDefault(x => x.Alias == status.Alias); + if (found != null) + { + //remove it and we'll replace it next + userTourStatuses.Remove(found); + } + userTourStatuses.Add(status); + Security.CurrentUser.TourData = JsonConvert.SerializeObject(userTourStatuses); + Services.UserService.Save(Security.CurrentUser); + return new UserTours(Security.CurrentUser.Id, userTourStatuses); + } + + /// + /// Returns the user's tours + /// + /// + public UserTours GetUserTours() + { + if (Security.CurrentUser.TourData.IsNullOrWhiteSpace()) + return new UserTours(Security.CurrentUser.Id, Enumerable.Empty()); + + var userTours = JsonConvert.DeserializeObject>(Security.CurrentUser.TourData); + return new UserTours(Security.CurrentUser.Id, userTours); + } /// /// When a user is invited and they click on the invitation link, they will be partially logged in diff --git a/src/Umbraco.Web/Models/UserTourStatus.cs b/src/Umbraco.Web/Models/UserTourStatus.cs new file mode 100644 index 0000000000..d1834f3d6b --- /dev/null +++ b/src/Umbraco.Web/Models/UserTourStatus.cs @@ -0,0 +1,60 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + /// + /// A model representing the tours a user has taken/completed + /// + [DataContract(Name = "userTourStatus", Namespace = "")] + public class UserTourStatus : IEquatable + { + /// + /// The tour alias + /// + [DataMember(Name = "alias")] + public string Alias { get; set; } + + /// + /// If the tour is completed + /// + [DataMember(Name = "completed")] + public bool Completed { get; set; } + + /// + /// If the tour is disabled + /// + [DataMember(Name = "disabled")] + public bool Disabled { get; set; } + + public bool Equals(UserTourStatus other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Alias, other.Alias); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((UserTourStatus) obj); + } + + public override int GetHashCode() + { + return Alias.GetHashCode(); + } + + public static bool operator ==(UserTourStatus left, UserTourStatus right) + { + return Equals(left, right); + } + + public static bool operator !=(UserTourStatus left, UserTourStatus right) + { + return !Equals(left, right); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/UserTours.cs b/src/Umbraco.Web/Models/UserTours.cs new file mode 100644 index 0000000000..7ae3034137 --- /dev/null +++ b/src/Umbraco.Web/Models/UserTours.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + [DataContract(Name = "userTours", Namespace = "")] + public class UserTours + { + public UserTours(int userId, IEnumerable tours) + { + UserId = userId; + Tours = tours; + } + + [DataMember(Name = "userId")] + public int UserId { get; private set; } + + [DataMember(Name = "tours")] + public IEnumerable Tours { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 417b6ac184..09355f648d 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -427,6 +427,8 @@ + +