new API project
This commit is contained in:
+2
-2
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Abstractions;
|
||||
namespace zero.Api.Abstractions;
|
||||
|
||||
public class PickerProvider<T> : IPickerProvider<T> where T : ZeroIdEntity, new()
|
||||
{
|
||||
@@ -83,7 +83,7 @@ public class PickerProvider<T> : IPickerProvider<T> where T : ZeroIdEntity, new(
|
||||
{
|
||||
return new()
|
||||
{
|
||||
HasError = true,
|
||||
//HasError = true,
|
||||
Icon = "fth-alert-circle color-red",
|
||||
Id = id,
|
||||
Name = "@errors.preview.notfound",
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace zero.Api;
|
||||
|
||||
public struct ApiRequestHints
|
||||
{
|
||||
public ApiResponsePreference ResponsePreference { get; set; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Preference for POST + PUT requests
|
||||
/// see https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#75-standard-request-headers
|
||||
/// </summary>
|
||||
public enum ApiResponsePreference
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a minimal repsonse for inserts and updates
|
||||
/// </summary>
|
||||
Minimal = 0,
|
||||
/// <summary>
|
||||
/// Returns status as well as model for inserts and updates
|
||||
/// </summary>
|
||||
Representation = 1
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using zero.Api.Modules.Search;
|
||||
|
||||
namespace zero.Api.Configuration;
|
||||
|
||||
public class ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configure search maps
|
||||
/// </summary>
|
||||
public SearchOptions Search { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace zero.Api.Configuration;
|
||||
|
||||
public static class BackofficeConstants
|
||||
{
|
||||
public static class HttpErrors
|
||||
{
|
||||
public const string NoIdMatchOnUpdate = "The Id as part of the URL does not match the Id of the model";
|
||||
|
||||
public const string IdNotFound = "Could not find persisted model for the given Id";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace zero.Api.Extensions;
|
||||
|
||||
public static class IMapperProfileExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Map data for a zero entity BasicModel
|
||||
/// </summary>
|
||||
public static void MapBasicData<TSource, TDestination>(this IMapperProfile profile, TSource source, TDestination target)
|
||||
where TSource : ZeroEntity
|
||||
where TDestination : BasicModel<TSource>
|
||||
{
|
||||
target.Id = source.Id;
|
||||
target.Name = source.Name;
|
||||
target.Alias = source.Alias;
|
||||
target.Key = source.Key;
|
||||
target.IsActive = source.IsActive;
|
||||
target.CreatedDate = source.CreatedDate;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map data for a zero entity DiplayModel
|
||||
/// </summary>
|
||||
public static void MapDisplayData<TSource, TDestination>(this IMapperProfile profile, TSource source, TDestination target)
|
||||
where TSource : ZeroEntity
|
||||
where TDestination : DisplayModel<TSource>
|
||||
{
|
||||
target.Id = source.Id;
|
||||
target.Name = source.Name;
|
||||
target.Alias = source.Alias;
|
||||
target.Key = source.Key;
|
||||
target.IsActive = source.IsActive;
|
||||
target.CreatedDate = source.CreatedDate;
|
||||
|
||||
target.Sort = source.Sort;
|
||||
target.Hash = source.Hash;
|
||||
target.LastModifiedById = source.LastModifiedById;
|
||||
target.LastModifiedDate = source.LastModifiedDate;
|
||||
target.CreatedById = source.CreatedById;
|
||||
target.LanguageId = source.LanguageId;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map data for a zero entity SaveModel
|
||||
/// </summary>
|
||||
public static void MapSaveData<TSource, TDestination>(this IMapperProfile profile, TSource source, TDestination target)
|
||||
where TSource : SaveModel<TDestination>
|
||||
where TDestination : ZeroEntity
|
||||
{
|
||||
target.Name = source.Name;
|
||||
target.Alias = source.Alias;
|
||||
target.Key = source.Key;
|
||||
target.Sort = source.Sort;
|
||||
target.IsActive = source.IsActive;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
namespace zero.Api.Controllers;
|
||||
|
||||
public class BackofficeFilterAttribute : IActionFilter
|
||||
{
|
||||
@@ -11,7 +11,7 @@ public class BackofficeFilterAttribute : IActionFilter
|
||||
{
|
||||
Type type = context.Controller.GetType();
|
||||
|
||||
if (typeof(ZeroBackofficeController).IsAssignableFrom(type))
|
||||
if (typeof(ZeroApiController).IsAssignableFrom(type))
|
||||
{
|
||||
if (context.HttpContext.Request.Query.TryGetValue(SCOPE_KEY, out var scope))
|
||||
{
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
namespace zero.Api;
|
||||
|
||||
internal class BackofficeJsonSerlializerSettings : JsonSerializerSettings
|
||||
{
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
namespace zero.Api.Controllers;
|
||||
|
||||
public class CanEditAttribute : TypeFilterAttribute
|
||||
{
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
namespace zero.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the request is not cached by the browser
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
namespace zero.Api.Controllers;
|
||||
|
||||
public class ModelStateValidationFilterAttribute : IActionFilter
|
||||
{
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public abstract class BasicModel<T> : ZeroIdEntity where T : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Full name of the entity
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias (non-unique) which can be used in the frontend and URLs
|
||||
/// </summary>
|
||||
public string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A key which can be used to query this entity in code
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity is visible in the frontend
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date of creation
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public abstract class DisplayModel : ZeroIdEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Meta data
|
||||
/// </summary>
|
||||
public DisplayModelConfiguration Configuration { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Permissions for this entity
|
||||
/// </summary>
|
||||
public DisplayModelPermissions Permissions { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
public abstract class DisplayModel<T> : DisplayModel where T : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Full name of the entity
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias (non-unique) which can be used in the frontend and URLs
|
||||
/// </summary>
|
||||
public string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A key which can be used to query this entity in code
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sort order
|
||||
/// </summary>
|
||||
public uint Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity is visible in the frontend
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique hash for this entity (primarily used for routing)
|
||||
/// </summary>
|
||||
public string Hash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Backoffice user who last modified this content
|
||||
/// </summary>
|
||||
public string LastModifiedById { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date of last modification
|
||||
/// </summary>
|
||||
public DateTimeOffset LastModifiedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Backoffice user who created this content
|
||||
/// </summary>
|
||||
public string CreatedById { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date of creation
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Language of the entity
|
||||
/// </summary>
|
||||
public string LanguageId { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class DisplayModelConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Wehther this entity is application aware
|
||||
/// </summary>
|
||||
public bool IsAppAware { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be shared across applications (only for IsAppAware=true)
|
||||
/// </summary>
|
||||
public bool CanBeShared { get; set; }
|
||||
|
||||
public bool IsShared { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The change token maps to a database entity which holds ID and collection of the model to edit
|
||||
/// If these values do not match the entity on save it is rejected
|
||||
/// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60);
|
||||
/// </summary>
|
||||
public string Token { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class DisplayModelPermissions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether an entity of this type can be created
|
||||
/// </summary>
|
||||
public bool CanCreate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether an entity of this type can be created in the shared app space
|
||||
/// </summary>
|
||||
public bool CanCreateShared { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be edited or only viewed
|
||||
/// </summary>
|
||||
public bool CanEdit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be deleted
|
||||
/// </summary>
|
||||
public bool CanDelete { get; set; }
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public class ListQuery<T>
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public enum ListQueryDisplayType
|
||||
{
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using Raven.Client.Documents.Linq;
|
||||
using Raven.Client.Documents.Session;
|
||||
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public static class ListQueryExtensions
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public enum ListQueryOrderType
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public class ListQueryDateRange
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public interface IListSpecificQuery { }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public struct PickerModel
|
||||
public class PickerModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public class PickerPreviewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Icon { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
//public static PickerPreviewModel NotFound(string id) => new()
|
||||
//{
|
||||
// HasError = true,
|
||||
// Icon = "fth-alert-circle color-red",
|
||||
// Id = id,
|
||||
// Name = "@errors.preview.notfound",
|
||||
// Text = "@errors.preview.notfound_text"
|
||||
//};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace zero.Api.Models;
|
||||
|
||||
public abstract class SaveModel<T> : ZeroIdEntity where T : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Full name of the entity
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias (non-unique) which can be used in the frontend and URLs
|
||||
/// </summary>
|
||||
public string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A key which can be used to query this entity in code
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sort order
|
||||
/// </summary>
|
||||
public uint Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity is visible in the frontend
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an item in a tree
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
namespace zero.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The modifier displays a small icon (with hover text) next to the main item icon
|
||||
+35
-14
@@ -1,8 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections;
|
||||
|
||||
namespace zero.Backoffice.Modules.Countries;
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public class CountriesController : ZeroBackofficeApiController
|
||||
public class CountriesController : ZeroApiController
|
||||
{
|
||||
protected ICountryStore Store { get; set; }
|
||||
|
||||
@@ -18,7 +19,7 @@ public class CountriesController : ZeroBackofficeApiController
|
||||
|
||||
[HttpGet("empty")]
|
||||
[ZeroAuthorize(CountryPermissions.Create)]
|
||||
public virtual async Task<ActionResult<CountryDisplay>> Empty()
|
||||
public virtual async Task<ActionResult<DisplayModel>> Empty()
|
||||
{
|
||||
Country model = await Store.Empty();
|
||||
|
||||
@@ -33,24 +34,26 @@ public class CountriesController : ZeroBackofficeApiController
|
||||
|
||||
[HttpGet("pick")]
|
||||
[ZeroAuthorize(CountryPermissions.Read)]
|
||||
public virtual async Task<ActionResult<Paged>> Pick(ListQuery<Country> query)
|
||||
public virtual async Task<ActionResult<Paged>> Pick([FromQuery] ListQuery<Country> query)
|
||||
{
|
||||
query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name);
|
||||
return await Picker.PickFrom(query.Page, query.PageSize, query);
|
||||
Paged<Country> result = await Store.Load<zero_Backoffice_Countries_Listing>(query.Page, query.PageSize, q => q.Filter(query));
|
||||
return Mapper.Map<Country, PickerModel>(result);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("pick")]
|
||||
[HttpGet("pickpreview")]
|
||||
[ZeroAuthorize(CountryPermissions.Read)]
|
||||
public virtual async Task<ActionResult<List<PickerPreviewModel>>> Pick(string[] ids)
|
||||
public virtual async Task<ActionResult<IEnumerable>> Pick([FromQuery] string[] ids)
|
||||
{
|
||||
return await Picker.GetPreviews(ids);
|
||||
Dictionary<string, Country> model = await Store.Load(ids);
|
||||
return Mapper.Map<Country, PickerPreviewModel>(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ZeroAuthorize(CountryPermissions.Read)]
|
||||
public virtual async Task<ActionResult<CountryDisplay>> Get(string id, string changeVector = null)
|
||||
public virtual async Task<ActionResult<DisplayModel>> Get(string id, string changeVector = null)
|
||||
{
|
||||
Country model = await Store.Load(id, changeVector);
|
||||
|
||||
@@ -65,7 +68,7 @@ public class CountriesController : ZeroBackofficeApiController
|
||||
|
||||
[HttpGet("")]
|
||||
[ZeroAuthorize(CountryPermissions.Read)]
|
||||
public virtual async Task<ActionResult<Paged>> Get(ListQuery<Country> query)
|
||||
public virtual async Task<ActionResult<Paged>> Get([FromQuery] ListQuery<Country> query)
|
||||
{
|
||||
query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name);
|
||||
Paged<Country> result = await Store.Load<zero_Backoffice_Countries_Listing>(query.Page, query.PageSize, q => q.Filter(query));
|
||||
@@ -86,7 +89,12 @@ public class CountriesController : ZeroBackofficeApiController
|
||||
return CreatedAtAction(nameof(CountriesController.Get), new { id = model.Id }, mappedResult);
|
||||
}
|
||||
|
||||
return result.WithoutModel();
|
||||
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
|
||||
{
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
return Mapper.Map<Country, CountryDisplay>(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -99,11 +107,24 @@ public class CountriesController : ZeroBackofficeApiController
|
||||
return BadRequest(BackofficeConstants.HttpErrors.NoIdMatchOnUpdate);
|
||||
}
|
||||
|
||||
Country model = Mapper.Map<CountrySave, Country>(updateModel);
|
||||
model.Id = id;
|
||||
Country model = await Store.Load(id);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return BadRequest(BackofficeConstants.HttpErrors.IdNotFound);
|
||||
}
|
||||
|
||||
Mapper.Map(updateModel, model);
|
||||
|
||||
Result<Country> result = await Store.Update(model);
|
||||
|
||||
return result.WithoutModel();
|
||||
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
|
||||
{
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
// TODO add Preference-Applied header, see https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#76-standard-response-headers
|
||||
return Mapper.Map<Country, CountryDisplay>(result);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Modules.Countries;
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public class CountryPermissions : PermissionProvider
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Modules.Countries;
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public class CountryPickerProvider : PickerProvider<Country>
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
|
||||
namespace zero.Backoffice.Modules.Countries;
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public class zero_Backoffice_Countries_Listing : ZeroIndex<Country>
|
||||
{
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
namespace zero.Backoffice.Modules.Countries;
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public class CountryBasic : BasicModel<Country>
|
||||
{
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public bool IsPreferred { get; set; }
|
||||
|
||||
public string Code { get; set; }
|
||||
+1
-5
@@ -1,12 +1,8 @@
|
||||
namespace zero.Backoffice.Modules.Countries;
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public class CountryDisplay : DisplayModel<Country>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IsPreferred { get; set; }
|
||||
|
||||
public string Code { get; set; }
|
||||
|
||||
public string LanguageId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public class CountryMapperProfile : ZeroMapperProfile
|
||||
{
|
||||
public override void Configure(IZeroMapper mapper)
|
||||
{
|
||||
mapper.Define<Country, CountryBasic>((source, ctx) => new(), Map);
|
||||
mapper.Define<Country, CountryDisplay>((source, ctx) => new(), Map);
|
||||
mapper.Define<Country, PickerModel>((source, ctx) => new(), Map);
|
||||
mapper.Define<Country, PickerPreviewModel>((source, ctx) => new(), Map);
|
||||
mapper.Define<CountrySave, Country>((source, ctx) => new(), Map);
|
||||
}
|
||||
|
||||
|
||||
void Map(Country source, CountryBasic target, IZeroMapperContext ctx)
|
||||
{
|
||||
this.MapBasicData(source, target);
|
||||
target.Code = source.Code;
|
||||
target.IsPreferred = source.IsPreferred;
|
||||
}
|
||||
|
||||
void Map(Country source, CountryDisplay target, IZeroMapperContext ctx)
|
||||
{
|
||||
this.MapDisplayData(source, target);
|
||||
target.Code = source.Code;
|
||||
target.IsPreferred = source.IsPreferred;
|
||||
}
|
||||
|
||||
void Map(CountrySave source, Country target, IZeroMapperContext ctx)
|
||||
{
|
||||
this.MapSaveData(source, target);
|
||||
target.Code = source.Code;
|
||||
target.IsPreferred = source.IsPreferred;
|
||||
}
|
||||
|
||||
void Map(Country source, PickerModel target, IZeroMapperContext ctx)
|
||||
{
|
||||
target.Id = source.Id;
|
||||
target.Name = source.Name;
|
||||
target.IsActive = source.IsActive;
|
||||
}
|
||||
|
||||
void Map(Country source, PickerPreviewModel target, IZeroMapperContext ctx)
|
||||
{
|
||||
target.Id = source.Id;
|
||||
target.Name = source.Name;
|
||||
target.Icon = "flag-" + source.Code.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public class CountrySave : SaveModel<Country>
|
||||
{
|
||||
public bool IsPreferred { get; set; }
|
||||
|
||||
public string Code { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Api.Modules.Countries;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddZeroBackofficeCountries(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IPickerProvider<Country>, CountryPickerProvider>();
|
||||
services.AddSingleton<IPermissionProvider, CountryPermissions>();
|
||||
services.AddSingleton<IMapperProfile, CountryMapperProfile>();
|
||||
|
||||
services.Configure<RavenOptions>(opts =>
|
||||
{
|
||||
opts.Indexes.Add<zero_Backoffice_Countries_Listing>();
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using Raven.Client.Documents.Linq;
|
||||
using Raven.Client.Documents.Session;
|
||||
|
||||
namespace zero.Backoffice.Modules.Pages;
|
||||
namespace zero.Api.Modules.Pages;
|
||||
|
||||
public class PageTreeService : IPageTreeService
|
||||
{
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using Raven.Client.Documents.Queries;
|
||||
using Raven.Client.Documents.Session;
|
||||
|
||||
namespace zero.Backoffice.Modules.Search;
|
||||
namespace zero.Api.Modules.Search;
|
||||
|
||||
public class BackofficeSearchService : IBackofficeSearchService
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
//using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
//namespace zero.Backoffice.Modules;
|
||||
//namespace zero.Api.Modules;
|
||||
|
||||
//[ZeroAuthorize]
|
||||
//public class SearchController : BackofficeController
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Raven.Client.Documents;
|
||||
|
||||
namespace zero.Backoffice.Modules.Search;
|
||||
namespace zero.Api.Modules.Search;
|
||||
|
||||
public class SearchIndexMap
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Modules.Search;
|
||||
namespace zero.Api.Modules.Search;
|
||||
|
||||
public class SearchOptions : List<SearchIndexMap>
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace zero.Backoffice.Modules.Search;
|
||||
namespace zero.Api.Modules.Search;
|
||||
|
||||
public class SearchResult
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Raven.Client.Documents;
|
||||
|
||||
namespace zero.Backoffice.Modules.Search;
|
||||
namespace zero.Api.Modules.Search;
|
||||
|
||||
public class zero_Backoffice_Search : ZeroJavascriptIndex
|
||||
{
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using zero.Api.Modules.Countries;
|
||||
using zero.Api.Modules.Pages;
|
||||
using zero.Api.Modules.Search;
|
||||
|
||||
namespace zero.Api.Modules;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddZeroBackofficeModules(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddZeroBackofficeCountries();
|
||||
|
||||
services.AddScoped<IPageTreeService, PageTreeService>();
|
||||
services.AddScoped<IBackofficeSearchService, BackofficeSearchService>();
|
||||
|
||||
//services.Configure<RavenOptions>(opts =>
|
||||
//{
|
||||
// opts.Indexes.Add<zero_Backoffice_Search>();
|
||||
//});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero.Api;
|
||||
|
||||
public class ZeroApiPlugin : ZeroPlugin
|
||||
{
|
||||
internal Action<IServiceCollection, IConfiguration> PostConfigureServices = null;
|
||||
|
||||
|
||||
public ZeroApiPlugin()
|
||||
{
|
||||
Options.Name = "zero.Api";
|
||||
}
|
||||
|
||||
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<ApiOptions>().Bind(configuration.GetSection("Zero:Api")).Configure<IWebHostEnvironment>(ConfigureOptions);
|
||||
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, ZeroBackofficeMvcOptions>());
|
||||
|
||||
//Mvc.AddNewtonsoftJson(x =>
|
||||
//{
|
||||
// // TODO this shall only be configurated for backoffice controllers
|
||||
// BackofficeJsonSerlializerSettings.Setup(x.SerializerSettings);
|
||||
//});
|
||||
|
||||
services.AddScoped<IZeroMapper, ZeroMapper>();
|
||||
|
||||
services.AddZeroBackofficeModules(configuration);
|
||||
|
||||
//services.AddTransient<ISectionsApi, SectionsApi>();
|
||||
//services.AddTransient<ISettingsApi, SettingsApi>();
|
||||
|
||||
//services.AddScoped<IBackofficeSearchService, BackofficeSearchService>();
|
||||
|
||||
|
||||
PostConfigureServices?.Invoke(services, configuration);
|
||||
}
|
||||
|
||||
|
||||
protected void ConfigureOptions(ApiOptions options, IWebHostEnvironment env)
|
||||
{
|
||||
options.Search.Enabled = true;
|
||||
//Map<Page>().Display((x, res, opts) =>
|
||||
//{
|
||||
// PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias);
|
||||
// if (pageType != null)
|
||||
// {
|
||||
// res.Icon = pageType.Icon;
|
||||
// }
|
||||
// res.Url = "/pages/edit/" + x.Id;
|
||||
//});
|
||||
//Map<MediaFolder>("fth-image");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Api;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static ZeroBuilder AddApi<T>(this ZeroBuilder builder) where T : ZeroApiPlugin, IZeroPlugin, new()
|
||||
{
|
||||
return builder.AddApi<T>(_ => { });
|
||||
}
|
||||
|
||||
public static ZeroBuilder AddApi(this ZeroBuilder builder)
|
||||
{
|
||||
return builder.AddApi<ZeroApiPlugin>();
|
||||
}
|
||||
|
||||
public static ZeroBuilder AddApi<T>(this ZeroBuilder builder, Action<ApiOptions> options) where T : ZeroApiPlugin, IZeroPlugin, new()
|
||||
{
|
||||
return builder.AddPlugin(services =>
|
||||
{
|
||||
T plugin = new();
|
||||
plugin.PostConfigureServices = (services, configuration) =>
|
||||
{
|
||||
services.Configure<ApiOptions>(opts => options(opts));
|
||||
};
|
||||
|
||||
return plugin;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
|
||||
global using zero.Applications;
|
||||
global using zero.Architecture;
|
||||
global using zero.Stores;
|
||||
global using zero.Configuration;
|
||||
global using zero.Context;
|
||||
global using zero.Extensions;
|
||||
global using zero.FileStorage;
|
||||
global using zero.Identity;
|
||||
global using zero.Localization;
|
||||
global using zero.Mapper;
|
||||
global using zero.Media;
|
||||
global using zero.Models;
|
||||
global using zero.Pages;
|
||||
global using zero.Persistence;
|
||||
global using zero.Routing;
|
||||
global using zero.Utils;
|
||||
|
||||
global using zero.Api.Configuration;
|
||||
global using zero.Api.Controllers;
|
||||
global using zero.Api.Models;
|
||||
global using zero.Api.Modules;
|
||||
global using zero.Api.Abstractions;
|
||||
global using zero.Api.Extensions;
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ZeroAuthorize]
|
||||
[DisableBrowserCache]
|
||||
//[ServiceFilter(typeof(ModelStateValidationFilterAttribute))]
|
||||
//[ServiceFilter(typeof(BackofficeFilterAttribute))]
|
||||
public abstract class ZeroApiController : ControllerBase
|
||||
{
|
||||
IZeroMapper _mapper;
|
||||
protected IZeroMapper Mapper => _mapper ?? (_mapper = HttpContext?.RequestServices?.GetService<IZeroMapper>());
|
||||
|
||||
|
||||
protected ApiRequestHints Hints { get; set; } = new();
|
||||
}
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
namespace zero.Api.Controllers;
|
||||
|
||||
public class ZeroBackofficeControllerModelConvention : IApplicationModelConvention
|
||||
{
|
||||
readonly AttributeRouteModel RouteModel;
|
||||
|
||||
readonly Type BaseClass = typeof(ZeroBackofficeController);
|
||||
readonly Type BaseClass = typeof(ZeroApiController);
|
||||
|
||||
|
||||
public ZeroBackofficeControllerModelConvention(string backofficePath)
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero.Api;
|
||||
|
||||
internal class ZeroBackofficeMvcOptions : IConfigureOptions<MvcOptions>
|
||||
{
|
||||
IZeroOptions Options { get; set; }
|
||||
|
||||
public ZeroBackofficeMvcOptions(IZeroOptions options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public void Configure(MvcOptions options)
|
||||
{
|
||||
options.Conventions.Add(new ZeroBackofficeControllerModelConvention(Options.ZeroPath));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>zero.Api</PackageId>
|
||||
<Version>0.1.0</Version>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Modules_legacy\**" />
|
||||
<EmbeddedResource Remove="Modules_legacy\**" />
|
||||
<None Remove="Modules_legacy\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\zero.Core\zero.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -5,5 +5,7 @@ public static class BackofficeConstants
|
||||
public static class HttpErrors
|
||||
{
|
||||
public const string NoIdMatchOnUpdate = "The Id as part of the URL does not match the Id of the model";
|
||||
|
||||
public const string IdNotFound = "Could not find persisted model for the given Id";
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace zero.Backoffice.Configuration;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class BackofficeMaps
|
||||
{
|
||||
public MapperConfiguration MapperConfiguration { get; internal set; }
|
||||
|
||||
internal List<Action<IMapperConfigurationExpression>> Expressions { get; }
|
||||
|
||||
|
||||
public void Configure(Action<IMapperConfigurationExpression> expression)
|
||||
{
|
||||
Expressions.Add(expression);
|
||||
}
|
||||
|
||||
|
||||
internal void Run(IMapperConfigurationExpression mapperConfig)
|
||||
{
|
||||
foreach (var expression in Expressions)
|
||||
{
|
||||
expression(mapperConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using AutoMapper;
|
||||
using System.Linq.Expressions;
|
||||
using zero.Backoffice.Modules.Search;
|
||||
|
||||
namespace zero.Backoffice.Configuration;
|
||||
namespace zero.Backoffice.Configuration;
|
||||
|
||||
public class BackofficeOptions
|
||||
{
|
||||
@@ -21,11 +17,6 @@ public class BackofficeOptions
|
||||
/// </summary>
|
||||
public ExternalServicesOptions ExternalServices { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Configure search maps
|
||||
/// </summary>
|
||||
public SearchOptions Search { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the vite development server
|
||||
/// </summary>
|
||||
@@ -40,9 +31,4 @@ public class BackofficeOptions
|
||||
/// Language ISO codes which are supported by the zero backoffice
|
||||
/// </summary>
|
||||
public string[] SupportedLanguages { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for AutoMapper
|
||||
/// </summary>
|
||||
public BackofficeMaps Mapper { get; private set; } = new();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
|
||||
[ApiController]
|
||||
//[ServiceFilter(typeof(ModelStateValidationFilterAttribute))]
|
||||
//[ServiceFilter(typeof(BackofficeFilterAttribute))]
|
||||
public abstract class ZeroBackofficeApiController : ZeroBackofficeController
|
||||
{
|
||||
protected IMapper Mapper { get; private set; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
|
||||
[ZeroAuthorize]
|
||||
[DisableBrowserCache]
|
||||
public abstract class ZeroBackofficeController : ControllerBase
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,81 +1,16 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using System.IO;
|
||||
using zero.Api.Controllers;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
|
||||
public static class ZeroBackofficeControllerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Transform entities to a preview list
|
||||
/// </summary>
|
||||
public static List<PickerPreviewModel> TransformToPreviewModels<T>(this ZeroBackofficeController controller, Dictionary<string, T> items, Action<T, PickerPreviewModel> transform = null) where T : ZeroIdEntity
|
||||
{
|
||||
List<PickerPreviewModel> previews = new();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Value == null)
|
||||
{
|
||||
previews.Add(new PickerPreviewModel()
|
||||
{
|
||||
HasError = true,
|
||||
Icon = "fth-alert-circle color-red",
|
||||
Id = item.Key,
|
||||
Name = "@errors.preview.notfound",
|
||||
Text = "@errors.preview.notfound_text"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
PickerPreviewModel model = new() { Id = item.Value.Id };
|
||||
|
||||
if (item.Value is ZeroEntity)
|
||||
{
|
||||
model.Name = (item.Value as ZeroEntity).Name;
|
||||
}
|
||||
|
||||
transform?.Invoke(item.Value, model);
|
||||
previews.Add(model);
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Transform entities to a select list
|
||||
/// </summary>
|
||||
public static List<PickerModel> TransformToSelectModels<T>(this ZeroBackofficeController controller, IEnumerable<T> enumerable, Action<T, PickerModel> transform = null) where T : ZeroIdEntity
|
||||
{
|
||||
List<PickerModel> items = new();
|
||||
|
||||
foreach (T item in enumerable)
|
||||
{
|
||||
PickerModel model = new()
|
||||
{
|
||||
Id = item.Id
|
||||
};
|
||||
|
||||
if (item is ZeroEntity)
|
||||
{
|
||||
model.Name = (item as ZeroEntity).Name;
|
||||
model.IsActive = (item as ZeroEntity).IsActive;
|
||||
}
|
||||
|
||||
transform?.Invoke(item, model);
|
||||
|
||||
items.Add(model);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Provides a file stream for download in the browser
|
||||
/// </summary>
|
||||
public static IActionResult DownloadFile(this ZeroBackofficeController controller, Stream stream, string filename)
|
||||
public static IActionResult DownloadFile(this ZeroApiController controller, Stream stream, string filename)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
@@ -102,7 +37,7 @@ public static class ZeroBackofficeControllerExtensions
|
||||
/// <summary>
|
||||
/// Provides a file stream to the browser
|
||||
/// </summary>
|
||||
public static IActionResult File(this ZeroBackofficeController controller, FileStorage.FileResult file)
|
||||
public static IActionResult File(this ZeroApiController controller, FileStorage.FileResult file)
|
||||
{
|
||||
if (file == null)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using zero.Api.Controllers;
|
||||
|
||||
namespace zero.Backoffice.Controllers;
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace zero.Backoffice.Mapper;
|
||||
|
||||
public static class MapperExtensions
|
||||
{
|
||||
public static Paged<TDestination> Map<TSource, TDestination>(this IMapper mapper, Paged<TSource> source)
|
||||
{
|
||||
return source.MapTo(srcItem => mapper.Map<TSource, TDestination>(srcItem));
|
||||
}
|
||||
|
||||
public static Result<TDestination> Map<TSource, TDestination>(this IMapper mapper, Result<TSource> source)
|
||||
{
|
||||
TDestination model = mapper.Map<TSource, TDestination>(source.Model);
|
||||
return source.ConvertTo(model);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Backoffice.Mapper
|
||||
{
|
||||
public class ZeroMapper : AutoMapper.Mapper
|
||||
{
|
||||
public ZeroMapper(AutoMapper.IConfigurationProvider provider) : base(provider)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace zero.Backoffice.Mapper;
|
||||
|
||||
public class ZeroMapperProfile : Profile
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
|
||||
public abstract class BasicModel<T> : ZeroIdEntity where T : ZeroIdEntity
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
|
||||
public abstract class DisplayModel<T> : ZeroIdEntity where T : ZeroIdEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Meta data
|
||||
/// </summary>
|
||||
public DisplayModelMeta Meta { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Permissions for this entity
|
||||
/// </summary>
|
||||
public DisplayModelPermissions Permissions { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
public class DisplayModelMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// Wehther this entity is application aware
|
||||
/// </summary>
|
||||
public bool IsAppAware { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be shared across applications (only for IsAppAware=true)
|
||||
/// </summary>
|
||||
public bool CanBeShared { get; set; }
|
||||
|
||||
public bool IsShared { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The change token maps to a database entity which holds ID and collection of the model to edit
|
||||
/// If these values do not match the entity on save it is rejected
|
||||
/// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60);
|
||||
/// </summary>
|
||||
public string Token { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class DisplayModelPermissions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether an entity of this type can be created
|
||||
/// </summary>
|
||||
public bool CanCreate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether an entity of this type can be created in the shared app space
|
||||
/// </summary>
|
||||
public bool CanCreateShared { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be edited or only viewed
|
||||
/// </summary>
|
||||
public bool CanEdit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can be deleted
|
||||
/// </summary>
|
||||
public bool CanDelete { get; set; }
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
|
||||
public struct PickerPreviewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Icon { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool HasError { get; set; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace zero.Backoffice.Models;
|
||||
|
||||
public abstract class SaveModel<T> : ZeroIdEntity where T : ZeroIdEntity
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace zero.Backoffice.Modules.Countries;
|
||||
|
||||
public class CountryMapperProfile : ZeroMapperProfile
|
||||
{
|
||||
public CountryMapperProfile()
|
||||
{
|
||||
CreateMap<Country, CountryBasic>();
|
||||
CreateMap<Country, CountryDisplay>();
|
||||
CreateMap<CountrySave, Country>();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace zero.Backoffice.Modules.Countries;
|
||||
|
||||
public class CountrySave : SaveModel<Country>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IsPreferred { get; set; }
|
||||
|
||||
public string Code { get; set; }
|
||||
|
||||
public string LanguageId { get; set; }
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using zero.Backoffice.Modules.Countries;
|
||||
using zero.Backoffice.Modules.Pages;
|
||||
using zero.Backoffice.Modules.Search;
|
||||
|
||||
namespace zero.Backoffice.Modules;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddZeroBackofficeModules(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddScoped<IPageTreeService, PageTreeService>();
|
||||
services.AddScoped<IBackofficeSearchService, BackofficeSearchService>();
|
||||
|
||||
services.AddScoped<IPickerProvider<Country>, CountryPickerProvider>();
|
||||
services.AddSingleton<IPermissionProvider, CountryPermissions>();
|
||||
|
||||
services.Configure<RavenOptions>(opts =>
|
||||
{
|
||||
opts.Indexes.Add<zero_Backoffice_Countries_Listing>();
|
||||
opts.Indexes.Add<zero_Backoffice_Search>();
|
||||
});
|
||||
|
||||
services.Configure<BackofficeOptions>(opts =>
|
||||
{
|
||||
opts.Mapper.Configure(maps =>
|
||||
{
|
||||
maps.AddProfile<CountryMapperProfile>();
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.IO;
|
||||
using zero.Backoffice.Modules;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
@@ -25,7 +20,6 @@ public class ZeroBackofficePlugin : ZeroPlugin
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<BackofficeOptions>().Bind(configuration.GetSection("Zero:Backoffice")).Configure<IWebHostEnvironment>(ConfigureOptions);
|
||||
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, ZeroBackofficeMvcOptions>());
|
||||
services.AddHostedService<ZeroDevService>();
|
||||
services.AddTransient<IZeroVue, ZeroVue>();
|
||||
|
||||
@@ -36,7 +30,6 @@ public class ZeroBackofficePlugin : ZeroPlugin
|
||||
//});
|
||||
|
||||
services.AddZeroBackofficeUIComposition();
|
||||
services.AddZeroBackofficeModules(configuration);
|
||||
|
||||
//services.AddTransient<ISectionsApi, SectionsApi>();
|
||||
//services.AddTransient<ISettingsApi, SettingsApi>();
|
||||
@@ -50,7 +43,6 @@ public class ZeroBackofficePlugin : ZeroPlugin
|
||||
|
||||
protected void ConfigureOptions(BackofficeOptions options, IWebHostEnvironment env)
|
||||
{
|
||||
options.Search.Enabled = true;
|
||||
options.DevServer.WorkingDirectory = Path.Combine(env.ContentRootPath, "..", "Zero.Web.UI", "App");
|
||||
|
||||
options.IconSets.Add(new BackofficeIconSet()
|
||||
@@ -63,16 +55,5 @@ public class ZeroBackofficePlugin : ZeroPlugin
|
||||
|
||||
options.SupportedLanguages = new string[2] { "en-US", "de-DE" };
|
||||
options.DefaultLanguage = options.SupportedLanguages[0];
|
||||
|
||||
//Map<Page>().Display((x, res, opts) =>
|
||||
//{
|
||||
// PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias);
|
||||
// if (pageType != null)
|
||||
// {
|
||||
// res.Icon = pageType.Icon;
|
||||
// }
|
||||
// res.Url = "/pages/edit/" + x.Id;
|
||||
//});
|
||||
//Map<MediaFolder>("fth-image");
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,9 @@ global using zero.Pages;
|
||||
global using zero.Persistence;
|
||||
global using zero.Routing;
|
||||
global using zero.Utils;
|
||||
global using zero.Mapper;
|
||||
|
||||
global using zero.Backoffice.Configuration;
|
||||
global using zero.Backoffice.Controllers;
|
||||
global using zero.Backoffice.Models;
|
||||
global using zero.Backoffice.Modules;
|
||||
global using zero.Backoffice.UIComposition;
|
||||
global using zero.Backoffice.DevServer;
|
||||
global using zero.Backoffice.Mapper;
|
||||
global using zero.Backoffice.Abstractions;
|
||||
global using zero.Backoffice.DevServer;
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using zero.Api.Controllers;
|
||||
|
||||
namespace zero.Backoffice;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>zero.Backoffice</PackageId>
|
||||
@@ -18,15 +18,8 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\zero.Core\zero.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Modules\Search\" />
|
||||
<ProjectReference Include="..\zero.Api\zero.Api.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace zero.Mapper;
|
||||
|
||||
public static class MapperExtensions
|
||||
{
|
||||
//public static TDestination Register<TSource, TDestination>(this IZeroMapper mapper, Action<TSource, TDestination, IZeroMapperContext> map) where TDestination : class, new()
|
||||
//{
|
||||
|
||||
//}
|
||||
|
||||
|
||||
//public static TDestination Register<TSource, TDestination>(this IZeroMapper mapper, Action<TSource, TDestination, IZeroMapperContext> map, TDestination destination)
|
||||
//{
|
||||
|
||||
//}
|
||||
|
||||
|
||||
public static TDestination Map<TSource, TDestination>(this IZeroMapper mapper, TSource source, TDestination destination)
|
||||
{
|
||||
return mapper.Map(source, typeof(TSource), destination);
|
||||
}
|
||||
|
||||
public static TDestination Map<TSource, TDestination>(this IZeroMapper mapper, TSource source)
|
||||
{
|
||||
return mapper.Map<TDestination>(source, typeof(TSource), default);
|
||||
}
|
||||
|
||||
public static Paged<TDestination> Map<TSource, TDestination>(this IZeroMapper mapper, Paged<TSource> source)
|
||||
{
|
||||
return source.MapTo(srcItem => mapper.Map<TSource, TDestination>(srcItem));
|
||||
}
|
||||
|
||||
public static Result<TDestination> Map<TSource, TDestination>(this IZeroMapper mapper, Result<TSource> source)
|
||||
{
|
||||
TDestination model = mapper.Map<TSource, TDestination>(source.Model);
|
||||
return source.ConvertTo(model);
|
||||
}
|
||||
|
||||
|
||||
public static Dictionary<string, TDestination> Map<TSource, TDestination>(this IZeroMapper mapper, Dictionary<string, TSource> source)
|
||||
{
|
||||
Dictionary<string, TDestination> model = new();
|
||||
|
||||
foreach ((string key, TSource sourceItem) in source)
|
||||
{
|
||||
model.Add(key, sourceItem == null ? default : mapper.Map<TSource, TDestination>(sourceItem));
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Mapper;
|
||||
|
||||
internal static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddZeroMapper(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IZeroMapper, ZeroMapper>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace zero.Mapper;
|
||||
|
||||
public class ZeroMapper : IZeroMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Concurrent cache for all ctor definitions
|
||||
/// </summary>
|
||||
protected ConcurrentDictionary<Type, Dictionary<Type, Action<object, object, IZeroMapperContext>>> MapDefinitions { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Concurrent cache for all constructor definitions
|
||||
/// </summary>
|
||||
protected ConcurrentDictionary<Type, Dictionary<Type, Func<object, IZeroMapperContext, object>>> ConstructorDefinitions { get; private set; } = new();
|
||||
|
||||
|
||||
public ZeroMapper(IEnumerable<IMapperProfile> profiles)
|
||||
{
|
||||
foreach (IMapperProfile profile in profiles)
|
||||
{
|
||||
profile.Configure(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Define<TSource, TDestination>(Func<TSource, IZeroMapperContext, TDestination> ctor, Action<TSource, TDestination, IZeroMapperContext> map)
|
||||
{
|
||||
Type sourceType = typeof(TSource);
|
||||
Type destinationType = typeof(TDestination);
|
||||
|
||||
var sourceMaps = MapDefinitions.GetOrAdd(sourceType, _ => new());
|
||||
var sourceCtors = ConstructorDefinitions.GetOrAdd(sourceType, _ => new());
|
||||
|
||||
sourceCtors[destinationType] = (source, ctx) => ctor((TSource)source, ctx);
|
||||
sourceMaps[destinationType] = (source, destination, ctx) => map((TSource)source, (TDestination)destination, ctx);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public TDestination Map<TDestination>(object source, Type sourceType, TDestination destination = default)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
Type destinationType = typeof(TDestination);
|
||||
ZeroMapperContext mapperContext = new(this);
|
||||
|
||||
var constructor = GetConstructor(sourceType, destinationType);
|
||||
var map = GetMap(sourceType, destinationType);
|
||||
|
||||
if (constructor != null && map != null)
|
||||
{
|
||||
destination ??= (TDestination)constructor(source, mapperContext);
|
||||
|
||||
map(source, destination, mapperContext);
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
// TODO enumerables
|
||||
|
||||
throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {destinationType.FullName}.");
|
||||
}
|
||||
|
||||
|
||||
protected virtual Func<object, IZeroMapperContext, object> GetConstructor(Type sourceType, Type targetType)
|
||||
{
|
||||
if (ConstructorDefinitions.TryGetValue(sourceType, out var sourceCtor) && sourceCtor.TryGetValue(targetType, out var ctor))
|
||||
{
|
||||
return ctor;
|
||||
}
|
||||
|
||||
// we *may* run this more than once but it does not matter
|
||||
|
||||
ctor = null;
|
||||
foreach (var (stype, sctors) in ConstructorDefinitions)
|
||||
{
|
||||
if (!stype.IsAssignableFrom(sourceType)) continue;
|
||||
if (!sctors.TryGetValue(targetType, out ctor)) continue;
|
||||
|
||||
sourceCtor = sctors;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ctor == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ConstructorDefinitions.AddOrUpdate(sourceType, sourceCtor, (k, v) =>
|
||||
{
|
||||
// Add missing constructors
|
||||
foreach (var c in sourceCtor)
|
||||
{
|
||||
if (!v.ContainsKey(c.Key))
|
||||
{
|
||||
v.Add(c.Key, c.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
});
|
||||
|
||||
|
||||
return ctor;
|
||||
}
|
||||
|
||||
protected virtual Action<object, object, IZeroMapperContext> GetMap(Type sourceType, Type targetType)
|
||||
{
|
||||
if (MapDefinitions.TryGetValue(sourceType, out var sourceMap) && sourceMap.TryGetValue(targetType, out var map))
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
// we *may* run this more than once but it does not matter
|
||||
|
||||
map = null;
|
||||
foreach (var (stype, smap) in MapDefinitions)
|
||||
{
|
||||
if (!stype.IsAssignableFrom(sourceType)) continue;
|
||||
|
||||
// TODO: consider looking for assignable types for target too?
|
||||
if (!smap.TryGetValue(targetType, out map)) continue;
|
||||
|
||||
sourceMap = smap;
|
||||
break;
|
||||
}
|
||||
|
||||
if (map == null) return null;
|
||||
|
||||
if (MapDefinitions.ContainsKey(sourceType))
|
||||
{
|
||||
foreach (var m in sourceMap)
|
||||
{
|
||||
if (!MapDefinitions[sourceType].TryGetValue(m.Key, out _))
|
||||
{
|
||||
MapDefinitions[sourceType].Add(m.Key, m.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MapDefinitions[sourceType] = sourceMap;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a new mapping
|
||||
/// </summary>
|
||||
void Define<TSource, TDestination>(Func<TSource, IZeroMapperContext, TDestination> ctor, Action<TSource, TDestination, IZeroMapperContext> map);
|
||||
|
||||
/// <summary>
|
||||
/// Map a source type to the destination type
|
||||
/// </summary>
|
||||
TDestination Map<TDestination>(object source, Type sourceType, TDestination destination = default);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace zero.Mapper;
|
||||
|
||||
public class ZeroMapperContext : IZeroMapperContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IZeroMapper Mapper { get; }
|
||||
|
||||
|
||||
public ZeroMapperContext(IZeroMapper mapper)
|
||||
{
|
||||
Mapper = mapper;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroMapperContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Access to the runtime mapper
|
||||
/// </summary>
|
||||
IZeroMapper Mapper { get; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace zero.Mapper;
|
||||
|
||||
public abstract class ZeroMapperProfile : IMapperProfile
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract void Configure(IZeroMapper mapper);
|
||||
}
|
||||
|
||||
|
||||
public interface IMapperProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// Configure maps for this profile
|
||||
/// </summary>
|
||||
void Configure(IZeroMapper mapper);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ global using zero.FileStorage;
|
||||
global using zero.Identity;
|
||||
global using zero.Localization;
|
||||
global using zero.Mails;
|
||||
global using zero.Mapper;
|
||||
global using zero.Media;
|
||||
global using zero.Models;
|
||||
global using zero.Pages;
|
||||
|
||||
@@ -50,6 +50,7 @@ public class ZeroBuilder
|
||||
services.AddZeroRendering();
|
||||
services.AddZeroRouting(Configuration);
|
||||
services.AddZeroStores();
|
||||
services.AddZeroMapper();
|
||||
|
||||
//if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1")
|
||||
//{
|
||||
|
||||
@@ -5,7 +5,7 @@ VisualStudioVersion = 17.0.31912.275
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Commerce", "plugins\zero.Commerce\zero.Commerce.csproj", "{63A8AD15-15F0-4555-BD62-C38B4465FFC1}"
|
||||
EndProject
|
||||
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "zero.Backoffice.UI", "zero.Backoffice.UI\", "{8CE1A69A-35DB-4748-AE1C-86F5BC2E11D4}"
|
||||
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "zero.Web.UI", "zero.Backoffice.UI\", "{8CE1A69A-35DB-4748-AE1C-86F5BC2E11D4}"
|
||||
ProjectSection(WebsiteProperties) = preProject
|
||||
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5"
|
||||
Debug.AspNetCompiler.VirtualPath = "/localhost_56763"
|
||||
@@ -43,7 +43,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Backoffice", "zero.Bac
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Core", "zero.Core\zero.Core.csproj", "{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Web", "zero.Web\zero.Web.csproj", "{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Web", "zero.Web\zero.Web.csproj", "{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Api", "zero.Api\zero.Api.csproj", "{C0442589-3915-43B5-9F78-2D5F9E8D8C94}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -79,6 +81,10 @@ Global
|
||||
{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user