Browse Source

API improvements.

pull/363/head
Sebastian Stehle 7 years ago
parent
commit
00873b6d4d
  1. 45
      src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs
  2. 18
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  3. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  4. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  5. 4
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  6. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
  7. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  8. 13
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  9. 10
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs
  10. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  11. 4
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  12. 3
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  13. 24
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  14. 6
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  15. 1
      src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
  16. 16
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  17. 25
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  18. 15
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  19. 8
      src/Squidex/app/features/content/pages/content/content-page.component.html
  20. 28
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  21. 12
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  22. 2
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  23. 3
      src/Squidex/app/framework/state.ts
  24. 19
      src/Squidex/app/framework/utils/hateos.ts
  25. 2
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  26. 15
      src/Squidex/app/shared/services/apps.service.ts
  27. 2
      src/Squidex/app/shared/services/assets.service.ts
  28. 62
      src/Squidex/app/shared/services/contents.service.spec.ts
  29. 54
      src/Squidex/app/shared/services/contents.service.ts
  30. 2
      src/Squidex/app/shared/services/contributors.service.ts
  31. 8
      src/Squidex/app/shared/services/rules.service.ts
  32. 19
      src/Squidex/app/shared/state/contents.state.ts
  33. 24
      src/Squidex/app/shared/state/rules.state.ts
  34. 2
      src/Squidex/app/shared/state/ui.state.spec.ts

45
src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -26,20 +27,30 @@ namespace Squidex.Areas.Api.Config.Swagger
{
var operation = context.OperationDescription.Operation;
var returnsDescription = await context.MethodInfo.GetXmlDocumentationTagAsync("returns") ?? string.Empty;
var returnsDescription = await context.MethodInfo.GetXmlDocumentationTagAsync("returns");
foreach (Match match in ResponseRegex.Matches(returnsDescription))
if (!string.IsNullOrWhiteSpace(returnsDescription))
{
var statusCode = match.Groups["Code"].Value;
if (!operation.Responses.TryGetValue(statusCode, out var response))
foreach (Match match in ResponseRegex.Matches(returnsDescription))
{
response = new SwaggerResponse();
var statusCode = match.Groups["Code"].Value;
operation.Responses[statusCode] = response;
}
if (!operation.Responses.TryGetValue(statusCode, out var response))
{
response = new SwaggerResponse();
operation.Responses[statusCode] = response;
}
response.Description = match.Groups["Description"].Value;
var description = match.Groups["Description"].Value;
if (description.Contains("=>"))
{
throw new InvalidOperationException("Description not formatted correcly.");
}
response.Description = description;
}
}
await AddInternalErrorResponseAsync(context, operation);
@ -51,9 +62,19 @@ namespace Squidex.Areas.Api.Config.Swagger
private static async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation)
{
var errorSchema = await context.SchemaGenerator.GetErrorDtoSchemaAsync(context.SchemaResolver);
if (!operation.Responses.ContainsKey("500"))
{
operation.AddResponse("500", "Operation failed", await context.SchemaGenerator.GetErrorDtoSchemaAsync(context.SchemaResolver));
operation.AddResponse("500", "Operation failed", errorSchema);
}
foreach (var (code, response) in operation.Responses)
{
if (code != "404" && code.StartsWith("4", StringComparison.OrdinalIgnoreCase) && response.Schema == null)
{
response.Schema = errorSchema;
}
}
}
@ -61,7 +82,9 @@ namespace Squidex.Areas.Api.Config.Swagger
{
foreach (var (code, response) in operation.Responses.ToList())
{
if (string.IsNullOrWhiteSpace(response.Description) || response.Description?.Contains("=>") == true)
if (string.IsNullOrWhiteSpace(response.Description) ||
response.Description?.Contains("=>") == true ||
response.Description?.Contains("=>") == true)
{
operation.Responses.Remove(code);
}

18
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -70,7 +70,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppClientsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostClient(string app, [FromBody] CreateClientDto request)
@ -86,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// Updates an app client.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="clientId">The id of the client that must be updated.</param>
/// <param name="id">The id of the client that must be updated.</param>
/// <param name="request">Client object that needs to be updated.</param>
/// <returns>
/// 200 => Client updated.
@ -97,14 +96,13 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// Only the display name can be changed, create a new client if necessary.
/// </remarks>
[HttpPut]
[Route("apps/{app}/clients/{clientId}/")]
[Route("apps/{app}/clients/{id}/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppClientsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateClientDto request)
public async Task<IActionResult> PutClient(string app, string id, [FromBody] UpdateClientDto request)
{
var command = request.ToCommand(clientId);
var command = request.ToCommand(id);
var response = await InvokeCommandAsync(command);
@ -115,7 +113,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// Revoke an app client.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="clientId">The id of the client that must be deleted.</param>
/// <param name="id">The id of the client that must be deleted.</param>
/// <returns>
/// 200 => Client revoked.
/// 404 => Client or app not found.
@ -124,13 +122,13 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// The application that uses this client credentials cannot access the API after it has been revoked.
/// </remarks>
[HttpDelete]
[Route("apps/{app}/clients/{clientId}/")]
[Route("apps/{app}/clients/{id}/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteClient(string app, string clientId)
public async Task<IActionResult> DeleteClient(string app, string id)
{
var command = new RevokeClient { Id = clientId };
var command = new RevokeClient { Id = id };
var response = await InvokeCommandAsync(command);

2
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -68,7 +68,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppContributorsAssign)]
[ApiCosts(1)]
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignContributorDto request)
@ -103,7 +102,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpDelete]
[Route("apps/{app}/contributors/{id}/")]
[ProducesResponseType(typeof(ContributorsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppContributorsRevoke)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteContributor(string app, string id)

2
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -65,7 +65,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguagesDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppLanguagesCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostLanguage(string app, [FromBody] AddLanguageDto request)
@ -91,7 +90,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPut]
[Route("apps/{app}/languages/{language}/")]
[ProducesResponseType(typeof(AppLanguagesDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppLanguagesUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutLanguage(string app, string language, [FromBody] UpdateLanguageDto request)

4
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -67,7 +67,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(PatternsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppPatternsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request)
@ -86,14 +85,13 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="id">The id of the pattern to be updated.</param>
/// <param name="request">Pattern to be updated for the app.</param>
/// <returns>
/// 204 => Pattern updated.
/// 200 => Pattern updated.
/// 400 => Pattern request not valid.
/// 404 => Pattern or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(PatternsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppPatternsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request)

2
src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs

@ -89,7 +89,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost]
[Route("apps/{app}/roles/")]
[ProducesResponseType(typeof(RolesDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppRolesCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostRole(string app, [FromBody] AddRoleDto request)
@ -139,7 +138,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpDelete]
[Route("apps/{app}/roles/{name}/")]
[ProducesResponseType(typeof(RolesDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppRolesDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteRole(string app, string name)

2
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -85,8 +85,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost]
[Route("apps/")]
[ProducesResponseType(typeof(AppDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> PostApp([FromBody] CreateAppDto request)

13
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -8,6 +8,7 @@
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -46,6 +47,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
private ClientDto CreateLinks(ApiController controller, string app)
{
var values = new { app, id = Id };
if (controller.HasPermission(Permissions.AppClientsUpdate, app))
{
AddPutLink("update", controller.Url<AppClientsController>(x => nameof(x.PutClient), values));
}
if (controller.HasPermission(Permissions.AppClientsDelete, app))
{
AddDeleteLink("delete", controller.Url<AppClientsController>(x => nameof(x.DeleteClient), values));
}
return this;
}
}

10
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs

@ -8,6 +8,7 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -32,6 +33,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
private ClientsDto CreateLinks(ApiController controller, string app)
{
var values = new { app };
AddSelfLink(controller.Url<AppClientsController>(x => nameof(x.GetClients), values));
if (controller.HasPermission(Permissions.AppClientsCreate, app))
{
AddPostLink("create", controller.Url<AppClientsController>(x => nameof(x.PostClient), values));
}
return this;
}
}

2
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -62,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
AddSelfLink(controller.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
if (controller.HasPermission(Permissions.AppContributorsAssign, app) && Items.Length < MaxContributors)
if (controller.HasPermission(Permissions.AppContributorsAssign, app) && (MaxContributors < 0 || Items.Length < MaxContributors))
{
AddPostLink("create", controller.Url<AppContributorsController>(x => nameof(x.PostContributor), values));
}

4
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -200,7 +200,6 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </remarks>
[HttpPut]
[Route("apps/{app}/assets/{id}/content/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(AssetDto), 200)]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
@ -228,7 +227,6 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </returns>
[HttpPut]
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(AssetDto), 200)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsUpdate)]
@ -248,7 +246,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset to delete.</param>
/// <returns>
/// 204 => Asset has been deleted.
/// 204 => Asset deleted.
/// 404 => Asset or app not found.
/// </returns>
[HttpDelete]

3
src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -76,7 +76,6 @@ namespace Squidex.Areas.Api.Controllers.Comments
[HttpPost]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> PostComment(string app, Guid commentsId, [FromBody] UpsertCommentDto request)
@ -104,7 +103,6 @@ namespace Squidex.Areas.Api.Controllers.Comments
/// </returns>
[HttpPut]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> PutComment(string app, Guid commentsId, Guid commentId, [FromBody] UpsertCommentDto request)
@ -126,7 +124,6 @@ namespace Squidex.Areas.Api.Controllers.Comments
/// </returns>
[HttpDelete]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteComment(string app, Guid commentsId, Guid commentId)

24
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -117,6 +117,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids, [FromQuery] string status = null)
@ -153,6 +154,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] string status = null)
@ -188,6 +190,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, Guid id)
@ -260,6 +263,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPost]
[Route("content/{app}/{name}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission(Permissions.AppContentsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
@ -296,6 +300,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission(Permissions.AppContentsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
@ -327,6 +332,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPatch]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission(Permissions.AppContentsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
@ -348,7 +354,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="id">The id of the content item to publish.</param>
/// <param name="request">The status request.</param>
/// <returns>
/// 204 => Content published.
/// 200 => Content published.
/// 404 => Content, schema or app not found.
/// 400 => Request is not valid.
/// </returns>
@ -357,6 +363,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/status/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> PutContentStatus(string app, string name, Guid id, ChangeStatusDto request)
@ -382,7 +389,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to discard changes.</param>
/// <returns>
/// 204 => Content restored.
/// 200 => Content restored.
/// 404 => Content, schema or app not found.
/// 400 => Content was not archived.
/// </returns>
@ -391,17 +398,18 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/discard/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission(Permissions.AppContentsDiscard)]
[ApiCosts(1)]
public async Task<IActionResult> DiscardChanges(string app, string name, Guid id)
public async Task<IActionResult> DiscardDraft(string app, string name, Guid id)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var command = new DiscardChanges { ContentId = id };
await CommandBus.PublishAsync(command);
var response = await InvokeCommandAsync(app, name, command);
return NoContent();
return Ok(response);
}
/// <summary>
@ -411,7 +419,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to delete.</param>
/// <returns>
/// 204 => Content has been deleted.
/// 204 => Content deleted.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks>
@ -427,9 +435,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
var command = new DeleteContent { ContentId = id };
var response = await InvokeCommandAsync(app, name, command);
await CommandBus.PublishAsync(command);
return Ok(response);
return NoContent();
}
private async Task<ContentDto> InvokeCommandAsync(string app, string schema, ICommand command)

6
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -119,12 +119,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{
if (controller.HasPermission(Permissions.AppContentsDiscard, app, schema))
{
AddPutLink("changes/discard", controller.Url<ContentsController>(x => nameof(x.DiscardChanges), values));
AddPutLink("draft/discard", controller.Url<ContentsController>(x => nameof(x.DiscardDraft), values));
}
if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published)))
{
AddPutLink("changes/publish", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values));
AddPutLink("draft/publish", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values));
}
}
@ -134,7 +134,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
if (Status == Status.Published)
{
AddPutLink("update/change", controller.Url<ContentsController>(x => nameof(x.PutContent), values) + "?asDraft");
AddPutLink("draft/propose", controller.Url<ContentsController>(x => nameof(x.PutContent), values) + "?asDraft=true");
}
}

1
src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs

@ -71,7 +71,6 @@ namespace Squidex.Areas.Api.Controllers.Plans
[HttpPut]
[Route("apps/{app}/plan/")]
[ProducesResponseType(typeof(PlanChangedDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppPlansChange)]
[ApiCosts(0)]
public async Task<IActionResult> PutPlan(string app, [FromBody] ChangePlanDto request)

16
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -103,8 +103,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// </returns>
[HttpPost]
[Route("apps/{app}/rules/")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(RuleDto), 201)]
[ApiPermission(Permissions.AppRulesCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostRule(string app, [FromBody] CreateRuleDto request)
@ -132,7 +131,6 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// </remarks>
[HttpPut]
[Route("apps/{app}/rules/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 200)]
[ProducesResponseType(typeof(RuleDto), 400)]
[ApiPermission(Permissions.AppRulesUpdate)]
[ApiCosts(1)]
@ -157,8 +155,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// </returns>
[HttpPut]
[Route("apps/{app}/rules/{id}/enable/")]
[ProducesResponseType(typeof(ErrorDto), 200)]
[ProducesResponseType(typeof(RuleDto), 400)]
[ProducesResponseType(typeof(RuleDto), 200)]
[ApiPermission(Permissions.AppRulesDisable)]
[ApiCosts(1)]
public async Task<IActionResult> EnableRule(string app, Guid id)
@ -182,8 +179,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// </returns>
[HttpPut]
[Route("apps/{app}/rules/{id}/disable/")]
[ProducesResponseType(typeof(ErrorDto), 200)]
[ProducesResponseType(typeof(RuleDto), 400)]
[ProducesResponseType(typeof(RuleDto), 200)]
[ApiPermission(Permissions.AppRulesDisable)]
[ApiCosts(1)]
public async Task<IActionResult> DisableRule(string app, Guid id)
@ -201,7 +197,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the rule to delete.</param>
/// <returns>
/// 204 => Rule has been deleted.
/// 204 => Rule deleted.
/// 404 => Rule or app not found.
/// </returns>
[HttpDelete]
@ -248,7 +244,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// <param name="app">The name of the app.</param>
/// <param name="id">The event to enqueue.</param>
/// <returns>
/// 200 => Rule enqueued.
/// 204 => Rule enqueued.
/// 404 => App or rule event not found.
/// </returns>
[HttpPut]
@ -275,7 +271,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// <param name="app">The name of the app.</param>
/// <param name="id">The event to enqueue.</param>
/// <returns>
/// 200 => Rule deqeued.
/// 204 => Rule deqeued.
/// 404 => App or rule event not found.
/// </returns>
[HttpDelete]

25
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -42,8 +42,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPost]
[Route("apps/{app}/schemas/{name}/fields/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PostField(string app, string name, [FromBody] AddFieldDto request)
@ -71,8 +69,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPost]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PostNestedField(string app, string name, long parentId, [FromBody] AddFieldDto request)
@ -91,14 +87,13 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="request">The request that contains the field ids.</param>
/// <returns>
/// 200 => Schema fields reorderd.
/// 200 => Schema fields reordered.
/// 400 => Schema field ids do not cover the fields of the schema.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/ordering/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutSchemaFieldOrdering(string app, string name, [FromBody] ReorderFieldsDto request)
@ -118,14 +113,13 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <param name="parentId">The parent field id.</param>
/// <param name="request">The request that contains the field ids.</param>
/// <returns>
/// 200 => Schema fields reorderd.
/// 200 => Schema fields reordered.
/// 400 => Schema field ids do not cover the fields of the schema.
/// 404 => Schema, field or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/ordering/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutNestedFieldOrdering(string app, string name, long parentId, [FromBody] ReorderFieldsDto request)
@ -152,7 +146,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutField(string app, string name, long id, [FromBody] UpdateFieldDto request)
@ -180,7 +173,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutNestedField(string app, string name, long parentId, long id, [FromBody] UpdateFieldDto request)
@ -209,7 +201,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/lock/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> LockField(string app, string name, long id)
@ -239,7 +230,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/lock/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> LockNestedField(string app, string name, long parentId, long id)
@ -268,7 +258,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/hide/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> HideField(string app, string name, long id)
@ -298,7 +287,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/hide/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> HideNestedField(string app, string name, long parentId, long id)
@ -327,7 +315,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/show/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> ShowField(string app, string name, long id)
@ -356,7 +343,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// </remarks>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/show/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> ShowNestedField(string app, string name, long parentId, long id)
@ -385,7 +372,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/enable/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> EnableField(string app, string name, long id)
@ -415,7 +401,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/enable/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> EnableNestedField(string app, string name, long parentId, long id)
@ -444,7 +429,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/disable/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> DisableField(string app, string name, long id)
@ -474,7 +458,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/disable/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> DisableNestedField(string app, string name, long parentId, long id)
@ -500,7 +483,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpDelete]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteField(string app, string name, long id)
@ -527,7 +509,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpDelete]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteNestedField(string app, string name, long parentId, long id)

15
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -109,8 +109,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPost]
[Route("apps/{app}/schemas/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ApiPermission(Permissions.AppSchemasCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostSchema(string app, [FromBody] CreateSchemaDto request)
@ -129,14 +127,13 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="request">The schema object that needs to updated.</param>
/// <returns>
/// 204 => Schema updated.
/// 200 => Schema updated.
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutSchema(string app, string name, [FromBody] UpdateSchemaDto request)
@ -162,7 +159,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/sync")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request)
@ -187,7 +183,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/category")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutCategory(string app, string name, [FromBody] ChangeCategoryDto request)
@ -212,7 +207,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/preview-urls")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request)
@ -225,7 +219,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
}
/// <summary>
/// Update the scripts of a schema.
/// Update the scripts.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
@ -238,7 +232,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/scripts/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasScripts)]
[ApiCosts(1)]
public async Task<IActionResult> PutScripts(string app, string name, [FromBody] SchemaScriptsDto request)
@ -263,7 +256,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/publish/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasPublish)]
[ApiCosts(1)]
public async Task<IActionResult> PublishSchema(string app, string name)
@ -288,7 +280,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/unpublish/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasPublish)]
[ApiCosts(1)]
public async Task<IActionResult> UnpublishSchema(string app, string name)
@ -306,7 +297,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema to delete.</param>
/// <returns>
/// 204 => Schema has been deleted.
/// 204 => Schema deleted.
/// 404 => Schema or app not found.
/// </returns>
[HttpDelete]

8
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -45,13 +45,13 @@
<ng-container *ngIf="content.isPending || !schema.isSingleton">
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="discardChanges()" *ngIf="content.canDiscardChanges">
<a class="dropdown-item" (click)="discardChanges()" *ngIf="content.canDraftDiscard">
Discard changes
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" (click)="publishChanges()" *ngIf="content.canPublishChanges">
<a class="dropdown-item" (click)="publishChanges()" *ngIf="content.canDraftPublish">
Publish changes
</a>
@ -74,8 +74,8 @@
</ng-container>
</div>
<button type="button" class="btn btn-secondary ml-1" (click)="saveAsProposal()" *ngIf="content.canProposeChange">
Save as Proposal
<button type="button" class="btn btn-secondary ml-1" (click)="saveAsDraft()" *ngIf="content.canDraftPropose">
Save as Draft
</button>
<ng-container *ngIf="content.canUpdate">

28
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -124,7 +124,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
this.saveContent(true, false);
}
public saveAsProposal() {
public saveAsDraft() {
this.saveContent(false, true);
}
@ -132,23 +132,27 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
this.saveContent(false, false);
}
private saveContent(publish: boolean, asProposal: boolean) {
if (this.content && this.content.status === 'Archived') {
return;
}
private saveContent(publish: boolean, asDraft: boolean) {
const value = this.contentForm.submit();
if (value) {
if (this.content) {
if (asProposal) {
this.contentsState.proposeUpdate(this.content, value)
if (asDraft) {
if (this.content && !this.content.canDraftPropose) {
return;
}
this.contentsState.proposeDraft(this.content, value)
.subscribe(() => {
this.contentForm.submitCompleted({ noReset: true });
}, error => {
this.contentForm.submitFailed(error);
});
} else {
if (this.content && !this.content.canUpdate) {
return;
}
this.contentsState.update(this.content, value)
.subscribe(() => {
this.contentForm.submitCompleted({ noReset: true });
@ -157,6 +161,10 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
});
}
} else {
if ((publish && !this.contentsState.snapshot.canCreate) || (!publish && !this.contentsState.snapshot.canCreateAndPublish)) {
return;
}
this.contentsState.create(value, publish)
.subscribe(() => {
this.back();
@ -178,7 +186,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
}
public discardChanges() {
this.contentsState.discardChanges(this.content);
this.contentsState.discardDraft(this.content);
}
public delete() {
@ -190,7 +198,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
public publishChanges() {
this.dueTimeSelector.selectDueTime('Publish').pipe(
switchMap(d => this.contentsState.publishChanges(this.content, d)), onErrorResumeNext())
switchMap(d => this.contentsState.publishDraft(this.content, d)), onErrorResumeNext())
.subscribe();
}

12
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -2,13 +2,7 @@
<sqx-panel desiredWidth="*" minWidth="50rem" contentClass="grid" showSidebar="true">
<ng-container title>
<ng-container *ngIf="contentsState.isArchive | async; else noArchive">
Archive
</ng-container>
<ng-template #noArchive>
Contents
</ng-template>
Contents
</ng-container>
<ng-container menu>
@ -36,8 +30,8 @@
<div class="col-auto pl-1" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.mutableValues"></sqx-language-selector>
</div>
<div class="col-auto pl-1" *ngIf="contentsState.canCreateAny | async">
<button type="button" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)">
<div class="col-auto pl-1">
<button type="button" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)" [disabled]="(contentsState.canCreateAny | async) === false">
<i class="icon-plus"></i> New
</button>

2
src/Squidex/app/features/rules/pages/rules/rules-page.component.html

@ -82,7 +82,7 @@
<ng-container sidebar>
<div class="panel-nav">
<ng-container *ngIf="rulesState.canCreate | async">
<ng-container *ngIf="rulesState.canReadEvents | async">
<a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i>
</a>

3
src/Squidex/app/framework/state.ts

@ -10,11 +10,10 @@ import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ErrorDto } from './utils/error';
import { ResourceLinks } from './utils/hateos';
import { Types } from './utils/types';
import { fullValue } from './angular/forms/forms-helper';
import { ResourceLinks } from '@app/shared';
export interface FormState {
submitted: boolean;

19
src/Squidex/app/framework/utils/hateos.ts

@ -1,3 +1,4 @@
/*
* Squidex Headless CMS
*
@ -16,15 +17,17 @@ export type ResourceLink = { href: string; method: ResourceMethod; };
export type Metadata = { [rel: string]: string };
function hasLink(value: Resource | ResourceLinks, rel: string): boolean {
const link = getLink(value, rel);
export function hasAnyLink(value: Resource | ResourceLinks, ...rels: string[]) {
if (!value) {
return false;
}
return !!(link && link.method && link.href);
}
const links = value._links || value;
export function hasAnyLink(value: Resource | ResourceLinks, ...rels: string[]) {
for (let rel of rels) {
if (hasLink(value, rel)) {
const link = links[rel];
if (link && link.method && link.href) {
return true;
}
}
@ -32,10 +35,6 @@ export function hasAnyLink(value: Resource | ResourceLinks, ...rels: string[])
return false;
}
export function getLink(value: Resource | ResourceLinks, rel: string): ResourceLink {
return value ? (value._links ? value._links[rel] : value[rel]) : undefined;
}
export type ResourceMethod =
'GET' |
'DELETE' |

2
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -181,5 +181,5 @@ function createLanguage(code: string, codes: string[], i: number) {
update: { method: 'PUT', href: `/languages/${code}` }
};
return new AppLanguageDto(links, code, code, i === 0, i % 2 === 1, codes.filter(x => x !== code))
return new AppLanguageDto(links, code, code, i === 0, i % 2 === 1, codes.filter(x => x !== code));
}

15
src/Squidex/app/shared/services/apps.service.ts

@ -14,6 +14,7 @@ import {
AnalyticsService,
ApiUrlConfig,
DateTime,
hasAnyLink,
pretifyError,
Resource,
ResourceLinks
@ -27,8 +28,8 @@ export class AppDto {
public readonly canReadBackups: boolean;
public readonly canReadClients: boolean;
public readonly canReadContributors: boolean;
public readonly canReadPatterns: boolean;
public readonly canReadLanguages: boolean;
public readonly canReadPatterns: boolean;
public readonly canReadPlans: boolean;
public readonly canReadRoles: boolean;
public readonly canReadRules: boolean;
@ -46,6 +47,18 @@ export class AppDto {
public readonly planUpgrade?: string
) {
this._links = links;
this.canCreateSchema = hasAnyLink(links, 'schemas/create');
this.canDelete = hasAnyLink(links, 'delete');
this.canReadBackups = hasAnyLink(links, 'backups');
this.canReadClients = hasAnyLink(links, 'clients');
this.canReadContributors = hasAnyLink(links, 'contributors');
this.canReadLanguages = hasAnyLink(links, 'languages');
this.canReadPatterns = hasAnyLink(links, 'patterns');
this.canReadPlans = hasAnyLink(links, 'plans');
this.canReadRoles = hasAnyLink(links, 'roles');
this.canReadRules = hasAnyLink(links, 'rules');
this.canReadSchemas = hasAnyLink(links, 'schemas');
}
}

2
src/Squidex/app/shared/services/assets.service.ts

@ -127,7 +127,7 @@ export class AssetsService {
return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe(
map(({ total, items, _links }) => {
const assets = items.map(item => parseAsset(item)); ;
const assets = items.map(item => parseAsset(item));
return new AssetsDto(total, assets, _links);
}),

62
src/Squidex/app/shared/services/contents.service.spec.ts

@ -178,13 +178,13 @@ describe('ContentsService', () => {
const resource: Resource = {
_links: {
['update/change']: { method: 'PUT', href: '/api/content/my-app/my-schema/content1?asDraft=true' }
update: { method: 'PUT', href: '/api/content/my-app/my-schema/content1?asDraft=true' }
}
};
let content: ContentDto;
contentsService.putContent('my-app', resource, dto, true, version).subscribe(result => {
contentsService.putContent('my-app', resource, dto, version).subscribe(result => {
content = result;
});
@ -225,18 +225,18 @@ describe('ContentsService', () => {
expect(content!).toEqual(createContent(12));
}));
it('should make put request to discard changes',
it('should make put request to discard draft',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
discard: { method: 'PUT', href: '/api/content/my-app/my-schema/content1/discard' }
['draft/discard']: { method: 'PUT', href: '/api/content/my-app/my-schema/content1/discard' }
}
};
let content: ContentDto;
contentsService.discardChanges('my-app', resource, version).subscribe(result => {
contentsService.discardDraft('my-app', resource, version).subscribe(result => {
content = result;
});
@ -250,6 +250,58 @@ describe('ContentsService', () => {
expect(content!).toEqual(createContent(12));
}));
it('should make put request to propose draft',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const dto = {};
const resource: Resource = {
_links: {
['draft/propose']: { method: 'PUT', href: '/api/content/my-app/my-schema/content1/status' }
}
};
let content: ContentDto;
contentsService.proposeDraft('my-app', resource, dto, version).subscribe(result => {
content = result;
});
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/status');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12));
}));
it('should make put request to publish draft',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
['draft/publish']: { method: 'PUT', href: '/api/content/my-app/my-schema/content1/status' }
}
};
let content: ContentDto;
contentsService.publishDraft('my-app', resource, null, version).subscribe(result => {
content = result;
});
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/status');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12));
}));
it('should make put request to change content status',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {

54
src/Squidex/app/shared/services/contents.service.ts

@ -50,9 +50,9 @@ export class ContentDto {
public readonly statusUpdates: string[];
public readonly canDelete: boolean;
public readonly canDiscardChanges: boolean;
public readonly canProposeChange: boolean;
public readonly canPublishChanges: boolean;
public readonly canDraftDiscard: boolean;
public readonly canDraftPropose: boolean;
public readonly canDraftPublish: boolean;
public readonly canUpdate: boolean;
constructor(links: ResourceLinks,
@ -70,6 +70,12 @@ export class ContentDto {
) {
this._links = links;
this.canDelete = hasAnyLink(links, 'delete');
this.canDraftDiscard = hasAnyLink(links, 'draft/discard');
this.canDraftPropose = hasAnyLink(links, 'draft/propose');
this.canDraftPublish = hasAnyLink(links, 'draft/publish');
this.canUpdate = hasAnyLink(links, 'update');
this.statusUpdates = Object.keys(links).filter(x => x.startsWith('status/')).map(x => x.substr(7));
}
}
@ -160,8 +166,8 @@ export class ContentsService {
pretifyError('Failed to create content. Please reload.'));
}
public putContent(appName: string, resource: Resource, dto: any, asDraft: boolean, version: Version): Observable<ContentDto> {
const link = resource._links[asDraft ? 'update/change' : 'update'];
public putContent(appName: string, resource: Resource, dto: any, version: Version): Observable<ContentDto> {
const link = resource._links['update'];
const url = this.apiUrl.buildUrl(link.href);
@ -190,8 +196,8 @@ export class ContentsService {
pretifyError('Failed to update content. Please reload.'));
}
public discardChanges(appName: string, resource: Resource, version: Version): Observable<ContentDto> {
const link = resource._links['discard'];
public discardDraft(appName: string, resource: Resource, version: Version): Observable<ContentDto> {
const link = resource._links['draft/discard'];
const url = this.apiUrl.buildUrl(link.href);
@ -202,7 +208,37 @@ export class ContentsService {
tap(() => {
this.analytics.trackEvent('Content', 'Discarded', appName);
}),
pretifyError('Failed to discard changes. Please reload.'));
pretifyError('Failed to discard draft. Please reload.'));
}
public proposeDraft(appName: string, resource: Resource, dto: any, version: Version): Observable<ContentDto> {
const link = resource._links['draft/propose'];
const url = this.apiUrl.buildUrl(link.href);
return HTTP.putVersioned(this.http, url, dto, version).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
}),
pretifyError('Failed to propose draft. Please reload.'));
}
public publishDraft(appName: string, resource: Resource, dueTime: string | null, version: Version): Observable<ContentDto> {
const link = resource._links['draft/publish'];
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, { status: 'Published', dueTime }).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Discarded', appName);
}),
pretifyError('Failed to publish draft. Please reload.'));
}
public putStatus(appName: string, resource: Resource, status: string, dueTime: string | null, version: Version): Observable<ContentDto> {
@ -248,5 +284,5 @@ function parseContent(response: any) {
response.isPending === true,
response.data,
response.dataDraft,
new Version(response.version));
new Version(response.version.toString()));
}

2
src/Squidex/app/shared/services/contributors.service.ts

@ -114,7 +114,7 @@ function parseContributors(response: any) {
item.contributorId,
item.role));
const { maxContributors, _links, _meta }= response;
const { maxContributors, _links, _meta } = response;
return { items: contributors, maxContributors, _links, _meta, canCreate: hasAnyLink(_links, 'create') };
}

8
src/Squidex/app/shared/services/rules.service.ts

@ -76,6 +76,14 @@ export class RuleElementPropertyDto {
}
export class RulesDto extends ResultSet<RuleDto> {
public get canCreate() {
return hasAnyLink(this._links, 'create');
}
public get canReadEvents() {
return hasAnyLink(this._links, 'events');
}
constructor(items: RuleDto[], links?: {}) {
super(items.length, items, links);
}

19
src/Squidex/app/shared/state/contents.state.ts

@ -10,7 +10,6 @@ import { forkJoin, Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
import {
DateTime,
DialogService,
ErrorDto,
ImmutableArray,
@ -228,8 +227,8 @@ export abstract class ContentsStateBase extends State<Snapshot> {
shareSubscribed(this.dialogs, { silent: true }));
}
public publishChanges(content: ContentDto, dueTime: string | null): Observable<ContentDto> {
return this.contentsService.putStatus(this.appName, content, 'Published', dueTime, content.version).pipe(
public publishDraft(content: ContentDto, dueTime: string | null): Observable<ContentDto> {
return this.contentsService.publishDraft(this.appName, content, dueTime, content.version).pipe(
tap(updated => {
this.dialogs.notifyInfo('Content updated successfully.');
@ -248,8 +247,8 @@ export abstract class ContentsStateBase extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public update(content: ContentDto, request: any, now?: DateTime): Observable<ContentDto> {
return this.contentsService.putContent(this.appName, content, request, false, content.version).pipe(
public update(content: ContentDto, request: any): Observable<ContentDto> {
return this.contentsService.putContent(this.appName, content, request, content.version).pipe(
tap(updated => {
this.dialogs.notifyInfo('Content updated successfully.');
@ -258,8 +257,8 @@ export abstract class ContentsStateBase extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public proposeUpdate(content: ContentDto, request: any): Observable<ContentDto> {
return this.contentsService.putContent(this.appName, content, request, true, content.version).pipe(
public proposeDraft(content: ContentDto, request: any): Observable<ContentDto> {
return this.contentsService.proposeDraft(this.appName, content, request, content.version).pipe(
tap(updated => {
this.dialogs.notifyInfo('Content updated successfully.');
@ -268,8 +267,8 @@ export abstract class ContentsStateBase extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public discardChanges(content: ContentDto, now?: DateTime): Observable<ContentDto> {
return this.contentsService.discardChanges(this.appName, content, content.version).pipe(
public discardDraft(content: ContentDto): Observable<ContentDto> {
return this.contentsService.discardDraft(this.appName, content, content.version).pipe(
tap(updated => {
this.dialogs.notifyInfo('Content updated successfully.');
@ -278,7 +277,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public patch(content: ContentDto, request: any, now?: DateTime): Observable<ContentDto> {
public patch(content: ContentDto, request: any): Observable<ContentDto> {
return this.contentsService.patchContent(this.appName, content, request, content.version).pipe(
tap(updated => {
this.dialogs.notifyInfo('Content updated successfully.');

24
src/Squidex/app/shared/state/rules.state.ts

@ -29,11 +29,17 @@ interface Snapshot {
// The current rules.
rules: RulesList;
// The resource links.
links: ResourceLinks;
// Indicates if the rules are loaded.
isLoaded?: boolean;
// Indicates if the user can create rules.
canCreate?: boolean;
// Indicates if the user can read events.
canReadEvents?: boolean;
// The resource links.
_links?: ResourceLinks;
}
type RulesList = ImmutableArray<RuleDto>;
@ -49,7 +55,11 @@ export class RulesState extends State<Snapshot> {
distinctUntilChanged());
public canCreate =
this.changes.pipe(map(x => x.links),
this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged());
public canReadEvents =
this.changes.pipe(map(x => !!x.canReadEvents),
distinctUntilChanged());
constructor(
@ -57,7 +67,7 @@ export class RulesState extends State<Snapshot> {
private readonly dialogs: DialogService,
private readonly rulesService: RulesService
) {
super({ rules: ImmutableArray.empty(), links: {} });
super({ rules: ImmutableArray.empty() });
}
public load(isReload = false): Observable<any> {
@ -66,7 +76,7 @@ export class RulesState extends State<Snapshot> {
}
return this.rulesService.getRules(this.appName).pipe(
tap(({ items, _links: links }) => {
tap(({ items, _links, canCreate, canReadEvents }) => {
if (isReload) {
this.dialogs.notifyInfo('Rules reloaded.');
}
@ -74,7 +84,7 @@ export class RulesState extends State<Snapshot> {
this.next(s => {
const rules = ImmutableArray.of(items);
return { ...s, rules, isLoaded: true, links };
return { ...s, rules, isLoaded: true, _links, canCreate, canReadEvents };
});
}),
shareSubscribed(this.dialogs));

2
src/Squidex/app/shared/state/ui.state.spec.ts

@ -39,7 +39,7 @@ describe('UIState', () => {
const resources: ResourceLinks = {
['admin/events']: { method: 'GET', href: '/api/events' },
['admin/restore']: { method: 'GET', href: '/api/restore' },
['admin/users']: { method: 'GET', href: '/api/users' },
['admin/users']: { method: 'GET', href: '/api/users' }
};
let usersService: IMock<UsersService>;

Loading…
Cancel
Save