-
-
-
-
-
-
@@ -410,33 +31,30 @@
-
+
-
+
-
+
@@ -447,25 +65,22 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html
new file mode 100644
index 0000000000..4f03b755f5
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html
@@ -0,0 +1,361 @@
+
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json
index 1e75e21cc0..1617bba0cb 100644
--- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json
+++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json
@@ -3,6 +3,7 @@
"name": "Introduction",
"alias": "umbIntroIntroduction",
"group": "Getting Started",
+ "groupOrder": 100,
"allowDisable": true,
"steps": [
{
@@ -86,113 +87,114 @@
"name": "Create document type",
"alias": "umbIntroCreateDocType",
"group": "Getting Started",
+ "groupOrder": 100,
"steps": [
{
- title: "Create your first Document Type",
- content: "Step 1 of any site is to create a Document Type.
A Document Type is a template for content. For each type of content you want to create you'll create a Document Type. This will define were content based on this Document Type can be created, how many properties it holds and what the input method should be for these properties.
When you have at least one Document type in place you can start creating content and this content can the be used in a template.
In this tour you will learn how to set up a basic Document Type with a property to enter a short text.
",
- type: "intro"
+ "title": "Create your first Document Type",
+ "content": "Step 1 of any site is to create a Document Type.
A Document Type is a template for content. For each type of content you want to create you'll create a Document Type. This will define were content based on this Document Type can be created, how many properties it holds and what the input method should be for these properties.
When you have at least one Document type in place you can start creating content and this content can the be used in a template.
In this tour you will learn how to set up a basic Document Type with a property to enter a short text.
",
+ "type": "intro"
},
{
- element: "#applications [data-element='section-settings']",
- title: "Navigate to the Settings sections",
- content: "In the Settings section you can create and manage Document types.",
- event: "click",
- backdropOpacity: 0.6
+ "element": "#applications [data-element='section-settings']",
+ "title": "Navigate to the Settings sections",
+ "content": "In the Settings section you can create and manage Document types.",
+ "event": "click",
+ "backdropOpacity": 0.6
},
{
- element: "#tree [data-element='tree-item-documentTypes']",
- title: "Create Document Type",
- content: "Hover the Document Type 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": "#tree [data-element='tree-item-documentTypes']",
+ "title": "Create Document Type",
+ "content": "Hover the Document Type 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. The template will be automatically created and set as the default template for this Document Type
You will use the template in a later tour render content.
",
- event: "click"
+ "element": "#dialog [data-element='action-documentType']",
+ "title": "Create Document Type",
+ "content": "Click Document Type to create a new document type with a template. The template will be automatically created and set as the default template for this Document Type
You will use the template in a later tour render content.
",
+ "event": "click"
},
{
- element: "[data-element='editor-name-field']",
- title: "Enter a name",
- content: "Your Document Type needs a name. Enter My Home Page in the field and click Next.",
- view: "doctypename"
+ "element": "[data-element='editor-name-field']",
+ "title": "Enter a name",
+ "content": "
Your Document Type needs a name. Enter My Home Page 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 page of the website
"
+ "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 page of the website
"
},
{
- element: "[data-element='group-add']",
- title: "Add tab",
- content: "Tabs are used to organize properties on content in the Content section. Click Add new tab to add a tab.",
- event: "click"
+ "element": "[data-element='group-add']",
+ "title": "Add tab",
+ "content": "Tabs are used to organize properties on content in the Content section. Click Add new tab to add a tab.",
+ "event": "click"
},
{
- element: "[data-element='group-name-field']",
- title: "Name the tab",
- content: "Enter Home in the tab name.
You can name a tab anything you want and if you have a lot of properties it can be useful to add multiple tabs.
",
- view: "tabName"
+ "element": "[data-element='group-name-field']",
+ "title": "Name the tab",
+ "content": "Enter Home in the tab name.
You can name a tab anything you want and if you have a lot of properties it can be useful to add multiple tabs.
",
+ "view": "tabName"
},
{
- element: "[data-element='property-add']",
- title: "Add a property",
- content: "Properties are the different input fields on a 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='property-add']",
+ "title": "Add a property",
+ "content": "Properties are the different input fields on a 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: "Name the property",
- content: "Enter Welcome Text as the name for the property.",
- view: "propertyname"
+ "element": "[data-element~='overlay-property-settings'] [data-element='property-name']",
+ "title": "Name the property",
+ "content": "Enter Welcome Text as the 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='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: "When you add an editor you choose what the input method for this property will be. Click Add editor to open the editor picker dialog.",
- event: "click"
+ "element": "[data-element~='overlay-property-settings'] [data-element='editor-add']",
+ "title": "Add editor",
+ "content": "When you add an editor you choose what the input method for this property will be. Click 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.
You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors)
"
+ "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.
You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors)
"
},
{
- element: "[data-element~='overlay-editor-picker'] [data-element='editor-Textarea']",
- title: "Select editor",
- content: "Select the Textarea editor. This will add a textarea to the Welcome Text property.",
- event: "click"
+ "element": "[data-element~='overlay-editor-picker'] [data-element='editor-Textarea']",
+ "title": "Select editor",
+ "content": "Select the Textarea editor. This will add a textarea to the Welcome Text property.",
+ "event": "click"
},
{
- element: "[data-element~='overlay-editor-settings']",
- elementPreventClick: true,
- title: "Editor settings",
- content: "Each property editor can have individual settings. For the textarea editor you can set a charachter limit but in this case it is not needed"
+ "element": "[data-element~='overlay-editor-settings']",
+ "elementPreventClick": true,
+ "title": "Editor settings",
+ "content": "Each property editor can have individual settings. For the textarea editor you can set a charachter limit but in this case it is not needed"
},
{
- element: "[data-element~='overlay-editor-settings'] [data-element='button-overlaySubmit']",
- title: "Save editor",
- content: "Click Submit to save the editor.",
- event: "click"
+ "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~='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"
+ "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"
}
]
},
@@ -200,48 +202,49 @@
"name": "Create Content",
"alias": "umbIntroCreateContent",
"group": "Getting Started",
+ "groupOrder": 100,
"steps": [
{
- title: "Creating your first content node",
- content: "In this tour you will learn how to create the home page for your website. It will use the Home Page Document type you created in the previous tour.
",
- type: "intro"
+ "title": "Creating your first content node",
+ "content": "In this tour you will learn how to create the home page for your website. It will use the Home Page Document type you created in the previous tour.
",
+ "type": "intro"
},
{
- element: "#applications [data-element='section-content']",
- title: "Navigate to the Content section",
- content: "In the Content section you can create and manage the content of the website.
The Content section contains the content of your website. Content is displayed as nodes in the content tree.
",
- event: "click",
- backdropOpacity: 0.6
+ "element": "#applications [data-element='section-content']",
+ "title": "Navigate to the Content section",
+ "content": "In the Content section you can create and manage the content of the website.
The Content section contains the content of your website. Content is displayed as nodes in the content tree.
",
+ "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='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-homePage']",
- title: "Create Home page",
- content: "The context menu shows you all the actions that are available on a node
Click on Home Page to create a new page of type Home Page.
",
- event: "click"
+ "element": "[data-element='action-create-homePage']",
+ "title": "Create Home page",
+ "content": "The context menu shows you all the actions that are available on a node
Click on Home Page to create a new page of type Home Page.
",
+ "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='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='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"
+ "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"
}
]
},
@@ -249,44 +252,45 @@
"name": "Render in template",
"alias": "umbIntroRenderInTemplate",
"group": "Getting Started",
+ "groupOrder": 100,
"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 you will learn how to render content from the Home Page document type so you can see the content added to our Home content page.
",
- type: "intro"
+ "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 you will learn how to render content from the Home Page document type so you can see the content added to our Home content 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": "#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']",
+ "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 Page']",
- title: "Open Home template",
- content: "Click the Home Page template to open and edit it.
",
- eventElement: "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page'] a.umb-tree-item__label",
- event: "click"
+ "element": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']",
+ "title": "Open Home template",
+ "content": "Click the Home Page template to open and edit it.
",
+ "eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page'] a.umb-tree-item__label",
+ "event": "click"
},
{
- element: "[data-element='editor-templates'] [data-element='code-editor']",
- title: "Edit template",
- content: '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:
<h1>@Model.Content.Name</h1>
<p>@Model.Content.WelcomeText</p>
'
+ "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:
<h1>@Model.Content.Name</h1>
<p>@Model.Content.WelcomeText</p>
"
},
{
- 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"
+ "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"
}
]
},
@@ -294,38 +298,39 @@
"name": "View Home page",
"alias": "umbIntroViewHomePage",
"group": "Getting Started",
+ "groupOrder": 100,
"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 you will learn how to see your published website.
",
- type: "intro"
+ "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 you will learn how to see your published website.
",
+ "type": "intro"
},
{
- element: "#applications [data-element='section-content']",
- title: "Navigate to the content sections",
- content: "In the Content section you will find the content of our website.",
- event: "click",
- backdropOpacity: 0.6
+ "element": "#applications [data-element='section-content']",
+ "title": "Navigate to the content sections",
+ "content": "In the Content section you will find the content of our website.",
+ "event": "click",
+ "backdropOpacity": 0.6
},
{
- element: "#tree [data-element='tree-item-Home']",
- title: "Open the Home page",
- content: "Click the Home page to open it
",
- event: "click",
- eventElement: "#tree [data-element='tree-item-Home'] a.umb-tree-item__label"
+ "element": "#tree [data-element='tree-item-Home']",
+ "title": "Open the Home page",
+ "content": "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-_umb_infoTab']",
- title: "Info",
- content: "Under the info tab you will find the default information about a content item.
",
- event: "click"
+ "element": "[data-element='editor-content'] [data-element='tab-_umb_infoTab']",
+ "title": "Info",
+ "content": "Under the info tab you will find the default information about a content item.
",
+ "event": "click"
},
{
- element: "[data-element='editor-content'] [data-element='node-info-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='node-info-urls'] a[target='_blank']"
+ "element": "[data-element='editor-content'] [data-element='node-info-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='node-info-urls'] a[target='_blank']"
}
]
},
@@ -333,86 +338,87 @@
"name": "The Media library",
"alias": "umbIntroMediaSection",
"group": "Getting Started",
+ "groupOrder": 100,
"steps": [
{
- title: "How to use the media library",
- content: "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 you will learn how to upload and organise your Media library in Umbraco. It will also show you how to view details about a specific media item.
",
- type: "intro"
+ "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 you will learn how to upload and organise your Media library in Umbraco. It will also show you how to view details about a specific media item.
",
+ "type": "intro"
},
{
- element: "#applications [data-element='section-media']",
- title: "Navigate to the Media section",
- content: "The media section is where you manage all your media items.",
- event: "click",
- backdropOpacity: 0.6
+ "element": "#applications [data-element='section-media']",
+ "title": "Navigate to the Media section",
+ "content": "The media section is where you manage all your media items.",
+ "event": "click",
+ "backdropOpacity": 0.6
},
{
- element: "#tree [data-element='tree-root']",
- title: "Create a new folder",
- content: "First create a folder for your images. Hover the media root node 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": "#tree [data-element='tree-root']",
+ "title": "Create a new folder",
+ "content": "First create a folder for your images. Hover the media root node 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 option to select the type folder.
",
- event: "click"
+ "element": "#dialog [data-element='action-Folder']",
+ "title": "Create a new folder",
+ "content": "Select the Folder option to select the type folder.
",
+ "event": "click"
},
{
- element: "[data-element='editor-media'] [data-element='editor-name-field']",
- title: "Enter a name",
- content: "Enter My Images in the field.
"
+ "element": "[data-element='editor-media'] [data-element='editor-name-field']",
+ "title": "Enter a name",
+ "content": "Enter My Images in the field.
"
},
{
- 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='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 Click here to choose files-button and select a couple of images on your computer and upload them.
",
- view: "uploadimages"
+ "element": "[data-element='editor-media'] [data-element='dropzone']",
+ "title": "Upload images",
+ "content": "In the upload area you can upload your media items.
Click the Click here to choose files-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='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.
"
+ "element": "[data-element='editor-media'] [data-element='property-umbracoFile']",
+ "title": "The uploaded image",
+ "content": "Here you can see the image you have uploaded.
"
},
{
- 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.
Media items work in much the same way as content. So you can add extra properties to an image by creating or editing the Media types in the Settings section.
"
+ "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.
Media items work in much the same way as content. So you can add extra properties to an image by creating or editing the Media types in the Settings section.
"
},
{
- element: "[data-element='editor-media'] [data-element='tab-_umb_infoTab']",
- title: "Info",
- content: "Like the content section you can also find default information about the media item. You will find these under the info tab.",
- event: "click"
+ "element": "[data-element='editor-media'] [data-element='tab-_umb_infoTab']",
+ "title": "Info",
+ "content": "Like the content section you can also find default information about the media item. You will find these under the info tab.",
+ "event": "click"
},
{
- element: "[data-element='editor-media'] [data-element='node-info-urls']",
- title: "Link to media",
- content: "The path to the media item..."
+ "element": "[data-element='editor-media'] [data-element='node-info-urls']",
+ "title": "Link to media",
+ "content": "The path to the media item..."
},
{
- element: "[data-element='editor-media'] [data-element='node-info-update-date']",
- title: "Last edited",
- content: "...and information about when the media item has been created and edited."
+ "element": "[data-element='editor-media'] [data-element='node-info-update-date']",
+ "title": "Last edited",
+ "content": "...and information about when the media item has been created and edited."
},
{
- element: "[data-element='editor-container']",
- title: "Using media items",
- content: "You can reference a media item directly in a template by using the path or try adding a Media Picker to a document type property so you can select media items from the content section."
+ "element": "[data-element='editor-container']",
+ "title": "Using media items",
+ "content": "You can reference a media item directly in a template by using the path or try adding a Media Picker to a document type property so you can select media items from the content section."
}
]
}
diff --git a/src/Umbraco.Web.UI/config/trees.Release.config b/src/Umbraco.Web.UI/config/trees.Release.config
index bce7604637..c0205c7db4 100644
--- a/src/Umbraco.Web.UI/config/trees.Release.config
+++ b/src/Umbraco.Web.UI/config/trees.Release.config
@@ -22,9 +22,9 @@
-
+
-
+
diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config
index 5ba3254549..a36df8bbff 100644
--- a/src/Umbraco.Web.UI/config/trees.config
+++ b/src/Umbraco.Web.UI/config/trees.config
@@ -20,11 +20,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -39,5 +39,5 @@
-
+
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/EditorModelEventArgs.cs b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs
new file mode 100644
index 0000000000..153a2d8786
--- /dev/null
+++ b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Umbraco.Web.Editors
+{
+ public sealed class EditorModelEventArgs : EditorModelEventArgs
+ {
+ public EditorModelEventArgs(EditorModelEventArgs baseArgs)
+ : base(baseArgs.Model, baseArgs.UmbracoContext)
+ {
+ Model = (T)baseArgs.Model;
+ }
+
+ public EditorModelEventArgs(T model, UmbracoContext umbracoContext)
+ : base(model, umbracoContext)
+ {
+ Model = model;
+ }
+
+ public new T Model { get; private set; }
+ }
+
+ public class EditorModelEventArgs : EventArgs
+ {
+ public EditorModelEventArgs(object model, UmbracoContext umbracoContext)
+ {
+ Model = model;
+ UmbracoContext = umbracoContext;
+ }
+
+ public object Model { get; private set; }
+ public UmbracoContext UmbracoContext { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/EditorModelEventManager.cs b/src/Umbraco.Web/Editors/EditorModelEventManager.cs
index 44454ca6c3..e204bdc044 100644
--- a/src/Umbraco.Web/Editors/EditorModelEventManager.cs
+++ b/src/Umbraco.Web/Editors/EditorModelEventManager.cs
@@ -1,39 +1,9 @@
-using System;
using System.Web.Http.Filters;
using Umbraco.Core.Events;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Editors
{
- public class EditorModelEventArgs : EventArgs
- {
- public EditorModelEventArgs(object model, UmbracoContext umbracoContext)
- {
- Model = model;
- UmbracoContext = umbracoContext;
- }
-
- public object Model { get; private set; }
- public UmbracoContext UmbracoContext { get; private set; }
- }
-
- public sealed class EditorModelEventArgs : EditorModelEventArgs
- {
- public EditorModelEventArgs(EditorModelEventArgs baseArgs)
- : base(baseArgs.Model, baseArgs.UmbracoContext)
- {
- Model = (T)baseArgs.Model;
- }
-
- public EditorModelEventArgs(T model, UmbracoContext umbracoContext)
- : base(model, umbracoContext)
- {
- Model = model;
- }
-
- public new T Model { get; private set; }
- }
-
///
/// Used to emit events for editor models in the back office
///
@@ -42,6 +12,13 @@ namespace Umbraco.Web.Editors
public static event TypedEventHandler> SendingContentModel;
public static event TypedEventHandler> SendingMediaModel;
public static event TypedEventHandler> SendingMemberModel;
+ public static event TypedEventHandler> SendingUserModel;
+
+ private static void OnSendingUserModel(HttpActionExecutedContext sender, EditorModelEventArgs e)
+ {
+ var handler = SendingUserModel;
+ if (handler != null) handler(sender, e);
+ }
private static void OnSendingContentModel(HttpActionExecutedContext sender, EditorModelEventArgs e)
{
@@ -85,6 +62,12 @@ namespace Umbraco.Web.Editors
{
OnSendingMemberModel(sender, new EditorModelEventArgs(e));
}
+
+ var userDisplay = e.Model as UserDisplay;
+ if (userDisplay != null)
+ {
+ OnSendingUserModel(sender, new EditorModelEventArgs(e));
+ }
}
}
diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs
index 61acf7dfed..b0677ed78e 100644
--- a/src/Umbraco.Web/Editors/TourController.cs
+++ b/src/Umbraco.Web/Editors/TourController.cs
@@ -2,9 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Web.Models;
@@ -25,24 +23,36 @@ namespace Umbraco.Web.Editors
if (UmbracoConfig.For.UmbracoSettings().BackOffice.Tours.EnableTours == false)
return result;
- var coreTourFiles = Directory.GetFiles(
- Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"), "*.json");
+ var filters = TourFilterResolver.Current.Filters.ToList();
+
+ //get all filters that will be applied to all tour aliases
+ var aliasOnlyFilters = filters.Where(x => x.PluginName == null && x.TourFileName == null).ToList();
- foreach (var tourFile in coreTourFiles)
+ //don't pass in any filters for core tours that have a plugin name assigned
+ var nonPluginFilters = filters.Where(x => x.PluginName == null).ToList();
+
+ //add core tour files
+ var coreToursPath = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours");
+ if (Directory.Exists(coreToursPath))
{
- var contents = File.ReadAllText(tourFile);
-
- result.Add(new BackOfficeTourFile
+ foreach (var tourFile in Directory.EnumerateFiles(coreToursPath, "*.json"))
{
- FileName = Path.GetFileNameWithoutExtension(tourFile),
- Tours = JsonConvert.DeserializeObject(contents)
- });
+ TryParseTourFile(tourFile, result, nonPluginFilters, aliasOnlyFilters);
+ }
}
- //collect all tour files in packges
+ //collect all tour files in packages
foreach (var plugin in Directory.EnumerateDirectories(IOHelper.MapPath(SystemDirectories.AppPlugins)))
{
var pluginName = Path.GetFileName(plugin.TrimEnd('\\'));
+ var pluginFilters = filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)).ToList();
+
+ //If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely
+ var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null);
+ if (isPluginFiltered) continue;
+
+ //combine matched package filters with filters not specific to a package
+ var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList();
foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice"))
{
@@ -50,13 +60,7 @@ namespace Umbraco.Web.Editors
{
foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json"))
{
- var contents = File.ReadAllText(tourFile);
- result.Add(new BackOfficeTourFile
- {
- FileName = Path.GetFileNameWithoutExtension(tourFile),
- PluginName = pluginName,
- Tours = JsonConvert.DeserializeObject(contents)
- });
+ TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName);
}
}
}
@@ -64,5 +68,54 @@ namespace Umbraco.Web.Editors
return result.OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase);
}
+
+ private void TryParseTourFile(string tourFile,
+ ICollection result,
+ List filters,
+ List aliasOnlyFilters,
+ string pluginName = null)
+ {
+ var fileName = Path.GetFileNameWithoutExtension(tourFile);
+ if (fileName == null) return;
+
+ //get the filters specific to this file
+ var fileFilters = filters.Where(x => x.TourFileName != null && x.TourFileName.IsMatch(fileName)).ToList();
+
+ //If there is any filter applied to match the file only (no tour alias) then ignore the file entirely
+ var isFileFiltered = fileFilters.Any(x => x.TourAlias == null);
+ if (isFileFiltered) return;
+
+ //now combine all aliases to filter below
+ var aliasFilters = aliasOnlyFilters.Concat(filters.Where(x => x.TourAlias != null))
+ .Select(x => x.TourAlias)
+ .ToList();
+
+ try
+ {
+ var contents = File.ReadAllText(tourFile);
+ var tours = JsonConvert.DeserializeObject(contents);
+
+ var tour = new BackOfficeTourFile
+ {
+ FileName = Path.GetFileNameWithoutExtension(tourFile),
+ PluginName = pluginName,
+ Tours = tours
+ .Where(x => aliasFilters.Count == 0 || aliasFilters.All(filter => filter.IsMatch(x.Alias)) == false)
+ .ToArray()
+ };
+
+ //don't add if all of the tours are filtered
+ if (tour.Tours.Any())
+ result.Add(tour);
+ }
+ catch (IOException e)
+ {
+ throw new IOException("Error while trying to read file: " + tourFile, e);
+ }
+ catch (JsonReaderException e)
+ {
+ throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs
index 2a8e052f26..2e6cd5dbbf 100644
--- a/src/Umbraco.Web/Editors/UsersController.cs
+++ b/src/Umbraco.Web/Editors/UsersController.cs
@@ -182,6 +182,7 @@ namespace Umbraco.Web.Editors
///
///
///
+ [OutgoingEditorModelEvent]
public UserDisplay GetById(int id)
{
var user = Services.UserService.GetUserById(id);
@@ -494,6 +495,7 @@ namespace Umbraco.Web.Editors
///
///
///
+ [OutgoingEditorModelEvent]
public async Task PostSaveUser(UserSave userSave)
{
if (userSave == null) throw new ArgumentNullException("userSave");
diff --git a/src/Umbraco.Web/Features/DisabledFeatures.cs b/src/Umbraco.Web/Features/DisabledFeatures.cs
new file mode 100644
index 0000000000..d771c101f4
--- /dev/null
+++ b/src/Umbraco.Web/Features/DisabledFeatures.cs
@@ -0,0 +1,24 @@
+using Umbraco.Core.Collections;
+using Umbraco.Web.WebApi;
+
+namespace Umbraco.Web.Features
+{
+ ///
+ /// Represents disabled features.
+ ///
+ internal class DisabledFeatures
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DisabledFeatures()
+ {
+ Controllers = new TypeList();
+ }
+
+ ///
+ /// Gets the disabled controllers.
+ ///
+ public TypeList Controllers { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Features/FeaturesResolver.cs b/src/Umbraco.Web/Features/FeaturesResolver.cs
new file mode 100644
index 0000000000..cdb6fae9ea
--- /dev/null
+++ b/src/Umbraco.Web/Features/FeaturesResolver.cs
@@ -0,0 +1,28 @@
+using Umbraco.Core.ObjectResolution;
+
+namespace Umbraco.Web.Features
+{
+ internal class FeaturesResolver : SingleObjectResolverBase
+ {
+ public FeaturesResolver(UmbracoFeatures value)
+ : base(value)
+ { }
+
+ ///
+ /// Sets the features.
+ ///
+ /// For developers, at application startup.
+ public void SetFeatures(UmbracoFeatures features)
+ {
+ Value = features;
+ }
+
+ ///
+ /// Gets the features.
+ ///
+ public UmbracoFeatures Features
+ {
+ get { return Value; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Features/UmbracoFeatures.cs b/src/Umbraco.Web/Features/UmbracoFeatures.cs
new file mode 100644
index 0000000000..5dc64ff5e4
--- /dev/null
+++ b/src/Umbraco.Web/Features/UmbracoFeatures.cs
@@ -0,0 +1,42 @@
+using System;
+using Umbraco.Web.WebApi;
+
+namespace Umbraco.Web.Features
+{
+ ///
+ /// Represents the Umbraco features.
+ ///
+ internal class UmbracoFeatures
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UmbracoFeatures()
+ {
+ Disabled = new DisabledFeatures();
+ }
+
+ // note
+ // currently, the only thing a FeatureSet does is list disabled controllers,
+ // but eventually we could enable and disable more parts of Umbraco. and then
+ // we would need some logic to figure out what's enabled/disabled - hence it's
+ // better to use IsEnabled, where the logic would go, rather than directly
+ // accessing the Disabled collection.
+
+ ///
+ /// Gets the disabled features.
+ ///
+ public DisabledFeatures Disabled { get; set; }
+
+ ///
+ /// Determines whether a feature is enabled.
+ ///
+ public bool IsEnabled(Type feature)
+ {
+ if (typeof(UmbracoApiControllerBase).IsAssignableFrom(feature))
+ return Disabled.Controllers.Contains(feature) == false;
+
+ throw new NotSupportedException("Not a supported feature type.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs
index 1e0f345b52..a973a92429 100644
--- a/src/Umbraco.Web/Models/BackOfficeTour.cs
+++ b/src/Umbraco.Web/Models/BackOfficeTour.cs
@@ -1,11 +1,10 @@
-using System;
-using System.Linq;
-using System.Runtime.Serialization;
-using System.Text;
-using System.Threading.Tasks;
+using System.Runtime.Serialization;
namespace Umbraco.Web.Models
{
+ ///
+ /// A model representing a tour.
+ ///
[DataContract(Name = "tour", Namespace = "")]
public class BackOfficeTour
{
@@ -15,6 +14,8 @@ namespace Umbraco.Web.Models
public string Alias { get; set; }
[DataMember(Name = "group")]
public string Group { get; set; }
+ [DataMember(Name = "groupOrder")]
+ public int GroupOrder { get; set; }
[DataMember(Name = "allowDisable")]
public bool AllowDisable { get; set; }
[DataMember(Name = "steps")]
diff --git a/src/Umbraco.Web/Models/BackOfficeTourFile.cs b/src/Umbraco.Web/Models/BackOfficeTourFile.cs
index 69b35c8088..7291a89ff4 100644
--- a/src/Umbraco.Web/Models/BackOfficeTourFile.cs
+++ b/src/Umbraco.Web/Models/BackOfficeTourFile.cs
@@ -3,6 +3,9 @@ using System.Runtime.Serialization;
namespace Umbraco.Web.Models
{
+ ///
+ /// A model representing the file used to load a tour.
+ ///
[DataContract(Name = "tourFile", Namespace = "")]
public class BackOfficeTourFile
{
diff --git a/src/Umbraco.Web/Models/BackOfficeTourFilter.cs b/src/Umbraco.Web/Models/BackOfficeTourFilter.cs
new file mode 100644
index 0000000000..994cdb6d29
--- /dev/null
+++ b/src/Umbraco.Web/Models/BackOfficeTourFilter.cs
@@ -0,0 +1,61 @@
+using System.Text.RegularExpressions;
+
+namespace Umbraco.Web.Models
+{
+ public class BackOfficeTourFilter
+ {
+ public Regex PluginName { get; private set; }
+ public Regex TourFileName { get; private set; }
+ public Regex TourAlias { get; private set; }
+
+ ///
+ /// Create a filter to filter out a whole plugin's tours
+ ///
+ ///
+ ///
+ public static BackOfficeTourFilter FilterPlugin(Regex pluginName)
+ {
+ return new BackOfficeTourFilter(pluginName, null, null);
+ }
+
+ ///
+ /// Create a filter to filter out a whole tour file
+ ///
+ ///
+ ///
+ public static BackOfficeTourFilter FilterFile(Regex tourFileName)
+ {
+ return new BackOfficeTourFilter(null, tourFileName, null);
+ }
+
+ ///
+ /// Create a filter to filter out a tour alias, this will filter out the same alias found in all files
+ ///
+ ///
+ ///
+ public static BackOfficeTourFilter FilterAlias(Regex tourAlias)
+ {
+ return new BackOfficeTourFilter(null, null, tourAlias);
+ }
+
+ ///
+ /// Constructor to create a tour filter
+ ///
+ /// Value to filter out tours by a plugin, can be null
+ /// Value to filter out a tour file, can be null
+ /// Value to filter out a tour alias, can be null
+ ///
+ /// Depending on what is null will depend on how the filter is applied.
+ /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check tour alias is not NULL and then match it,
+ /// if any steps is NULL then the filters upstream are applied.
+ /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from the plugin "hello" but not from other plugins if the same file name exists.
+ /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the plugin or file name
+ ///
+ public BackOfficeTourFilter(Regex pluginName, Regex tourFileName, Regex tourAlias)
+ {
+ PluginName = pluginName;
+ TourFileName = tourFileName;
+ TourAlias = tourAlias;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/BackOfficeTourStep.cs b/src/Umbraco.Web/Models/BackOfficeTourStep.cs
index e0371bf4b5..a6b104d882 100644
--- a/src/Umbraco.Web/Models/BackOfficeTourStep.cs
+++ b/src/Umbraco.Web/Models/BackOfficeTourStep.cs
@@ -2,6 +2,9 @@
namespace Umbraco.Web.Models
{
+ ///
+ /// A model representing a step in a tour.
+ ///
[DataContract(Name = "step", Namespace = "")]
public class BackOfficeTourStep
{
@@ -16,7 +19,7 @@ namespace Umbraco.Web.Models
[DataMember(Name = "elementPreventClick")]
public bool ElementPreventClick { get; set; }
[DataMember(Name = "backdropOpacity")]
- public float BackdropOpacity { get; set; }
+ public float? BackdropOpacity { get; set; }
[DataMember(Name = "event")]
public string Event { get; set; }
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs b/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs
new file mode 100644
index 0000000000..29922750cf
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs
@@ -0,0 +1,26 @@
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ ///
+ /// A model representing the navigation ("apps") inside an editor in the back office
+ ///
+ [DataContract(Name = "user", Namespace = "")]
+ public class EditorNavigation
+ {
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ [DataMember(Name = "alias")]
+ public string Alias { get; set; }
+
+ [DataMember(Name = "icon")]
+ public string Icon { get; set; }
+
+ [DataMember(Name = "view")]
+ public string View { get; set; }
+
+ [DataMember(Name = "active")]
+ public bool Active { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
index 75e1462ec3..5acad7ea49 100644
--- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
@@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
-{
+{
///
/// Represents information for the current user
///
@@ -29,7 +29,7 @@ namespace Umbraco.Web.Models.ContentEditing
[Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
[EditorBrowsable(EditorBrowsableState.Never)]
[ReadOnly(true)]
- [DataMember(Name = "userType")]
+ [DataMember(Name = "userType")]
public string UserType { get; set; }
[ReadOnly(true)]
@@ -64,8 +64,8 @@ namespace Umbraco.Web.Models.ContentEditing
/// A list of sections the user is allowed to view.
///
[DataMember(Name = "allowedSections")]
- public IEnumerable AllowedSections { get; set; }
-
-
+ public IEnumerable AllowedSections { get; set; }
+
+
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs
index 8a79344c8e..1464d9580f 100644
--- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs
@@ -18,8 +18,13 @@ namespace Umbraco.Web.Models.ContentEditing
AvailableCultures = new Dictionary();
StartContentIds = new List();
StartMediaIds = new List();
+ Navigation = new List();
}
-
+
+ [DataMember(Name = "navigation")]
+ [ReadOnly(true)]
+ public IEnumerable Navigation { get; set; }
+
///
/// Gets the available cultures (i.e. to populate a drop down)
/// The key is the culture stored in the database, the value is the Name
diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs
index 80990fed5b..9262e7eebc 100644
--- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs
+++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Linq.Expressions;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Models.Mapping;
@@ -231,12 +232,14 @@ namespace Umbraco.Web.Models.Mapping
});
//Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that
- // this will cause an N+1 and we'll need to change how this works.
+ // this will cause an N+1 and we'll need to change how this works.
+
config.CreateMap()
.ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetUserAvatarUrls(applicationContext.ApplicationCache.RuntimeCache)))
.ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username))
.ForMember(detail => detail.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?) user.LastLoginDate))
.ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups))
+ .ForMember(detail => detail.Navigation, opt => opt.MapFrom(user => CreateUserEditorNavigation(applicationContext.Services.TextService)))
.ForMember(
detail => detail.CalculatedStartContentIds,
opt => opt.MapFrom(user => GetStartNodeValues(
@@ -255,7 +258,7 @@ namespace Umbraco.Web.Models.Mapping
"media/mediaRoot")))
.ForMember(
detail => detail.StartContentIds,
- opt => opt.MapFrom(user => GetStartNodeValues(
+ opt => opt.MapFrom(user => GetStartNodeValues(
user.StartContentIds.ToArray(),
applicationContext.Services.TextService,
applicationContext.Services.EntityService,
@@ -263,7 +266,7 @@ namespace Umbraco.Web.Models.Mapping
"content/contentRoot")))
.ForMember(
detail => detail.StartMediaIds,
- opt => opt.MapFrom(user => GetStartNodeValues(
+ opt => opt.MapFrom(user => GetStartNodeValues(
user.StartMediaIds.ToArray(),
applicationContext.Services.TextService,
applicationContext.Services.EntityService,
@@ -330,7 +333,7 @@ namespace Umbraco.Web.Models.Mapping
//the best we can do here is to return the user's first user group as a IUserType object
//but we should attempt to return any group that is the built in ones first
var groups = user.Groups.ToArray();
- detail.UserGroups = user.Groups.Select(x => x.Alias).ToArray();
+ detail.UserGroups = user.Groups.Select(x => x.Alias).ToArray();
if (groups.Length == 0)
{
@@ -358,6 +361,21 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id)));
}
+ private IEnumerable CreateUserEditorNavigation(ILocalizedTextService textService)
+ {
+ return new[]
+ {
+ new EditorNavigation
+ {
+ Active = true,
+ Alias = "details",
+ Icon = "icon-umb-users",
+ Name = textService.Localize("general/user"),
+ View = "views/users/views/user/details.html"
+ }
+ };
+ }
+
private IEnumerable GetStartNodeValues(int[] startNodeIds,
ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType,
string localizedKey)
diff --git a/src/Umbraco.Web/Models/Trees/MenuItem.cs b/src/Umbraco.Web/Models/Trees/MenuItem.cs
index 1717ecb1b6..d06fb92ecd 100644
--- a/src/Umbraco.Web/Models/Trees/MenuItem.cs
+++ b/src/Umbraco.Web/Models/Trees/MenuItem.cs
@@ -187,7 +187,7 @@ namespace Umbraco.Web.Models.Trees
nodeType,
item == null ? "" : item.Name, currentSection),
action => LaunchDialogUrl(action.Url, action.DialogTitle))
- .OnFailure(() => LegacyTreeDataConverter.GetLegacyConfirmView(Action, currentSection),
+ .OnFailure(() => LegacyTreeDataConverter.GetLegacyConfirmView(Action),
view => LaunchDialogView(
view,
ui.GetText("defaultdialogs", "confirmdelete") + " '" + (item == null ? "" : item.Name) + "' ?"));
diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs
index c016d328a9..9f235e00d5 100644
--- a/src/Umbraco.Web/Properties/AssemblyInfo.cs
+++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs
@@ -40,6 +40,7 @@ using System.Security;
[assembly: InternalsVisibleTo("Umbraco.Deploy.Cloud")]
[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")]
[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.AspNet")]
+[assembly: InternalsVisibleTo("Umbraco.Headless")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Umbraco.Forms.Core")]
diff --git a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs
index e0cda08e47..b3a23d8081 100644
--- a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs
+++ b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs
@@ -12,7 +12,7 @@ namespace Umbraco.Web.Scheduling
/// depending on whether the task is implemented as a sync or async method, and then
/// optionnally overriding RunsOnShutdown, to indicate whether the latched task should run
/// immediately on shutdown, or just be abandonned (default).
- public abstract class LatchedBackgroundTaskBase : DisposableObject, ILatchedBackgroundTask
+ public abstract class LatchedBackgroundTaskBase : DisposableObjectSlim, ILatchedBackgroundTask
{
private TaskCompletionSource _latch;
diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs
index d9bd33dd30..447a98527e 100644
--- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs
+++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs
@@ -72,7 +72,7 @@ namespace Umbraco.Web.Scheduling
if (_runner.TryAdd(this))
_timer.Change(_periodMilliseconds, 0);
else
- Dispose(true);
+ Dispose();
}
///
diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs
index 460ca536eb..dd7e757c8d 100644
--- a/src/Umbraco.Web/Security/WebSecurity.cs
+++ b/src/Umbraco.Web/Security/WebSecurity.cs
@@ -23,7 +23,7 @@ namespace Umbraco.Web.Security
///
/// A utility class used for dealing with USER security in Umbraco
///
- public class WebSecurity : DisposableObject
+ public class WebSecurity : DisposableObjectSlim
{
private HttpContextBase _httpContext;
private ApplicationContext _applicationContext;
diff --git a/src/Umbraco.Web/TourFilterResolver.cs b/src/Umbraco.Web/TourFilterResolver.cs
new file mode 100644
index 0000000000..586d0cb89f
--- /dev/null
+++ b/src/Umbraco.Web/TourFilterResolver.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Umbraco.Core.Logging;
+using Umbraco.Core.ObjectResolution;
+using Umbraco.Web.Models;
+
+namespace Umbraco.Web
+{
+ ///
+ /// Allows for adding filters for tours during startup
+ ///
+ public class TourFilterResolver : ManyObjectsResolverBase
+ {
+ public TourFilterResolver(IServiceProvider serviceProvider, ILogger logger) : base(serviceProvider, logger)
+ {
+ }
+
+ private readonly HashSet _instances = new HashSet();
+
+ public IEnumerable Filters
+ {
+ get { return Values; }
+ }
+
+ ///
+ /// Adds a filter instance
+ ///
+ ///
+ public void AddFilter(BackOfficeTourFilter filter)
+ {
+ using (Resolution.Configuration)
+ _instances.Add(filter);
+ }
+
+ ///
+ /// Removes a filter instance
+ ///
+ ///
+ public void RemoveFilter(BackOfficeTourFilter filter)
+ {
+ using (Resolution.Configuration)
+ _instances.Remove(filter);
+ }
+
+ ///
+ /// Removes a filter instance based on callback
+ ///
+ ///
+ public void RemoveFilterWhere(Func filter)
+ {
+ using (Resolution.Configuration)
+ _instances.RemoveWhere(new Predicate(filter));
+ }
+
+ ///
+ ///
+ /// Overridden to return the combined created instances based on the resolved Types and the Concrete values added with AddFilter
+ ///
+ ///
+ protected override IEnumerable CreateInstances()
+ {
+ var createdInstances = base.CreateInstances();
+ return createdInstances.Concat(_instances);
+ }
+
+ public override void Clear()
+ {
+ base.Clear();
+ _instances.Clear();
+ }
+
+ internal override void ResetCollections()
+ {
+ base.ResetCollections();
+ _instances.Clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs
index 89a615f353..37d880f262 100644
--- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs
+++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs
@@ -84,10 +84,19 @@ namespace Umbraco.Web.Trees
private async Task GetRootForMultipleAppTree(ApplicationTree configTree, FormDataCollection queryStrings)
{
if (configTree == null) throw new ArgumentNullException("configTree");
- var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext);
- if (byControllerAttempt.Success)
- {
- return byControllerAttempt.Result;
+ try
+ {
+ var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext);
+ if (byControllerAttempt.Success)
+ {
+ return byControllerAttempt.Result;
+ }
+ }
+ catch (HttpResponseException)
+ {
+ //if this occurs its because the user isn't authorized to view that tree, in this case since we are loading multiple trees we
+ //will just return null so that it's not added to the list.
+ return null;
}
var legacyAttempt = configTree.TryGetRootNodeFromLegacyTree(queryStrings, Url, configTree.ApplicationAlias);
diff --git a/src/Umbraco.Web/Trees/FileSystemTreeController.cs b/src/Umbraco.Web/Trees/FileSystemTreeController.cs
index 4f414a941d..f26e255a41 100644
--- a/src/Umbraco.Web/Trees/FileSystemTreeController.cs
+++ b/src/Umbraco.Web/Trees/FileSystemTreeController.cs
@@ -72,23 +72,64 @@ namespace Umbraco.Web.Trees
return nodes;
}
- protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
+ protected virtual MenuItemCollection GetMenuForRootNode(FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
+ //set the default to create
+ menu.DefaultMenuAlias = ActionNew.Instance.Alias;
+ //create action
+ menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
+ //refresh action
+ menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
+
+ return menu;
+ }
+
+ protected virtual MenuItemCollection GetMenuForFolder(string path, FormDataCollection queryStrings)
+ {
+ var menu = new MenuItemCollection();
+
+ //set the default to create
+ menu.DefaultMenuAlias = ActionNew.Instance.Alias;
+ //create action
+ menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
+
+ var hasChildren = FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
+
+ //We can only delete folders if it doesn't have any children (folders or files)
+ if (hasChildren == false)
+ {
+ //delete action
+ menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true);
+ }
+
+ //refresh action
+ menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
+
+ return menu;
+ }
+
+ protected virtual MenuItemCollection GetMenuForFile(string path, FormDataCollection queryStrings)
+ {
+ var menu = new MenuItemCollection();
+
+ //if it's not a directory then we only allow to delete the item
+ menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
+
+ return menu;
+ }
+
+ protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
+ {
//if root node no need to visit the filesystem so lets just create the menu and return it
if (id == Constants.System.Root.ToInvariantString())
{
- //set the default to create
- menu.DefaultMenuAlias = ActionNew.Instance.Alias;
- //create action
- menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
- //refresh action
- menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
-
- return menu;
+ return GetMenuForRootNode(queryStrings);
}
+ var menu = new MenuItemCollection();
+
var path = string.IsNullOrEmpty(id) == false && id != Constants.System.Root.ToInvariantString()
? HttpUtility.UrlDecode(id).TrimStart("/")
: "";
@@ -98,30 +139,10 @@ namespace Umbraco.Web.Trees
if (isDirectory)
{
- //set the default to create
- menu.DefaultMenuAlias = ActionNew.Instance.Alias;
- //create action
- menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
-
- var hasChildren = FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
-
- //We can only delete folders if it doesn't have any children (folders or files)
- if (hasChildren == false)
- {
- //delete action
- menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true);
- }
-
- //refresh action
- menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
- }
- else if (isFile)
- {
- //if it's not a directory then we only allow to delete the item
- menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
+ return GetMenuForFolder(path, queryStrings);
}
- return menu;
+ return isFile ? GetMenuForFile(path, queryStrings) : menu;
}
}
}
diff --git a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs
index 32a7ad5d99..c6be75f124 100644
--- a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs
+++ b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs
@@ -134,7 +134,7 @@ namespace Umbraco.Web.Trees
Attempt
.Try(GetUrlAndTitleFromLegacyAction(currentAction, xmlTreeNode.NodeID, xmlTreeNode.NodeType, xmlTreeNode.Text, currentSection),
action => menuItem.LaunchDialogUrl(action.Url, action.DialogTitle))
- .OnFailure(() => GetLegacyConfirmView(currentAction, currentSection),
+ .OnFailure(() => GetLegacyConfirmView(currentAction),
view => menuItem.LaunchDialogView(
view,
ui.GetText("defaultdialogs", "confirmdelete") + " '" + xmlTreeNode.Text + "' ?"))
@@ -164,9 +164,8 @@ namespace Umbraco.Web.Trees
/// This will look at the legacy IAction's JsFunctionName and convert it to a confirmation dialog view if possible
///
///
- ///
///
- internal static Attempt GetLegacyConfirmView(IAction action, string currentSection)
+ internal static Attempt GetLegacyConfirmView(IAction action)
{
if (action.JsFunctionName.IsNullOrWhiteSpace())
{
diff --git a/src/Umbraco.Web/Trees/MacroTreeController.cs b/src/Umbraco.Web/Trees/MacroTreeController.cs
new file mode 100644
index 0000000000..4a6ca8ad3d
--- /dev/null
+++ b/src/Umbraco.Web/Trees/MacroTreeController.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Net.Http.Formatting;
+using umbraco;
+using Umbraco.Core;
+using Umbraco.Core.Models;
+using Umbraco.Web.Models.Trees;
+using Umbraco.Web.Mvc;
+using System.Linq;
+using umbraco.BusinessLogic.Actions;
+using Umbraco.Web.WebApi.Filters;
+using Umbraco.Core.Services;
+using Constants = Umbraco.Core.Constants;
+
+namespace Umbraco.Web.Trees
+{
+ [UmbracoTreeAuthorize(Constants.Trees.Macros)]
+ [Tree(Constants.Applications.Developer, Constants.Trees.Macros, null, sortOrder: 2)]
+ [LegacyBaseTree(typeof(loadMacros))]
+ [PluginController("UmbracoTrees")]
+ [CoreTree]
+ public class MacroTreeController : TreeController
+ {
+ protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
+ {
+ var intId = id.TryConvertTo();
+ if (intId == false) throw new InvalidOperationException("Id must be an integer");
+
+ var nodes = new TreeNodeCollection();
+
+ nodes.AddRange(
+ Services.MacroService.GetAll()
+ .OrderBy(entity => entity.Name)
+ .Select(macro =>
+ {
+ var node = CreateTreeNode(macro.Id.ToInvariantString(), id, queryStrings, macro.Name, "icon-settings-alt", false);
+ node.Path = "-1," + macro.Id;
+ node.AssignLegacyJsCallback("javascript:UmbClientMgr.contentFrame('developer/macros/editMacro.aspx?macroID=" + macro.Id + "');");
+ return node;
+ }));
+
+ return nodes;
+ }
+
+ protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
+ {
+ var menu = new MenuItemCollection();
+
+ if (id == Constants.System.Root.ToInvariantString())
+ {
+ //set the default to create
+ menu.DefaultMenuAlias = ActionNew.Instance.Alias;
+
+ // root actions
+ menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)))
+ .ConvertLegacyMenuItem(null, Constants.Trees.Macros, queryStrings.GetValue("application"));
+
+ menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true);
+ return menu;
+ }
+
+ //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy dialogs
+ var menuItem = menu.Items.Add(ActionDelete.Instance, Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
+ var legacyConfirmView = LegacyTreeDataConverter.GetLegacyConfirmView(ActionDelete.Instance);
+ if (legacyConfirmView == false)
+ throw new InvalidOperationException("Could not resolve the confirmation view for the legacy action " + ActionDelete.Instance.Alias);
+ menuItem.LaunchDialogView(
+ legacyConfirmView.Result,
+ Services.TextService.Localize("general/delete"));
+
+ return menu;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Trees/ScriptTreeController.cs b/src/Umbraco.Web/Trees/ScriptTreeController.cs
index 171688bd1a..c7a3dd5f04 100644
--- a/src/Umbraco.Web/Trees/ScriptTreeController.cs
+++ b/src/Umbraco.Web/Trees/ScriptTreeController.cs
@@ -8,7 +8,7 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Scripts)]
- [Tree(Constants.Applications.Settings, "scripts", null, sortOrder: 4)]
+ [Tree(Constants.Applications.Settings, Constants.Trees.Scripts, null, sortOrder: 4)]
[LegacyBaseTree(typeof(loadScripts))]
[PluginController("UmbracoTrees")]
[CoreTree]
diff --git a/src/Umbraco.Web/Trees/XsltTreeController.cs b/src/Umbraco.Web/Trees/XsltTreeController.cs
new file mode 100644
index 0000000000..f35dc42b3a
--- /dev/null
+++ b/src/Umbraco.Web/Trees/XsltTreeController.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Net.Http.Formatting;
+using umbraco;
+using umbraco.BusinessLogic.Actions;
+using Umbraco.Core.IO;
+using Umbraco.Core.Services;
+using Umbraco.Web.Models.Trees;
+using Umbraco.Web.Mvc;
+using Umbraco.Web.WebApi.Filters;
+using Constants = Umbraco.Core.Constants;
+
+namespace Umbraco.Web.Trees
+{
+ [UmbracoTreeAuthorize(Constants.Trees.Xslt)]
+ [Tree(Constants.Applications.Developer, Constants.Trees.Xslt, null, sortOrder: 5)]
+ [LegacyBaseTree(typeof(loadXslt))]
+ [PluginController("UmbracoTrees")]
+ [CoreTree]
+ public class XsltTreeController : FileSystemTreeController
+ {
+ protected override void OnRenderFileNode(ref TreeNode treeNode)
+ {
+ ////TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy views
+ treeNode.AssignLegacyJsCallback("javascript:UmbClientMgr.contentFrame('developer/xslt/editXslt.aspx?file=" + treeNode.Id + "');");
+ }
+
+ protected override void OnRenderFolderNode(ref TreeNode treeNode)
+ {
+ //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy views
+ treeNode.AssignLegacyJsCallback("javascript:void(0);");
+ }
+
+ protected override MenuItemCollection GetMenuForFile(string path, FormDataCollection queryStrings)
+ {
+ var menu = new MenuItemCollection();
+
+ //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy dialogs
+ var menuItem = menu.Items.Add(ActionDelete.Instance, Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
+ var legacyConfirmView = LegacyTreeDataConverter.GetLegacyConfirmView(ActionDelete.Instance);
+ if (legacyConfirmView == false)
+ throw new InvalidOperationException("Could not resolve the confirmation view for the legacy action " + ActionDelete.Instance.Alias);
+ menuItem.LaunchDialogView(
+ legacyConfirmView.Result,
+ Services.TextService.Localize("general/delete"));
+
+ return menu;
+ }
+
+ protected override MenuItemCollection GetMenuForRootNode(FormDataCollection queryStrings)
+ {
+ var menu = new MenuItemCollection();
+
+ //set the default to create
+ menu.DefaultMenuAlias = ActionNew.Instance.Alias;
+
+ // root actions
+ menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)))
+ .ConvertLegacyMenuItem(null, Constants.Trees.Xslt, queryStrings.GetValue("application"));
+
+ menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true);
+ return menu;
+ }
+
+ protected override IFileSystem2 FileSystem
+ {
+ get { return FileSystemProviderManager.Current.XsltFileSystem; }
+ }
+
+ private static readonly string[] ExtensionsStatic = { "xslt" };
+
+ protected override string[] Extensions
+ {
+ get { return ExtensionsStatic; }
+ }
+ protected override string FileIcon
+ {
+ get { return "icon-code"; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 83ed982639..e77c8e167b 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -315,9 +315,11 @@
+
+
@@ -328,6 +330,8 @@
+
+
@@ -335,6 +339,8 @@
+
+
@@ -376,6 +382,7 @@
+
@@ -496,6 +503,7 @@
+
@@ -504,6 +512,7 @@
+
@@ -794,6 +803,7 @@
+
diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs
index dbb165a526..3532575f38 100644
--- a/src/Umbraco.Web/UmbracoContext.cs
+++ b/src/Umbraco.Web/UmbracoContext.cs
@@ -20,7 +20,7 @@ namespace Umbraco.Web
///
/// Class that encapsulates Umbraco information of a specific HTTP request
///
- public class UmbracoContext : DisposableObject, IDisposeOnRequestEnd
+ public class UmbracoContext : DisposableObjectSlim, IDisposeOnRequestEnd
{
internal const string HttpContextItemName = "Umbraco.Web.UmbracoContext";
private static readonly object Locker = new object();
diff --git a/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs
new file mode 100644
index 0000000000..58495a06b3
--- /dev/null
+++ b/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs
@@ -0,0 +1,19 @@
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using Umbraco.Web.Features;
+
+namespace Umbraco.Web.WebApi.Filters
+{
+ ///
+ /// Ensures that the controller is an authorized feature.
+ ///
+ /// Else returns unauthorized.
+ public sealed class FeatureAuthorizeAttribute : AuthorizeAttribute
+ {
+ protected override bool IsAuthorized(HttpActionContext actionContext)
+ {
+ var controllerType = actionContext.ControllerContext.ControllerDescriptor.ControllerType;
+ return FeaturesResolver.Current.Features.IsEnabled(controllerType);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs
index f82d73edf7..48b1bc5d46 100644
--- a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs
+++ b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs
@@ -6,12 +6,14 @@ using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Services;
using Umbraco.Web.Security;
+using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.WebApi
{
///
/// The base class for API controllers that expose Umbraco services - THESE ARE NOT AUTO ROUTED
///
+ [FeatureAuthorize]
public abstract class UmbracoApiControllerBase : ApiController
{
protected UmbracoApiControllerBase()
@@ -35,7 +37,7 @@ namespace Umbraco.Web.WebApi
InstanceId = Guid.NewGuid();
_umbraco = umbracoHelper;
}
-
+
private UmbracoHelper _umbraco;
///
diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs
index c38540b6a2..793e56dbba 100644
--- a/src/Umbraco.Web/WebBootManager.cs
+++ b/src/Umbraco.Web/WebBootManager.cs
@@ -38,6 +38,7 @@ using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Web.Editors;
+using Umbraco.Web.Features;
using Umbraco.Web.HealthCheck;
using Umbraco.Web.Profiling;
using Umbraco.Web.Search;
@@ -352,6 +353,10 @@ namespace Umbraco.Web
protected override void InitializeResolvers()
{
base.InitializeResolvers();
+
+ FeaturesResolver.Current = new FeaturesResolver(new UmbracoFeatures());
+
+ TourFilterResolver.Current = new TourFilterResolver(ServiceProvider, LoggerResolver.Current.Logger);
SearchableTreeResolver.Current = new SearchableTreeResolver(ServiceProvider, LoggerResolver.Current.Logger, ApplicationContext.Services.ApplicationTreeService, () => PluginManager.ResolveSearchableTrees());
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadMacros.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadMacros.cs
index 18754f6c75..7e61810078 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadMacros.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadMacros.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Data;
using System.IO;
using System.Text;
@@ -29,10 +30,8 @@ using Umbraco.Core;
namespace umbraco
{
- ///
- /// Handles loading of the cache application into the developer application tree
- ///
- [Tree(Constants.Applications.Developer, "macros", "Macros", sortOrder: 2)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed from the codebase in the future")]
public class loadMacros : BaseTree
{
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadXslt.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadXslt.cs
index 4a03b31551..9b26de52ec 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadXslt.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadXslt.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Data;
using System.IO;
using System.Text;
@@ -29,10 +30,8 @@ using Umbraco.Core;
namespace umbraco
{
- ///
- /// Handles loading of the xslt files into the application tree
- ///
- [Tree(Constants.Applications.Developer, "xslt", "XSLT Files", sortOrder: 5)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed from the codebase in the future")]
public class loadXslt : FileSystemTree
{
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs
index 73800ca117..2939bd0b31 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs
@@ -42,8 +42,8 @@ namespace umbraco.cms.presentation.developer
if (IsPostBack == false)
{
ClientTools
- .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias)
- .SyncTree("-1,init," + _macro.Id, false);
+ .SetActiveTreeType(Constants.Trees.Macros)
+ .SyncTree("-1," + _macro.Id, false);
string tempMacroAssembly = _macro.ControlAssembly ?? "";
string tempMacroType = _macro.ControlType ?? "";
@@ -321,8 +321,8 @@ namespace umbraco.cms.presentation.developer
Page.Validate();
ClientTools
- .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias)
- .SyncTree("-1,init," + _macro.Id.ToInvariantString(), true); //true forces the reload
+ .SetActiveTreeType(Constants.Trees.Macros)
+ .SyncTree("-1," + _macro.Id.ToInvariantString(), true); //true forces the reload
var tempMacroAssembly = macroAssembly.Text;
var tempMacroType = macroType.Text;
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs
index 03ce088941..b1414ddb68 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs
@@ -33,9 +33,9 @@ namespace umbraco.cms.presentation.developer
if (!IsPostBack)
{
string file = Request.QueryString["file"];
- string path = DeepLink.GetTreePathFromFilePath(file);
+ string path = DeepLink.GetTreePathFromFilePath(file, false, true);
ClientTools
- .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias)
+ .SetActiveTreeType(Constants.Trees.Xslt)
.SyncTree(path, false);
}
diff --git a/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs b/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs
index 28cc814a02..8609504313 100644
--- a/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs
+++ b/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs
@@ -14,7 +14,7 @@ using Umbraco.Core.IO;
namespace umbraco.cms.businesslogic.packager.repositories
{
[Obsolete("This should not be used and will be removed in future Umbraco versions")]
- public class Repository : DisposableObject
+ public class Repository : DisposableObjectSlim
{
public string Guid { get; private set; }
@@ -289,7 +289,7 @@ namespace umbraco.cms.businesslogic.packager.repositories
}
///
- /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic.
+ /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic.
///
protected override void DisposeResources()
{
diff --git a/src/umbraco.cms/helpers/DeepLink.cs b/src/umbraco.cms/helpers/DeepLink.cs
index ac0fdce5b7..4b0878c355 100644
--- a/src/umbraco.cms/helpers/DeepLink.cs
+++ b/src/umbraco.cms/helpers/DeepLink.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net;
using System.Text;
using umbraco.BusinessLogic;
using System.Web;
@@ -11,15 +12,24 @@ namespace umbraco.cms.helpers
public class DeepLink
{
public static string GetTreePathFromFilePath(string filePath)
+ {
+ return GetTreePathFromFilePath(filePath, true, false);
+ }
+
+ internal static string GetTreePathFromFilePath(string filePath, bool includeInit, bool urlEncode)
{
List treePath = new List();
treePath.Add("-1");
- treePath.Add("init");
+ if (includeInit)
+ treePath.Add("init");
string[] pathPaths = filePath.Split('/');
for (int p = 0; p < pathPaths.Length; p++)
{
- treePath.Add(string.Join("/", pathPaths.Take(p + 1).ToArray()));
+ var s = string.Join("/", pathPaths.Take(p + 1).ToArray());
+ if (urlEncode)
+ s = WebUtility.UrlEncode(s);
+ treePath.Add(s);
}
string sPath = string.Join(",", treePath.ToArray());
return sPath;