@@ -97,22 +98,28 @@
.ui-tabs-list
{
- border-bottom: 1px solid var(--color-line);
- padding: 0 10px;
+ /*border-bottom: 1px solid var(--color-line);*/
+ padding: var(--padding) var(--padding) 0;
+ margin-bottom: calc(var(--padding) * -1);
}
.ui-tabs-list-item
{
display: inline-flex;
align-items: center;
- height: 60px;
- padding: 0 20px;
- margin: 0;
+ height: 58px;
+ padding: 0 var(--padding);
font-size: var(--font-size);
- color: var(--color-fg-light);
+ color: var(--color-fg);
position: relative;
- overflow: hidden;
transition: color 0.2s ease;
+ border-radius: var(--radius) var(--radius) 0 0;
+ background: rgba(white, 0.5); // TODO
+
+ & + .ui-tabs-list-item
+ {
+ margin-left: 4px;
+ }
&:hover
{
@@ -121,7 +128,14 @@
&:before
{
- display: none;
+ content: '';
+ width: 100%;
+ height: 3px;
+ position: absolute;
+ left: 0;
+ top: 0;
+ background: transparent;
+ border-radius: var(--radius) var(--radius) 0 0;
}
&[disabled]
@@ -134,25 +148,48 @@
{
content: '';
height: 4px;
- border-radius: 4px 4px 0 0;
- background: var(--color-line);
+ width: 4px;
position: absolute;
- left: 18px;
- right: 18px;
- bottom: 0;
- transform: translateY(5px) scaleX(0.5);
- transition: transform 0.2s ease;
+ left: 0;
+ bottom: -4px;
}
&.is-active
{
font-weight: 700;
color: var(--color-fg);
+ background: var(--color-bg-light);
+
+ .ui-tabs-list-item-count
+ {
+ background: var(--color-bg);
+ }
+
+ &:before
+ {
+ background: var(--color-line);
+ }
&:after
{
- transform: translateY(0) scaleX(1);
+ background: var(--color-bg-light);
}
}
}
+
+ .ui-tabs-list-item-count
+ {
+ font-style: normal;
+ font-size: 12px;
+ overflow: hidden;
+ float: right;
+ padding: 2px 6px;
+ background: var(--color-bg-light);
+ border-radius: 10px;
+ margin-left: 8px;
+ margin-right: -4px;
+ margin-top: -1px;
+ font-weight: bold;
+ color: var(--color-fg);
+ }
\ No newline at end of file
diff --git a/zero.Web/App/Pages/settings/application.vue b/zero.Web/App/Pages/settings/application.vue
new file mode 100644
index 00000000..5b95479a
--- /dev/null
+++ b/zero.Web/App/Pages/settings/application.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/zero.Web/App/Pages/settings/applications.vue b/zero.Web/App/Pages/settings/applications.vue
new file mode 100644
index 00000000..7bb35f5e
--- /dev/null
+++ b/zero.Web/App/Pages/settings/applications.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+ {{app.name}}
+ {{app.alias}}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/zero.Web/App/Resources/applications.js b/zero.Web/App/Resources/applications.js
index 022310ac..da8787d8 100644
--- a/zero.Web/App/Resources/applications.js
+++ b/zero.Web/App/Resources/applications.js
@@ -1,10 +1,42 @@
import Axios from 'axios';
+const base = 'applications/';
+
export default {
- // get all applications
- getAll()
+ // get application by id
+ getById(id)
{
- return Axios.get('applications/getAll').then(res => Promise.resolve(res.data));
+ return Axios.get(base + 'getById', { params: { id } }).then(res => Promise.resolve(res.data));
+ },
+
+ // get new application model
+ getEmpty()
+ {
+ return Axios.get(base + 'getEmpty').then(res => Promise.resolve(res.data));
+ },
+
+ // get all applications
+ getAll(query)
+ {
+ return Axios.get(base + 'getAll', { params: query }).then(res => Promise.resolve(res.data));
+ },
+
+ // get all application features
+ getAllFeatures()
+ {
+ return Axios.get(base + 'getAllFeatures').then(res => Promise.resolve(res.data));
+ },
+
+ // save an application
+ save(model)
+ {
+ return Axios.post(base + 'save', model).then(res => Promise.resolve(res.data));
+ },
+
+ // deletes an application
+ delete(id)
+ {
+ return Axios.delete(base + 'delete', { params: { id } }).then(res => Promise.resolve(res.data));
}
};
\ No newline at end of file
diff --git a/zero.Web/App/pages/settings/routes.js b/zero.Web/App/pages/settings/routes.js
index 776eb6fb..e8652d23 100644
--- a/zero.Web/App/pages/settings/routes.js
+++ b/zero.Web/App/pages/settings/routes.js
@@ -12,6 +12,7 @@ detailPages[zero.alias.settings.users] = [
detailPages[zero.alias.settings.countries] = 'country';
detailPages[zero.alias.settings.translations] = 'translations';
detailPages[zero.alias.settings.languages] = 'language';
+detailPages[zero.alias.settings.applications] = 'application';
if (section)
{
diff --git a/zero.Web/Controllers/ApplicationsController.cs b/zero.Web/Controllers/ApplicationsController.cs
index 93807dfe..411887de 100644
--- a/zero.Web/Controllers/ApplicationsController.cs
+++ b/zero.Web/Controllers/ApplicationsController.cs
@@ -1,24 +1,92 @@
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using System.Globalization;
+using System.Linq;
using System.Threading.Tasks;
using zero.Core;
using zero.Core.Api;
+using zero.Core.Entities;
+using zero.Core.Extensions;
+using zero.Core.Identity;
+using zero.Web.Mapper;
+using zero.Web.Models;
namespace zero.Web.Controllers
{
+ [ZeroAuthorize(Permissions.Settings.Applications, PermissionsValue.Read)]
public class ApplicationsController : BackofficeController
{
private IApplicationsApi Api { get; set; }
- public ApplicationsController(IZeroConfiguration config, IApplicationsApi api) : base(config)
+ private ZeroOptions Options { get; set; }
+
+
+ public ApplicationsController(IZeroConfiguration config, IApplicationsApi api, IMapper mapper, IToken token, IOptionsMonitor
options) : base(config, mapper, token)
{
Api = api;
+ Options = options.CurrentValue;
}
- public async Task GetAll()
+ ///
+ /// Get translation by id
+ ///
+ public IActionResult GetEmpty()
{
- return Json(await Api.GetAll());
+ return Json(new ApplicationEditModel());
+ }
+
+
+ ///
+ /// Get translation by id
+ ///
+ public async Task GetById([FromQuery] string id)
+ {
+ return await As(await Api.GetById(id));
+ }
+
+
+ ///
+ /// Get all translations
+ ///
+ public async Task GetAll([FromQuery] ListQuery query = null)
+ {
+ if (query == null)
+ {
+ return await As(await Api.GetAll());
+ }
+
+ return await As(await Api.GetByQuery(query));
+ }
+
+
+ ///
+ /// Get all available features to select
+ ///
+ public IActionResult GetAllFeatures()
+ {
+ return Json(Options.Features);
+ }
+
+
+ ///
+ /// Save translation
+ ///
+ [ZeroAuthorize(Permissions.Settings.Applications, PermissionsValue.Write)]
+ public async Task Save([FromBody] ApplicationEditModel model)
+ {
+ Application entity = await Mapper.Map(model, await Api.GetById(model.Id));
+ return await As(await Api.Save(entity));
+ }
+
+
+ ///
+ /// Deletes a translation
+ ///
+ [ZeroAuthorize(Permissions.Settings.Applications, PermissionsValue.Write)]
+ public async Task Delete([FromQuery] string id)
+ {
+ return await As(await Api.Delete(id));
}
}
}
diff --git a/zero.Web/Mapper/ApplicationMapperConfig.cs b/zero.Web/Mapper/ApplicationMapperConfig.cs
new file mode 100644
index 00000000..b6d12b69
--- /dev/null
+++ b/zero.Web/Mapper/ApplicationMapperConfig.cs
@@ -0,0 +1,47 @@
+using zero.Core.Entities;
+using zero.Web.Models;
+
+namespace zero.Web.Mapper
+{
+ public class ApplicationMapperConfig : IMapperConfig
+ {
+ ///
+ public void Configure(IMapper config)
+ {
+ config.CreateMap((source, target) =>
+ {
+ target.Id = source.Id;
+ target.Name = source.Name;
+ target.FullName = source.FullName;
+ target.Email = source.Email;
+ target.IsActive = source.IsActive;
+ target.CreatedDate = source.CreatedDate;
+ target.Domains = source.Domains;
+ target.Image = source.Image;
+ target.Icon = source.Icon;
+ target.Features = source.Features;
+ });
+
+ config.CreateMap((source, target) =>
+ {
+ target.Name = source.Name;
+ target.FullName = source.FullName;
+ target.Email = source.Email;
+ target.IsActive = source.IsActive;
+ target.Domains = source.Domains;
+ target.Image = source.Image;
+ target.Icon = source.Icon;
+ target.Features = source.Features;
+ });
+
+ config.CreateMap((source, target) =>
+ {
+ target.Id = source.Id;
+ target.Name = source.Name;
+ target.IsActive = source.IsActive;
+ target.Domains = source.Domains;
+ target.Image = source.Image?.Source;
+ });
+ }
+ }
+}
diff --git a/zero.Web/Models/ApplicationEditModel.cs b/zero.Web/Models/ApplicationEditModel.cs
new file mode 100644
index 00000000..d92470e6
--- /dev/null
+++ b/zero.Web/Models/ApplicationEditModel.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using zero.Core.Entities;
+
+namespace zero.Web.Models
+{
+ public class ApplicationEditModel : EditModel
+ {
+ public string Name { get; set; }
+
+ public Media Image { get; set; }
+
+ public Media Icon { get; set; }
+
+ public string[] Domains { get; set; } = new string[] { };
+
+ public string FullName { get; set; }
+
+ public string Email { get; set; }
+
+ public List Features { get; set; } = new List();
+ }
+}
diff --git a/zero.Web/Models/ApplicationListModel.cs b/zero.Web/Models/ApplicationListModel.cs
new file mode 100644
index 00000000..0145d355
--- /dev/null
+++ b/zero.Web/Models/ApplicationListModel.cs
@@ -0,0 +1,13 @@
+namespace zero.Web.Models
+{
+ public class ApplicationListModel : ListModel
+ {
+ public string Name { get; set; }
+
+ public string Image { get; set; }
+
+ public string[] Domains { get; set; }
+
+ public bool IsActive { get; set; }
+ }
+}
diff --git a/zero.Web/Resources/Localization/zero.en-us.json b/zero.Web/Resources/Localization/zero.en-us.json
index cd0a2198..c0ccb50a 100644
--- a/zero.Web/Resources/Localization/zero.en-us.json
+++ b/zero.Web/Resources/Localization/zero.en-us.json
@@ -5,6 +5,7 @@
"ui": {
"more": "More",
+ "add": "Add",
"actions": "Actions",
"back": "Go back",
"close": "Close",
@@ -260,6 +261,28 @@
"title": "Pick an icon"
},
+ "application": {
+ "list": "Applications",
+ "name": "Application",
+ "tab_features": "Features",
+ "tab_domains": "Domains",
+ "fields": {
+ "name_text": "Short human-readable name",
+ "fullName": "Full name",
+ "fullName_text": "Full company or product name",
+ "image": "Image",
+ "image_text": "Image of the app (with transparent background), used for all mails, PDFs, ...",
+ "icon": "Icon",
+ "icon_text": "Icon of the app. Simplified image for minor application parts",
+ "email": "Email",
+ "email_text": "Generic contact email",
+ "domains": "Domains",
+ "domains_text": "Select domains for this application",
+ "domains_help": "Valid domain names are: \"example.com\", \"www.example.com\", \"example.com:8080\", or \"https://www.example.com\".",
+ "domains_add": "Add domain"
+ }
+ },
+
"_test": {
"fields": {
"name": "Name",
diff --git a/zero.Web/Sass/Core/_forms.scss b/zero.Web/Sass/Core/_forms.scss
index e837e1e4..444f3835 100644
--- a/zero.Web/Sass/Core/_forms.scss
+++ b/zero.Web/Sass/Core/_forms.scss
@@ -18,7 +18,7 @@ textarea, .ui-native-select
padding: 6px 12px;
line-height: 1.5;
color: var(--color-fg);
- border-radius: 3px;
+ border-radius: var(--radius);
vertical-align: middle;
box-sizing: border-box;
width: 100%;
@@ -55,7 +55,7 @@ textarea
top: 0;
bottom: 0;
width: 100%;
- padding-left: 12px;
+ padding-left: 9px;
-webkit-appearance: none;
border: none;
}
diff --git a/zero.Web/Sass/Modules/Buttons/_button.scss b/zero.Web/Sass/Modules/Buttons/_button.scss
index 89a67ec6..7378f06c 100644
--- a/zero.Web/Sass/Modules/Buttons/_button.scss
+++ b/zero.Web/Sass/Modules/Buttons/_button.scss
@@ -287,7 +287,7 @@ button::-moz-focus-inner
{
width: 50px;
height: 50px;
- line-height: 50px;
+ line-height: 50px !important;
border-radius: var(--radius);
background: var(--color-bg-mid);
color: var(--color-fg);
diff --git a/zero.Web/Startup.cs b/zero.Web/Startup.cs
index 368e0811..6117d968 100644
--- a/zero.Web/Startup.cs
+++ b/zero.Web/Startup.cs
@@ -96,6 +96,9 @@ namespace zero.Web
opts.Spaces.AddSeparator();
opts.Spaces.AddEditor("social", "Social", "Links to social media", "fth-twitter");
+ opts.Features.Add(Features.Wishlist, "Wishlist", "Frontend wishlist for logged-in users");
+ opts.Features.Add(Features.SocialShopping, "Social shopping", "Integrate products into social media portals");
+
opts.Renderers.Add();
opts.Renderers.Add();
diff --git a/zero.Web/ZeroBuilder.cs b/zero.Web/ZeroBuilder.cs
index 23f16404..904df7b7 100644
--- a/zero.Web/ZeroBuilder.cs
+++ b/zero.Web/ZeroBuilder.cs
@@ -33,6 +33,7 @@ namespace zero.Web
opts.Add();
opts.Add();
opts.Add();
+ opts.Add();
});
Services.AddIdentity(opts =>