diff --git a/backend/src/Notifo.Domain/Apps/App.cs b/backend/src/Notifo.Domain/Apps/App.cs index 7e8ca64a..7eef054c 100644 --- a/backend/src/Notifo.Domain/Apps/App.cs +++ b/backend/src/Notifo.Domain/Apps/App.cs @@ -26,6 +26,8 @@ public sealed record App(string Id, Instant Created) public Instant LastUpdate { get; init; } + public AppAuthScheme? AuthScheme { get; init; } + public ReadonlyList Languages { get; init; } = DefaultLanguages; public ReadonlyDictionary ApiKeys { get; init; } = ReadonlyDictionary.Empty(); diff --git a/backend/src/Notifo.Domain/Apps/AppAuthScheme.cs b/backend/src/Notifo.Domain/Apps/AppAuthScheme.cs new file mode 100644 index 00000000..9eb2bccb --- /dev/null +++ b/backend/src/Notifo.Domain/Apps/AppAuthScheme.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Notifo.io +// ========================================================================== +// Copyright (c) Sebastian Stehle +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Notifo.Domain.Apps; + +public sealed class AppAuthScheme +{ + public string Domain { get; init; } + + public string DisplayName { get; init; } + + public string ClientId { get; init; } + + public string ClientSecret { get; init; } + + public string Authority { get; init; } + + public string? SignoutRedirectUrl { get; init; } +} diff --git a/backend/src/Notifo.Domain/Apps/AppStore.cs b/backend/src/Notifo.Domain/Apps/AppStore.cs index 64f750d2..8a6b3645 100644 --- a/backend/src/Notifo.Domain/Apps/AppStore.cs +++ b/backend/src/Notifo.Domain/Apps/AppStore.cs @@ -45,6 +45,12 @@ public async Task CollectAsync(TrackingKey key, CounterMap counters, } } + public Task AnyAuthDomainAsync( + CancellationToken ct = default) + { + return repository.AnyAuthDomainAsync(ct); + } + public IAsyncEnumerable QueryAllAsync( CancellationToken ct = default) { @@ -116,6 +122,18 @@ public async Task> QueryAsync(string contributorId, return app; } + public async Task GetByAuthDomainAsync(string domain, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(domain); + + var (app, _) = await repository.GetByAuthDomainAsync(domain, ct); + + await DeliverAsync(app); + + return app; + } + public async ValueTask HandleAsync(AppCommand command, CancellationToken ct) { diff --git a/backend/src/Notifo.Domain/Apps/DeleteAppAuthScheme.cs b/backend/src/Notifo.Domain/Apps/DeleteAppAuthScheme.cs new file mode 100644 index 00000000..b5dda16d --- /dev/null +++ b/backend/src/Notifo.Domain/Apps/DeleteAppAuthScheme.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Notifo.io +// ========================================================================== +// Copyright (c) Sebastian Stehle +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Notifo.Domain.Apps; + +public sealed class DeleteAppAuthScheme : AppCommand +{ + public override ValueTask ExecuteAsync(App target, IServiceProvider serviceProvider, + CancellationToken ct) + { + target = target with { AuthScheme = null }; + + return new ValueTask(target); + } +} diff --git a/backend/src/Notifo.Domain/Apps/IAppRepository.cs b/backend/src/Notifo.Domain/Apps/IAppRepository.cs index b2cbec53..eb807b65 100644 --- a/backend/src/Notifo.Domain/Apps/IAppRepository.cs +++ b/backend/src/Notifo.Domain/Apps/IAppRepository.cs @@ -26,9 +26,15 @@ Task> QueryAsync(string contributorId, Task<(App? App, string? Etag)> GetAsync(string id, CancellationToken ct = default); + Task<(App? App, string? Etag)> GetByAuthDomainAsync(string domain, + CancellationToken ct = default); + Task UpsertAsync(App app, string? oldEtag = null, CancellationToken ct = default); Task DeleteAsync(string id, CancellationToken ct = default); + + Task AnyAuthDomainAsync( + CancellationToken ct = default); } diff --git a/backend/src/Notifo.Domain/Apps/IAppStore.cs b/backend/src/Notifo.Domain/Apps/IAppStore.cs index 16f4fee8..589a9d6d 100644 --- a/backend/src/Notifo.Domain/Apps/IAppStore.cs +++ b/backend/src/Notifo.Domain/Apps/IAppStore.cs @@ -24,6 +24,12 @@ Task> QueryAsync(string contributorId, Task GetAsync(string id, CancellationToken ct = default); + Task GetByAuthDomainAsync(string domain, + CancellationToken ct = default); + Task GetCachedAsync(string id, CancellationToken ct = default); + + Task AnyAuthDomainAsync( + CancellationToken ct = default); } diff --git a/backend/src/Notifo.Domain/Apps/MongoDb/MongoDbAppRepository.cs b/backend/src/Notifo.Domain/Apps/MongoDb/MongoDbAppRepository.cs index 44ebeb21..467df3f6 100644 --- a/backend/src/Notifo.Domain/Apps/MongoDb/MongoDbAppRepository.cs +++ b/backend/src/Notifo.Domain/Apps/MongoDb/MongoDbAppRepository.cs @@ -109,6 +109,19 @@ await Collection.Find(x => x.ContributorIds.Contains(contributorId)) } } + public async Task<(App? App, string? Etag)> GetByAuthDomainAsync(string domain, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoDbAppRepository/GetByAuthDomainAsync")) + { + var document = await + Collection.Find(x => x.Doc.AuthScheme!.Domain == domain) + .FirstOrDefaultAsync(ct); + + return (document?.ToApp(), document?.Etag); + } + } + public async Task<(App? App, string? Etag)> GetAsync(string id, CancellationToken ct = default) { @@ -131,6 +144,15 @@ public async Task UpsertAsync(App app, string? oldEtag = null, } } + public async Task AnyAuthDomainAsync( + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoDbAppRepository/AnyAuthDomainAsync")) + { + return await Collection.Find(x => x.Doc.AuthScheme != null).AnyAsync(ct); + } + } + public async Task BatchWriteAsync(List<(string Key, CounterMap Counters)> counters, CancellationToken ct) { diff --git a/backend/src/Notifo.Domain/Apps/UpsertAppAuthScheme.cs b/backend/src/Notifo.Domain/Apps/UpsertAppAuthScheme.cs new file mode 100644 index 00000000..d126cdae --- /dev/null +++ b/backend/src/Notifo.Domain/Apps/UpsertAppAuthScheme.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Notifo.io +// ========================================================================== +// Copyright (c) Sebastian Stehle +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FluentValidation; +using Notifo.Infrastructure.Validation; + +namespace Notifo.Domain.Apps; + +public sealed class UpsertAppAuthScheme : AppCommand +{ + public string Domain { get; init; } + + public string DisplayName { get; init; } + + public string ClientId { get; init; } + + public string ClientSecret { get; init; } + + public string Authority { get; init; } + + public string? SignoutRedirectUrl { get; init; } + + private sealed class Validator : AbstractValidator + { + public Validator() + { + RuleFor(x => x.Domain).NotNull().NotEmpty().Domain(); + RuleFor(x => x.DisplayName).NotNull().NotEmpty(); + RuleFor(x => x.ClientId).NotNull().NotEmpty(); + RuleFor(x => x.ClientSecret).NotNull().NotEmpty(); + RuleFor(x => x.Authority).NotNull().NotEmpty().Url(); + RuleFor(x => x.SignoutRedirectUrl).Url(); + } + } + + public override ValueTask ExecuteAsync(App target, IServiceProvider serviceProvider, + CancellationToken ct) + { + Validate.It(this); + + var newScheme = new AppAuthScheme + { + Authority = Authority, + ClientId = ClientId, + ClientSecret = ClientSecret, + DisplayName = DisplayName, + Domain = Domain, + SignoutRedirectUrl = SignoutRedirectUrl, + }; + + if (!Equals(target.AuthScheme, newScheme)) + { + target = target with { AuthScheme = newScheme }; + } + + return new ValueTask(target); + } +} diff --git a/backend/src/Notifo.Domain/Resources/Texts.Designer.cs b/backend/src/Notifo.Domain/Resources/Texts.Designer.cs index 687d0cbd..9ee7f00c 100644 --- a/backend/src/Notifo.Domain/Resources/Texts.Designer.cs +++ b/backend/src/Notifo.Domain/Resources/Texts.Designer.cs @@ -267,6 +267,15 @@ internal static string TemplateError { } } + /// + /// Looks up a localized string similar to {PropertyName} must be a valid domain.. + /// + internal static string ValidationDomain { + get { + return ResourceManager.GetString("ValidationDomain", resourceCulture); + } + } + /// /// Looks up a localized string similar to {PropertyName} must be a valid language.. /// diff --git a/backend/src/Notifo.Domain/Resources/Texts.resx b/backend/src/Notifo.Domain/Resources/Texts.resx index 4bd0ef43..0e538dbd 100644 --- a/backend/src/Notifo.Domain/Resources/Texts.resx +++ b/backend/src/Notifo.Domain/Resources/Texts.resx @@ -186,6 +186,9 @@ Unhandled template error. + + {PropertyName} must be a valid domain. + {PropertyName} must be a valid language. diff --git a/backend/src/Notifo.Domain/Utils/Validation/ValidatorExtensions.cs b/backend/src/Notifo.Domain/Utils/Validation/ValidatorExtensions.cs index 76dfb04d..efbefd9d 100644 --- a/backend/src/Notifo.Domain/Utils/Validation/ValidatorExtensions.cs +++ b/backend/src/Notifo.Domain/Utils/Validation/ValidatorExtensions.cs @@ -33,6 +33,14 @@ public static class ValidatorExtensions }).WithMessage(Texts.ValidationUrl); } + public static IRuleBuilderOptions Domain(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must(value => + { + return string.IsNullOrWhiteSpace(value) || Uri.CheckHostName(value) == UriHostNameType.Dns; + }).WithMessage(Texts.ValidationDomain); + } + public static IRuleBuilderOptions Language(this IRuleBuilder ruleBuilder) { return ruleBuilder.Must(value => diff --git a/backend/src/Notifo.Identity/AuthenticationBuilderExtensions.cs b/backend/src/Notifo.Identity/AuthenticationBuilderExtensions.cs index ce43a4cd..9b129ddf 100644 --- a/backend/src/Notifo.Identity/AuthenticationBuilderExtensions.cs +++ b/backend/src/Notifo.Identity/AuthenticationBuilderExtensions.cs @@ -51,7 +51,11 @@ public static AuthenticationBuilder AddOidc(this AuthenticationBuilder authBuild authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options => { - options.Events = new OidcHandler(identityOptions); + options.Events = new OidcHandler(new OdicOptions + { + SignoutRedirectUrl = identityOptions.OidcOnSignoutRedirectUrl + }); + options.Authority = identityOptions.OidcAuthority; options.ClientId = identityOptions.OidcClient; options.ClientSecret = identityOptions.OidcSecret; diff --git a/backend/src/Notifo.Identity/Dynamic/DynamicOpenIdConnectHandler.cs b/backend/src/Notifo.Identity/Dynamic/DynamicOpenIdConnectHandler.cs new file mode 100644 index 00000000..bb8f398b --- /dev/null +++ b/backend/src/Notifo.Identity/Dynamic/DynamicOpenIdConnectHandler.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Notifo.io +// ========================================================================== +// Copyright (c) Sebastian Stehle +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Notifo.Identity.Dynamic; + +public sealed class DynamicOpenIdConnectHandler : OpenIdConnectHandler +{ + public DynamicOpenIdConnectHandler(IOptionsMonitor options, ILoggerFactory logger, HtmlEncoder htmlEncoder, UrlEncoder encoder) + : base(options, logger, htmlEncoder, encoder) + { + } +} diff --git a/backend/src/Notifo.Identity/Dynamic/DynamicOpenIdConnectOptions.cs b/backend/src/Notifo.Identity/Dynamic/DynamicOpenIdConnectOptions.cs new file mode 100644 index 00000000..3d3da504 --- /dev/null +++ b/backend/src/Notifo.Identity/Dynamic/DynamicOpenIdConnectOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Notifo.io +// ========================================================================== +// Copyright (c) Sebastian Stehle +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +namespace Notifo.Identity.Dynamic; + +public sealed class DynamicOpenIdConnectOptions : OpenIdConnectOptions +{ +} diff --git a/backend/src/Notifo.Identity/Dynamic/DynamicSchemeProvider.cs b/backend/src/Notifo.Identity/Dynamic/DynamicSchemeProvider.cs new file mode 100644 index 00000000..95102295 --- /dev/null +++ b/backend/src/Notifo.Identity/Dynamic/DynamicSchemeProvider.cs @@ -0,0 +1,184 @@ +// ========================================================================== +// Notifo.io +// ========================================================================== +// Copyright (c) Sebastian Stehle +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Notifo.Domain.Apps; + +namespace Notifo.Identity.Dynamic; + +public sealed class DynamicSchemeProvider : AuthenticationSchemeProvider, IOptionsMonitor +{ + private static readonly string[] UrlPrefixes = ["signin-", "signout-callback-", "signout-"]; + + private readonly IAppStore appStore; + private readonly IHttpContextAccessor httpContextAccessor; + private readonly OpenIdConnectPostConfigureOptions configure; + + public DynamicOpenIdConnectOptions CurrentValue => null!; + + public DynamicSchemeProvider(IAppStore appStore, IHttpContextAccessor httpContextAccessor, + OpenIdConnectPostConfigureOptions configure, + IOptions options) + : base(options) + { + this.appStore = appStore; + this.httpContextAccessor = httpContextAccessor; + this.configure = configure; + } + + public Task HasCustomSchemeAsync() + { + return appStore.AnyAuthDomainAsync(default); + } + + public async Task GetSchemaByEmailAddressAsync(string email) + { + if (string.IsNullOrWhiteSpace(email)) + { + return null; + } + + var parts = email.Split('@'); + + if (parts.Length != 2) + { + return null; + } + + var app = await appStore.GetByAuthDomainAsync(parts[1], default); + + if (app?.AuthScheme != null) + { + return CreateScheme(app.Id, app.AuthScheme).Scheme; + } + + return null; + } + + public override async Task GetSchemeAsync(string name) + { + var result = await GetSchemeCoreAsync(name); + + if (result != null) + { + return result.Scheme; + } + + return await base.GetSchemeAsync(name); + } + + public override async Task> GetRequestHandlerSchemesAsync() + { + var result = (await base.GetRequestHandlerSchemesAsync()).ToList(); + + if (httpContextAccessor.HttpContext == null) + { + return result; + } + + var path = httpContextAccessor.HttpContext.Request.Path.Value; + + if (string.IsNullOrWhiteSpace(path)) + { + return result; + } + + var lastSegment = path.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty; + + foreach (var prefix in UrlPrefixes) + { + if (lastSegment.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + var name = lastSegment[prefix.Length..]; + + var scheme = await GetSchemeCoreAsync(name); + if (scheme != null) + { + result.Add(scheme.Scheme); + } + } + } + + return result; + } + + public DynamicOpenIdConnectOptions Get(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return new DynamicOpenIdConnectOptions(); + } + + var scheme = GetSchemeCoreAsync(name).Result; + + return scheme?.Options ?? new DynamicOpenIdConnectOptions(); + } + + public IDisposable? OnChange(Action listener) + { + return null; + } + + private async Task GetSchemeCoreAsync(string name) + { + var cacheKey = ("DYNAMIC_SCHEME", name); + + if (httpContextAccessor.HttpContext?.Items.TryGetValue(cacheKey, out var cached) == true) + { + return cached as SchemeResult; + } + + var app = await appStore.GetAsync(name, default); + + var result = (SchemeResult?)null; + if (app?.AuthScheme != null) + { + result = CreateScheme(app.Id, app.AuthScheme); + } + + if (httpContextAccessor.HttpContext != null) + { + httpContextAccessor.HttpContext.Items[cacheKey] = result; + } + + return result; + } + + private SchemeResult CreateScheme(string name, AppAuthScheme config) + { + var scheme = new AuthenticationScheme(name, config.DisplayName, typeof(DynamicOpenIdConnectHandler)); + + var options = new DynamicOpenIdConnectOptions + { + Events = new OidcHandler(new OdicOptions + { + SignoutRedirectUrl = config.SignoutRedirectUrl + }), + Authority = config.Authority, + CallbackPath = new PathString($"/signin-{name}"), + ClientId = config.ClientId, + ClientSecret = config.ClientSecret, + RemoteSignOutPath = new PathString($"/signout-{name}"), + RequireHttpsMetadata = false, + ResponseType = "code", + SignedOutRedirectUri = new PathString($"/signout-callback-{name}") + }; + + configure.PostConfigure(name, options); + + return new SchemeResult(scheme, options); + } + +#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + private sealed record SchemeResult(AuthenticationScheme Scheme, DynamicOpenIdConnectOptions Options); +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter +#pragma warning restore RECS0082 // Parameter has the same name as a member and hides it +} diff --git a/backend/src/Notifo.Identity/IdentityServiceExtensions.cs b/backend/src/Notifo.Identity/IdentityServiceExtensions.cs index e6e827f7..85215cfb 100644 --- a/backend/src/Notifo.Identity/IdentityServiceExtensions.cs +++ b/backend/src/Notifo.Identity/IdentityServiceExtensions.cs @@ -6,14 +6,17 @@ // ========================================================================== using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.DataProtection.KeyManagement; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Logging; using Notifo.Domain.Identity; using Notifo.Identity; using Notifo.Identity.ApiKey; +using Notifo.Identity.Dynamic; using Notifo.Identity.InMemory; using Notifo.Identity.MongoDb; using OpenIddict.Abstractions; @@ -42,6 +45,12 @@ public static void AddMyIdentity(this IServiceCollection services, IConfiguratio services.AddSingletonAs() .AsSelf(); + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf().As().As>(); + services.AddSingletonAs() .As(); diff --git a/backend/src/Notifo.Identity/Notifo.Identity.csproj b/backend/src/Notifo.Identity/Notifo.Identity.csproj index bf0df667..b8a42ad0 100644 --- a/backend/src/Notifo.Identity/Notifo.Identity.csproj +++ b/backend/src/Notifo.Identity/Notifo.Identity.csproj @@ -17,6 +17,7 @@ + diff --git a/backend/src/Notifo.Identity/NotifoIdentityOptions.cs b/backend/src/Notifo.Identity/NotifoIdentityOptions.cs index c33d4851..4b9c9c6f 100644 --- a/backend/src/Notifo.Identity/NotifoIdentityOptions.cs +++ b/backend/src/Notifo.Identity/NotifoIdentityOptions.cs @@ -39,7 +39,7 @@ public sealed class NotifoIdentityOptions public string OidcOnSignoutRedirectUrl { get; set; } - public string OidcPrompt { get; set; } + public string? OidcPrompt { get; set; } public string[] OidcScopes { get; set; } diff --git a/backend/src/Notifo.Identity/OidcHandler.cs b/backend/src/Notifo.Identity/OidcHandler.cs index 29cd1913..a026da55 100644 --- a/backend/src/Notifo.Identity/OidcHandler.cs +++ b/backend/src/Notifo.Identity/OidcHandler.cs @@ -5,25 +5,31 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Security.Claims; using Microsoft.AspNetCore.Authentication.OpenIdConnect; +#pragma warning disable MA0048 // File name must match type name + namespace Notifo.Identity; +public class OdicOptions +{ + public string? SignoutRedirectUrl { get; set; } +} + public sealed class OidcHandler : OpenIdConnectEvents { - private readonly NotifoIdentityOptions options; + private readonly OdicOptions options; - public OidcHandler(NotifoIdentityOptions options) + public OidcHandler(OdicOptions options) { this.options = options; } public override Task RedirectToIdentityProviderForSignOut(RedirectContext context) { - if (!string.IsNullOrEmpty(options.OidcOnSignoutRedirectUrl)) + if (!string.IsNullOrEmpty(options.SignoutRedirectUrl)) { - var logoutUri = options.OidcOnSignoutRedirectUrl; + var logoutUri = options.SignoutRedirectUrl; context.Response.Redirect(logoutUri); context.HandleResponse(); diff --git a/backend/src/Notifo/Areas/Account/Pages/ExternalLogin.cshtml.cs b/backend/src/Notifo/Areas/Account/Pages/ExternalLogin.cshtml.cs index bd542b78..6eab3bda 100644 --- a/backend/src/Notifo/Areas/Account/Pages/ExternalLogin.cshtml.cs +++ b/backend/src/Notifo/Areas/Account/Pages/ExternalLogin.cshtml.cs @@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations; using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Notifo.Areas.Account.Pages.Utils; @@ -20,6 +21,8 @@ namespace Notifo.Areas.Account.Pages; public sealed class ExternalLoginModel : PageModelBase { + private readonly IAuthenticationSchemeProvider schemes; + public string LoginProvider { get; set; } public string TermsOfServiceUrl { get; set; } @@ -31,7 +34,12 @@ public sealed class ExternalLoginModel : PageModelBase public bool MustAcceptsPrivacyPolicy { get; set; } = true; [BindProperty] - public ConfirmationModel Model { get; set; } = new ConfirmationModel(); + public ConfirmationForm Model { get; set; } = new ConfirmationForm(); + + public ExternalLoginModel(IAuthenticationSchemeProvider schemes) + { + this.schemes = schemes; + } public IActionResult OnGet() { @@ -40,6 +48,7 @@ public IActionResult OnGet() public async Task OnGetCallback(string? remoteError = null) { + // This should actually never happen. if (remoteError != null) { ThrowHelper.InvalidOperationException(T["ExternalLoginError"]); @@ -48,6 +57,7 @@ public async Task OnGetCallback(string? remoteError = null) var loginInfo = await SignInManager.GetExternalLoginInfoAsync(); + // This should actually never happen. if (loginInfo == null) { return RedirectToPage("./Login"); @@ -55,6 +65,7 @@ public async Task OnGetCallback(string? remoteError = null) var result = await SignInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, false); + // Only redirect the user if he is not locked out manually or due too many invalid login attempts. if (result.Succeeded) { return RedirectTo(ReturnUrl); @@ -64,13 +75,17 @@ public async Task OnGetCallback(string? remoteError = null) return RedirectToPage("./Lockout"); } - LoginProvider = loginInfo.LoginProvider; + var provider = await schemes.GetSchemeAsync(loginInfo.LoginProvider); + + // Get the display name of the provider that is not added to the cookie or session. + LoginProvider = provider?.DisplayName ?? loginInfo.LoginProvider; var email = loginInfo.Principal.FindFirst(ClaimTypes.Email)?.Value; + // Some providers do not provide an email address, therefore we need to handle it here. if (string.IsNullOrWhiteSpace(email)) { - var errorMessage = T["GithubEmailPrivateError"]; + var errorMessage = T["EmailPrivateError"]; return RedirectToPage("./Login", new { errorMessage }); } @@ -113,6 +128,7 @@ public async Task OnPostConfirmation() ModelState.AddModelError($"{nameof(Model)}.{field}", T[$"{field}Error"]!); } + // This is invalid when we have added one of the model errors above. if (!ModelState.IsValid) { return Page(); @@ -120,6 +136,7 @@ public async Task OnPostConfirmation() var loginInfo = await SignInManager.GetExternalLoginInfoAsync(); + // This should actually never happen. if (loginInfo == null) { ThrowHelper.InvalidOperationException(T["ExternalLoginError"]); @@ -128,6 +145,7 @@ public async Task OnPostConfirmation() var email = loginInfo.Principal.FindFirst(ClaimTypes.Email)?.Value; + // Some providers do not provide an email address, therefore we need to handle it here. if (string.IsNullOrWhiteSpace(email)) { var errorMessage = T["GithubEmailPrivateError"]; @@ -168,7 +186,7 @@ private async Task HasLoginAsync(IUser user) } } -public sealed class ConfirmationModel +public sealed class ConfirmationForm { [Required] [Display(Name = nameof(AcceptPrivacyPolicy))] diff --git a/backend/src/Notifo/Areas/Account/Pages/Login.cshtml b/backend/src/Notifo/Areas/Account/Pages/Login.cshtml index 5cbdf648..813d4a88 100644 --- a/backend/src/Notifo/Areas/Account/Pages/Login.cshtml +++ b/backend/src/Notifo/Areas/Account/Pages/Login.cshtml @@ -1,4 +1,5 @@ @page +@using Microsoft.AspNetCore.Mvc.ModelBinding @inject IHtmlLocalizer T @model LoginModel @@ -7,29 +8,20 @@ Layout = "_LayoutLogin"; - void RenderValidation(string field) - { - @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) - { -
- @Html.ValidationMessage(field) -
- } - } - - var loginText = Model.Signup ? "SignupWith" : "LoginWith"; + var loginWithText = Model.Signup ? "SignupWith" : "LoginWith"; + var loginNormalText = Model.Signup ? "Signup" : "Login"; } -@if (!string.IsNullOrWhiteSpace(Model!.ErrorMessage)) +@if (!string.IsNullOrWhiteSpace(Model.ErrorMessage)) {
- @Model!.ErrorMessage + @Model.ErrorMessage
}
- @if (Model!.Signup) + @if (Model.Signup) { @T["Login"] } @@ -38,7 +30,7 @@ @T["Login"] } - @if (!Model!.Signup) + @if (!Model.Signup) { @T["Signup"] } @@ -49,74 +41,105 @@
-@if (Model!.HasPasswordAuth) -{ - if (Model.Signup) +
+ @if (Model.HasPasswordAuth) { -
- @T["NoAccount"] -
- } - else - { -
-
-
+ if (Model.Signup) + { +
+ @T["NoAccount"] +
+ } + else + { +
+ + @if (Model.LoginEmailForm.IsActive) + { +
+ } -
- +
+ - @{ RenderValidation("Model.Email"); } +
+ +
- -
+
+ -
- +
+ +
+ +
+
+ + +
+
+ +
+ +
+ +
+ } + } + + @if (Model.ExternalLogins?.Count > 0) + { + + +
+
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + var lowerName = provider.DisplayName!.ToLowerInvariant(); + + + } +

+
+
+
+ } - @{ RenderValidation("Model.Password"); } + @if (Model.HasDynamicAuthScheme) + { + - +
+
+ @if (Model.LoginDynamicForm.IsActive) + { +
+ } + +
+ @T["LoginCustom"]
-
- - -
+ + +
+
- +
} -} - -@if (Model!.HasPasswordAuth && Model?.ExternalLogins?.Count > 0) -{ - -} - -@if (Model?.ExternalLogins?.Count > 0) -{ -
-
-
-

- @foreach (var provider in Model.ExternalLogins) - { - var lowerName = provider.DisplayName!.ToLowerInvariant(); - - - } -

-
-
-
-} \ No newline at end of file +
\ No newline at end of file diff --git a/backend/src/Notifo/Areas/Account/Pages/Login.cshtml.cs b/backend/src/Notifo/Areas/Account/Pages/Login.cshtml.cs index 04e507f5..ecd28f28 100644 --- a/backend/src/Notifo/Areas/Account/Pages/Login.cshtml.cs +++ b/backend/src/Notifo/Areas/Account/Pages/Login.cshtml.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Notifo.Areas.Account.Pages.Utils; +using Notifo.Identity.Dynamic; #pragma warning disable MA0048 // File name must match type name @@ -17,18 +18,30 @@ namespace Notifo.Areas.Account.Pages; public sealed class LoginModel : PageModelBase { + private readonly DynamicSchemeProvider schemeProvider; + public IList ExternalLogins { get; set; } public bool RememberMe { get; set; } + public bool HasDynamicAuthScheme { get; set; } + + public LoginEmailForm LoginEmailForm { get; set; } = new LoginEmailForm(); + + public LoginDynamicForm LoginDynamicForm { get; set; } = new LoginDynamicForm(); + [BindProperty(SupportsGet = true)] public bool Signup { get; set; } - [BindProperty] - public LoginInputModel Model { get; set; } = new LoginInputModel(); + public LoginModel(DynamicSchemeProvider schemeProvider) + { + this.schemeProvider = schemeProvider; + } public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) { + HasDynamicAuthScheme = await schemeProvider.HasCustomSchemeAsync(); + ExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); await next(); @@ -38,14 +51,17 @@ public void OnGet() { } - public async Task OnPost(LoginInputModel model) + public async Task OnPost( + [FromForm(Name = "LoginEmailForm")] LoginEmailForm form) { + LoginEmailForm = form with { IsActive = true }; + if (!ModelState.IsValid) { return Page(); } - var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, true); + var result = await SignInManager.PasswordSignInAsync(form.Email, form.Password, form.RememberMe, true); if (result.Succeeded) { @@ -61,9 +77,36 @@ public async Task OnPost(LoginInputModel model) return Page(); } + + public async Task OnPostDynamic( + [FromForm(Name = "LoginDynamicForm")] LoginDynamicForm form) + { + LoginDynamicForm = form with { IsActive = true }; + + if (!ModelState.IsValid) + { + return Page(); + } + + var scheme = await schemeProvider.GetSchemaByEmailAddressAsync(form.Email); + + if (scheme != null) + { + var provider = scheme.Name; + + var challengeRedirectUrl = Url.Page("ExternalLogin", "Callback", new { ReturnUrl }); + var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl); + + return Challenge(challengeProperties, provider); + } + + ModelState.AddModelError(string.Empty, T["LoginCustomNoProvider"]!); + + return Page(); + } } -public sealed class LoginInputModel +public sealed record LoginEmailForm { [Required] [EmailAddress] @@ -77,4 +120,16 @@ public sealed class LoginInputModel [Required] [Display(Name = nameof(RememberMe))] public bool RememberMe { get; set; } + + public bool IsActive { get; set; } +} + +public sealed record LoginDynamicForm +{ + [Required] + [EmailAddress] + [Display(Name = nameof(Email))] + public string Email { get; set; } + + public bool IsActive { get; set; } } diff --git a/backend/src/Notifo/Areas/Account/Pages/Profile.cshtml b/backend/src/Notifo/Areas/Account/Pages/Profile.cshtml index 34a954a6..53a73609 100644 --- a/backend/src/Notifo/Areas/Account/Pages/Profile.cshtml +++ b/backend/src/Notifo/Areas/Account/Pages/Profile.cshtml @@ -5,49 +5,43 @@ @{ ViewBag.Title = T["UsersProfileTitle"]; - - void RenderValidation(string field) - { - @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) - { -
- @Html.ValidationMessage(field) -
- } - } }

@T["UsersProfileHeadline"]

@T["UsersProfilePii"]

-@if (!string.IsNullOrWhiteSpace(Model!.StatusMessage)) +@if (!string.IsNullOrWhiteSpace(Model.StatusMessage)) {
- @Model!.StatusMessage + @Model.StatusMessage
} -@if (!string.IsNullOrWhiteSpace(Model!.ErrorMessage)) +@if (!string.IsNullOrWhiteSpace(Model.ErrorMessage)) {
- @Model!.ErrorMessage + @Model.ErrorMessage
}
- + @if (Model.ChangeForm.IsActive) + { +
+ } - @{ RenderValidation("Model.Email"); } + - +
+
-@if (Model!.ExternalProviders.Any()) +@if (Model.ExternalProviders.Any()) {
@@ -60,7 +54,7 @@ - @foreach (var login in Model!.ExternalLogins) + @foreach (var login in Model.ExternalLogins) { @@ -70,11 +64,11 @@ @login.ProviderDisplayName - @if (Model!.ExternalLogins.Count > 1 || Model!.HasPassword) + @if (Model.ExternalLogins.Count > 1 || Model.HasPassword) {
- - + +
+ ); }; + +function delay(timeout: number) { + return new Promise(resolve => { + setTimeout(resolve, timeout); + }); +} \ No newline at end of file diff --git a/frontend/src/app/framework/react/ClearInput.tsx b/frontend/src/app/framework/react/ClearInput.tsx index ee81b064..91ddfe05 100644 --- a/frontend/src/app/framework/react/ClearInput.tsx +++ b/frontend/src/app/framework/react/ClearInput.tsx @@ -18,7 +18,6 @@ export interface ClearInputProps extends InputProps { export const ClearInput = (props: InputProps) => { const { bsSize, onClear, ...other } = props; - const container = React.useRef(null); const [value, setValue] = React.useState(props.value); React.useEffect(() => { @@ -26,27 +25,22 @@ export const ClearInput = (props: InputProps) => { }, [props.value]); const doClear = useEventCallback(() => { - if (container.current) { - setValue(undefined); + setValue(undefined); - const input: HTMLInputElement = container.current.children[0] as HTMLInputElement; + props?.onChange?.({ target: { value: '' } } as React.ChangeEvent); - if (input) { - setNativeValue(input, ''); - - input.dispatchEvent(new Event('input', { bubbles: true })); - } - - onClear && onClear(); - } + onClear && onClear(); }); return ( -
- +
+ {value &&
); }; - -function setNativeValue(element: HTMLInputElement, value: string) { - const valueSetter = Object.getOwnPropertyDescriptor(element, 'value')!.set; - - const prototypeInstance = Object.getPrototypeOf(element); - const prototypeSetter = Object.getOwnPropertyDescriptor(prototypeInstance, 'value')!.set; - - if (valueSetter && valueSetter !== prototypeSetter) { - prototypeSetter!.call(element, value); - } else { - valueSetter!.call(element, value); - } -} diff --git a/frontend/src/app/framework/react/ErrorBoundary.tsx b/frontend/src/app/framework/react/ErrorBoundary.tsx index 48ca45b0..9268cb16 100644 --- a/frontend/src/app/framework/react/ErrorBoundary.tsx +++ b/frontend/src/app/framework/react/ErrorBoundary.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { texts } from '@app/texts'; export interface ErrorBoundaryProps extends React.PropsWithChildren { - // True if silent. + // True, if silent. silent?: boolean; } diff --git a/frontend/src/app/framework/react/Icon.tsx b/frontend/src/app/framework/react/Icon.tsx index 965fc29a..9bdd01c5 100644 --- a/frontend/src/app/framework/react/Icon.tsx +++ b/frontend/src/app/framework/react/Icon.tsx @@ -15,6 +15,7 @@ export type IconType = 'browser' | 'clear' | 'code' | + 'check' | 'create' | 'dashboard' | 'delete' | diff --git a/frontend/src/app/framework/react/Loader.tsx b/frontend/src/app/framework/react/Loader.tsx index ce40c926..9990fc2d 100644 --- a/frontend/src/app/framework/react/Loader.tsx +++ b/frontend/src/app/framework/react/Loader.tsx @@ -23,7 +23,7 @@ export interface LoaderProps { // Optional text. text?: string; - // True if light. + // True, if light. light?: boolean; } diff --git a/frontend/src/app/framework/react/Toggle.tsx b/frontend/src/app/framework/react/Toggle.tsx index ce710099..5c2e0d3d 100644 --- a/frontend/src/app/framework/react/Toggle.tsx +++ b/frontend/src/app/framework/react/Toggle.tsx @@ -16,7 +16,7 @@ export interface ToggleProps { // Set to allow three states. indeterminate?: boolean; - // True if disabled. + // True, if disabled. disabled?: boolean; // The label string. diff --git a/frontend/src/app/framework/react/hooks.ts b/frontend/src/app/framework/react/hooks.ts index e5f293ed..3d825903 100644 --- a/frontend/src/app/framework/react/hooks.ts +++ b/frontend/src/app/framework/react/hooks.ts @@ -209,4 +209,12 @@ export function useDebounce(value: T, debounce: number): T { }, [debounce, value]); return state; +} + +export function useClipboard() { + function writeText(text: string): Promise { + return navigator.clipboard.writeText(text); + } + + return writeText; } \ No newline at end of file diff --git a/frontend/src/app/pages/InternalPage.tsx b/frontend/src/app/pages/InternalPage.tsx index 8029d05e..64f6ca63 100644 --- a/frontend/src/app/pages/InternalPage.tsx +++ b/frontend/src/app/pages/InternalPage.tsx @@ -20,7 +20,7 @@ export const InternalPage = () => { React.useEffect(() => { dispatch(loadProfile()); - dispatch(loadApps()); + dispatch(loadApps({})); dispatch(loadLanguages()); dispatch(loadTimezones()); dispatch(loadMjmlSchema()); diff --git a/frontend/src/app/pages/app/AppAuth.tsx b/frontend/src/app/pages/app/AppAuth.tsx new file mode 100644 index 00000000..1bea0e11 --- /dev/null +++ b/frontend/src/app/pages/app/AppAuth.tsx @@ -0,0 +1,146 @@ +/* + * Notifo.io + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved. + */ + +import { yupResolver } from '@hookform/resolvers/yup'; +import * as React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useDispatch } from 'react-redux'; +import { Button, Card, CardBody, Form } from 'reactstrap'; +import * as Yup from 'yup'; +import { ApiValue, combineUrl, FormAlert, FormError, Loader, Toggle, useEventCallback } from '@app/framework'; +import { AppDetailsDto, AuthSchemeDto, getApiUrl } from '@app/service'; +import { Forms } from '@app/shared/components'; +import { removeAuth, upsertAuth, useApps } from '@app/state'; +import { texts } from '@app/texts'; + +export interface AppAuthProps { + // The app details. + appDetails: AppDetailsDto; +} + +export const AppAuth = (props: AppAuthProps) => { + const { appDetails } = props; + + const dispatch = useDispatch(); + const auth = useApps(x => x.auth); + const [enabled, setEnabled] = React.useState(false); + + React.useEffect(() => { + setEnabled(!!auth?.scheme); + }, [auth?.scheme]); + + const doUpdate = useEventCallback((value: boolean) => { + setEnabled(value); + + if (!value) { + dispatch(removeAuth({ appId: appDetails.id })); + } + }); + + return ( + <> +

{texts.auth.title}

+ + + + + +
+ +
+ + {enabled && + + } +
+
+ + ); +}; + +const FormSchema = Yup.object().shape({ + // Required domain. + domain: Yup.string() + .label(texts.auth.domain).requiredI18n(), + + // Required display name. + displayName: Yup.string() + .label(texts.auth.displayName).requiredI18n(), + + // Required client ID. + clientId: Yup.string() + .label(texts.auth.clientId).requiredI18n(), + + // Required client secret + clientSecret: Yup.string() + .label(texts.auth.clientSecret).requiredI18n(), + + // Required authory as URL. + authority: Yup.string() + .label(texts.auth.authority).requiredI18n(), + + // Valid URL. + signoutRedirectUrl: Yup.string() + .label(texts.auth.signoutRedirectUrl).urlI18n(), +}); + +const AuthForm = ({ appDetails, scheme }: { scheme?: AuthSchemeDto } & AppAuthProps) => { + const dispatch = useDispatch(); + const updateError = useApps(x => x.updatingAuth?.error); + const updateRunning = useApps(x => x.updatingAuth?.isRunning); + + const form = useForm({ resolver: yupResolver(FormSchema), mode: 'onChange' }); + + const doSave = useEventCallback((params: AuthSchemeDto) => { + dispatch(upsertAuth({ appId: appDetails.id, params })); + }); + + React.useEffect(() => { + form.reset(scheme); + }, [scheme, form]); + + return ( + +
+
+ + + + + + + + + + + +
+ +
+ + + +
+ + + +
+ +
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/app/pages/app/AppSettings.tsx b/frontend/src/app/pages/app/AppSettings.tsx index 59c7e08b..d6be51cb 100644 --- a/frontend/src/app/pages/app/AppSettings.tsx +++ b/frontend/src/app/pages/app/AppSettings.tsx @@ -14,11 +14,11 @@ import * as Yup from 'yup'; import { FormError, Loader, useEventCallback } from '@app/framework'; import { AppDetailsDto, UpsertAppDto } from '@app/service'; import { Forms } from '@app/shared/components'; -import { upsertApp, useApps, useCore } from '@app/state'; +import { updateApp, useApps, useCore } from '@app/state'; import { texts } from '@app/texts'; const FormSchema = Yup.object().shape({ - // Required name + // Required name. name: Yup.string() .label(texts.common.name).requiredI18n(), @@ -41,15 +41,15 @@ export const AppSettings = (props: AppSettingsProps) => { const dispatch = useDispatch(); const languages = useCore(x => x.languages); - const upserting = useApps(x => x.upserting); - const upsertingError = useApps(x => x.upsertingError); + const updateError = useApps(x => x.updating?.error); + const updateRunning = useApps(x => x.updating?.isRunning); const languageValues = React.useMemo(() => { return languages.map(x => x.value); }, [languages]); const doSave = useEventCallback((params: UpsertAppDto) => { - dispatch(upsertApp({ appId: appDetails.id, params })); + dispatch(updateApp({ appId: appDetails.id, params })); }); const form = useForm({ resolver: yupResolver(FormSchema), mode: 'onChange' }); @@ -66,7 +66,7 @@ export const AppSettings = (props: AppSettingsProps) => {
-
+
@@ -74,18 +74,18 @@ export const AppSettings = (props: AppSettingsProps) => { label={texts.common.languages} />
-
+
{texts.app.urls}
- +
-
diff --git a/frontend/src/app/pages/app/AppSettingsPage.tsx b/frontend/src/app/pages/app/AppSettingsPage.tsx index 13743102..a4601590 100644 --- a/frontend/src/app/pages/app/AppSettingsPage.tsx +++ b/frontend/src/app/pages/app/AppSettingsPage.tsx @@ -8,6 +8,7 @@ import * as React from 'react'; import { useDispatch } from 'react-redux'; import { loadDetails, useApp, useApps } from '@app/state'; +import { AppAuth } from './AppAuth'; import { AppSettings } from './AppSettings'; import { Contributors } from './Contributors'; @@ -30,6 +31,8 @@ export const AppSettingsPage = () => { + +
); }; diff --git a/frontend/src/app/pages/app/Contributors.tsx b/frontend/src/app/pages/app/Contributors.tsx index 086dafc2..1174f1f3 100644 --- a/frontend/src/app/pages/app/Contributors.tsx +++ b/frontend/src/app/pages/app/Contributors.tsx @@ -23,8 +23,8 @@ export const Contributors = (props: ContributorsProps) => { const dispatch = useDispatch(); const [email, setEmail] = React.useState(''); - const contributorsError = useApps(x => x.contributorsError); - const contributorsUpdating = useApps(x => x.contributorsUpdating); + const updateError = useApps(x => x.updatingContributors?.error); + const updateRunning = useApps(x => x.updatingContributors?.isRunning); const userId = useLogin(x => x.user?.sub); const doSetEmail = useEventCallback((event: React.ChangeEvent) => { @@ -43,7 +43,7 @@ export const Contributors = (props: ContributorsProps) => { dispatch(removeContributor({ appId: appDetails.id, id })); }); - const disabled = appDetails.role !== 'Owner' || contributorsUpdating; + const disabled = appDetails.role !== 'Owner' || updateRunning; return ( <> @@ -51,7 +51,7 @@ export const Contributors = (props: ContributorsProps) => { - + {appDetails.contributors.map(x => diff --git a/frontend/src/app/pages/apps/AppDialog.tsx b/frontend/src/app/pages/apps/AppDialog.tsx index 36c5ffd3..97347774 100644 --- a/frontend/src/app/pages/apps/AppDialog.tsx +++ b/frontend/src/app/pages/apps/AppDialog.tsx @@ -13,7 +13,7 @@ import { Button, Form, Modal, ModalBody, ModalFooter, ModalHeader } from 'reacts import * as Yup from 'yup'; import { FormAlert, FormError, Loader, useEventCallback } from '@app/framework'; import { Forms } from '@app/shared/components'; -import { createApp, CreateAppParams, createAppReset, useApps } from '@app/state'; +import { createApp, CreateAppParams, useApps } from '@app/state'; import { texts } from '@app/texts'; const FormSchema = Yup.object().shape({ @@ -31,25 +31,25 @@ export const AppDialog = (props: AppDialogProps) => { const { onClose } = props; const dispatch = useDispatch(); - const creating = useApps(x => x.creating); - const creatingError = useApps(x => x.creatingError); + const createError = useApps(x => x.creating?.error); + const createRunning = useApps(x => x.creating?.isRunning); const [wasCreating, setWasCreating] = React.useState(false); React.useEffect(() => { - dispatch(createAppReset()); + dispatch(createApp.reset()); }, [dispatch]); React.useEffect(() => { - if (creating) { + if (createRunning) { setWasCreating(true); } - }, [creating]); + }, [createRunning]); React.useEffect(() => { - if (!creating && wasCreating && !creatingError && onClose) { + if (!createRunning && wasCreating && !createError && onClose) { onClose(); } - }, [creating, creatingError, onClose, wasCreating]); + }, [createRunning, createError, onClose, wasCreating]); const doSave = useEventCallback((params: CreateAppParams) => { dispatch(createApp({ params })); @@ -68,19 +68,19 @@ export const AppDialog = (props: AppDialogProps) => { -
+
- + diff --git a/frontend/src/app/pages/email-templates/EmailTemplate.tsx b/frontend/src/app/pages/email-templates/EmailTemplate.tsx index db74af66..f4e70a5b 100644 --- a/frontend/src/app/pages/email-templates/EmailTemplate.tsx +++ b/frontend/src/app/pages/email-templates/EmailTemplate.tsx @@ -62,15 +62,15 @@ export const EmailTemplate = (props: EmailTemplateProps) => { } = props; const dispatch = useDispatch(); - const creatingLanguage = useEmailTemplates(x => x.creatingLanguage); - const creatingLanguageError = useEmailTemplates(x => x.creatingLanguageError); - const deletingLanguage = useEmailTemplates(x => x.deletingLanguage); - const deletingLanguageError = useEmailTemplates(x => x.deletingLanguageError); + const creatingLanguage = useEmailTemplates(x => x.creatingLanguage?.isRunning); + const creatingLanguageError = useEmailTemplates(x => x.creatingLanguage?.error); + const deletingLanguage = useEmailTemplates(x => x.deletingLanguage?.isRunning); + const deletingLanguageError = useEmailTemplates(x => x.deletingLanguage?.error); const schema = useEmailTemplates(x => x.schema); const showHtml = useBooleanObj(true); const updateDialog = useBooleanObj(); - const updatingLanguage = useEmailTemplates(x => x.updatingLanguage); - const updatingLanguageError = useEmailTemplates(x => x.updatingLanguageError); + const updatingLanguage = useEmailTemplates(x => x.updatingLanguage?.isRunning); + const updatingLanguageError = useEmailTemplates(x => x.updatingLanguage?.error); const disabled = updatingLanguage || deletingLanguage; React.useEffect(() => { diff --git a/frontend/src/app/pages/email-templates/EmailTemplatePage.tsx b/frontend/src/app/pages/email-templates/EmailTemplatePage.tsx index 7696c5b9..a6376e79 100644 --- a/frontend/src/app/pages/email-templates/EmailTemplatePage.tsx +++ b/frontend/src/app/pages/email-templates/EmailTemplatePage.tsx @@ -26,14 +26,14 @@ export const EmailTemplatePage = () => { const appId = app.id; const appLanguages = app.languages; const appName = app.name; - const loadingTemplate = useEmailTemplates(x => x.loadingTemplate); - const loadingTemplateError = useEmailTemplates(x => x.loadingTemplateError); + const loadingTemplate = useEmailTemplates(x => x.loadingTemplate?.isRunning); + const loadingTemplateError = useEmailTemplates(x => x.loadingTemplate?.error); const sidebar = useBooleanObj(); const template = useEmailTemplates(x => x.template); const templateId = useParams().templateId!; - const updating = useEmailTemplates(x => x.updating); - const updatingError = useEmailTemplates(x => x.updatingError); - const upserting = useEmailTemplates(x => x.creatingLanguage || x.deletingLanguage || x.updatingLanguage); + const updating = useEmailTemplates(x => x.updating?.isRunning); + const updatingError = useEmailTemplates(x => x.updating?.error); + const upserting = useEmailTemplates(x => x.creatingLanguage?.isRunning || x.deletingLanguage?.isRunning || x.updatingLanguage?.isRunning); const [language, setLanguage] = React.useState(appLanguages[0]); const properties = useSimpleQuery({ diff --git a/frontend/src/app/pages/email-templates/EmailTemplatesPage.tsx b/frontend/src/app/pages/email-templates/EmailTemplatesPage.tsx index 7abeddaf..6b0a5214 100644 --- a/frontend/src/app/pages/email-templates/EmailTemplatesPage.tsx +++ b/frontend/src/app/pages/email-templates/EmailTemplatesPage.tsx @@ -19,13 +19,13 @@ export const EmailTemplatesPage = () => { const dispatch = useDispatch(); const app = useApp()!; const appId = app.id; - const creating = useEmailTemplates(x => x.creating); - const creatingError = useEmailTemplates(x => x.creatingError); - const deletingError = useEmailTemplates(x => x.deletingError); + const creating = useEmailTemplates(x => x.creating?.isRunning); + const creatingError = useEmailTemplates(x => x.creating?.error); + const deletingError = useEmailTemplates(x => x.deleting?.error); const emailTemplates = useEmailTemplates(x => x.templates); React.useEffect(() => { - dispatch(loadEmailTemplates(appId)); + dispatch(loadEmailTemplates({ appId })); }, [dispatch, appId]); React.useEffect(() => { diff --git a/frontend/src/app/pages/events/EventsPage.tsx b/frontend/src/app/pages/events/EventsPage.tsx index 2d5cc521..01ec60a9 100644 --- a/frontend/src/app/pages/events/EventsPage.tsx +++ b/frontend/src/app/pages/events/EventsPage.tsx @@ -29,15 +29,15 @@ export const EventsPage = () => { const [channels, setChannels] = React.useState([]); React.useEffect(() => { - dispatch(loadEvents(appId, {}, undefined, channels)); + dispatch(loadEvents({ appId, channels, query: { search: '', page: 0 } })); }, [dispatch, appId, channels]); const doRefresh = useEventCallback(() => { - dispatch(loadEvents(appId, undefined, undefined, channels)); + dispatch(loadEvents({ appId, channels })); }); const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadEvents(appId, q, undefined, channels)); + dispatch(loadEvents({ appId, channels, ...q })); }); return ( diff --git a/frontend/src/app/pages/integrations/IntegrationDialog.tsx b/frontend/src/app/pages/integrations/IntegrationDialog.tsx index 0e9c36f3..66c4baf2 100644 --- a/frontend/src/app/pages/integrations/IntegrationDialog.tsx +++ b/frontend/src/app/pages/integrations/IntegrationDialog.tsx @@ -51,8 +51,8 @@ export const IntegrationDialog = (props: IntegrationDialogProps) => { } = props; const dispatch = useDispatch(); - const upserting = useIntegrations(x => x.upserting); - const upsertingError = useIntegrations(x => x.upsertingError); + const upserting = useIntegrations(x => x.upserting?.isRunning); + const upsertingError = useIntegrations(x => x.upserting?.error); const wasUpserting = React.useRef(false); React.useEffect(() => { diff --git a/frontend/src/app/pages/integrations/IntegrationsPage.tsx b/frontend/src/app/pages/integrations/IntegrationsPage.tsx index 436d00ca..f39ac5a8 100644 --- a/frontend/src/app/pages/integrations/IntegrationsPage.tsx +++ b/frontend/src/app/pages/integrations/IntegrationsPage.tsx @@ -31,8 +31,8 @@ export const IntegrationsPage = () => { const appId = app.id; const configured = useIntegrations(x => x.configured || DEFAULTS); const definitions = useIntegrations(x => x.supported || DEFAULTS); - const loading = useIntegrations(x => x.loading); - const loadingError = useIntegrations(x => x.loadingError); + const loading = useIntegrations(x => x.loading?.isRunning); + const loadingError = useIntegrations(x => x.loading?.error); const [selected, setSelected] = React.useState(); React.useEffect(() => { diff --git a/frontend/src/app/pages/log/LogPage.tsx b/frontend/src/app/pages/log/LogPage.tsx index 2a993fb6..8f1cd5f1 100644 --- a/frontend/src/app/pages/log/LogPage.tsx +++ b/frontend/src/app/pages/log/LogPage.tsx @@ -25,20 +25,20 @@ export const LogPage = () => { const dispatch = useDispatch(); const app = useApp()!; const appId = app.id; - const logEntries = useLog(x => x.entries); + const logEntries = useLog(x => x.log); const userId = useParams().userId!; const [systems, setSystems] = React.useState([]); React.useEffect(() => { - dispatch(loadLog(appId, {}, false, systems, userId)); + dispatch(loadLog({ appId, systems, userId, query: { search: '', page: 0 } })); }, [dispatch, appId, systems, userId]); const doRefresh = useEventCallback(() => { - dispatch(loadLog(appId, undefined, false, systems, userId)); + dispatch(loadLog({ appId, systems, userId })); }); - const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadLog(appId, q, false, systems, userId)); + const doLoad = useEventCallback((query?: Partial) => { + dispatch(loadLog({ appId, systems, userId, query })); }); return ( diff --git a/frontend/src/app/pages/media/MediaPage.tsx b/frontend/src/app/pages/media/MediaPage.tsx index 4947807b..ce184b36 100644 --- a/frontend/src/app/pages/media/MediaPage.tsx +++ b/frontend/src/app/pages/media/MediaPage.tsx @@ -22,15 +22,15 @@ export const MediaPage = () => { const media = useMedia(x => x.media); React.useEffect(() => { - dispatch(loadMedia(appId)); + dispatch(loadMedia({ appId, query: { search: '', page: 0 } })); }, [dispatch, appId]); const doRefresh = useEventCallback(() => { - dispatch(loadMedia(appId)); + dispatch(loadMedia({ appId })); }); - const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadMedia(appId, q)); + const doLoad = useEventCallback((query?: Partial) => { + dispatch(loadMedia({ appId, query })); }); const doDelete = useEventCallback((media: MediaDto) => { diff --git a/frontend/src/app/pages/messaging-templates/MessagingTemplatePage.tsx b/frontend/src/app/pages/messaging-templates/MessagingTemplatePage.tsx index 8607466e..911b3397 100644 --- a/frontend/src/app/pages/messaging-templates/MessagingTemplatePage.tsx +++ b/frontend/src/app/pages/messaging-templates/MessagingTemplatePage.tsx @@ -30,12 +30,12 @@ export const MessagingTemplatePage = () => { const app = useApp()!; const appId = app.id; const appLanguages = app.languages; - const loadingTemplate = useMessagingTemplates(x => x.loadingTemplate); - const loadingTemplateError = useMessagingTemplates(x => x.loadingTemplateError); + const loadingTemplate = useMessagingTemplates(x => x.loadingTemplate?.isRunning); + const loadingTemplateError = useMessagingTemplates(x => x.loadingTemplate?.error); const template = useMessagingTemplates(x => x.template); const templateId = useParams().templateId!; - const updating = useMessagingTemplates(x => x.updating); - const updatingError = useMessagingTemplates(x => x.updatingError); + const updating = useMessagingTemplates(x => x.updating?.isRunning); + const updatingError = useMessagingTemplates(x => x.updating?.error); const [language, setLanguage] = React.useState(appLanguages[0]); const properties = useSimpleQuery({ diff --git a/frontend/src/app/pages/messaging-templates/MessagingTemplatesPage.tsx b/frontend/src/app/pages/messaging-templates/MessagingTemplatesPage.tsx index 182057f4..1d166b97 100644 --- a/frontend/src/app/pages/messaging-templates/MessagingTemplatesPage.tsx +++ b/frontend/src/app/pages/messaging-templates/MessagingTemplatesPage.tsx @@ -19,13 +19,13 @@ export const MessagingTemplatesPage = () => { const dispatch = useDispatch(); const app = useApp()!; const appId = app.id; - const creating = useMessagingTemplates(x => x.creating); - const creatingError = useMessagingTemplates(x => x.creatingError); - const deletingError = useMessagingTemplates(x => x.deletingError); + const creating = useMessagingTemplates(x => x.creating?.isRunning); + const creatingError = useMessagingTemplates(x => x.creating?.error); + const deletingError = useMessagingTemplates(x => x.deleting?.error); const messagingTemplates = useMessagingTemplates(x => x.templates); React.useEffect(() => { - dispatch(loadMessagingTemplates(appId)); + dispatch(loadMessagingTemplates({ appId })); }, [dispatch, appId]); React.useEffect(() => { diff --git a/frontend/src/app/pages/publish/PublishDialog.tsx b/frontend/src/app/pages/publish/PublishDialog.tsx index 510ff4d6..0625348a 100644 --- a/frontend/src/app/pages/publish/PublishDialog.tsx +++ b/frontend/src/app/pages/publish/PublishDialog.tsx @@ -80,8 +80,8 @@ const PublishDialogInner = () => { const appId = app.id; const appLanguages = app.languages; const dialogValues = usePublish(x => x.dialogValues || EMPTY_VALUES); - const publishing = usePublish(x => x.publishing); - const publishingError = usePublish(x => x.publishingError); + const publishing = usePublish(x => x.publishing?.isRunning); + const publishingError = usePublish(x => x.publishing?.error); const templates = useTemplates(x => x.templates); const wasPublishing = usePrevious(publishing); const [language, setLanguage] = React.useState(appLanguages[0]); @@ -96,7 +96,7 @@ const PublishDialogInner = () => { React.useEffect(() => { if (!templates.isLoaded) { - dispatch(loadTemplates(appId)); + dispatch(loadTemplates({ appId })); } }, [appId, dispatch, templates.isLoaded]); diff --git a/frontend/src/app/pages/sms-templates/SmsTemplatePage.tsx b/frontend/src/app/pages/sms-templates/SmsTemplatePage.tsx index faa7bee6..2abb3773 100644 --- a/frontend/src/app/pages/sms-templates/SmsTemplatePage.tsx +++ b/frontend/src/app/pages/sms-templates/SmsTemplatePage.tsx @@ -30,12 +30,12 @@ export const SmsTemplatePage = () => { const app = useApp()!; const appId = app.id; const appLanguages = app.languages; - const loadingTemplate = useSmsTemplates(x => x.loadingTemplate); - const loadingTemplateError = useSmsTemplates(x => x.loadingTemplateError); + const loadingTemplate = useSmsTemplates(x => x.loadingTemplate?.isRunning); + const loadingTemplateError = useSmsTemplates(x => x.loadingTemplate?.error); const template = useSmsTemplates(x => x.template); const templateId = useParams().templateId!; - const updating = useSmsTemplates(x => x.updating); - const updatingError = useSmsTemplates(x => x.updatingError); + const updating = useSmsTemplates(x => x.updating?.isRunning); + const updatingError = useSmsTemplates(x => x.updating?.error); const [language, setLanguage] = React.useState(appLanguages[0]); const properties = useSimpleQuery({ diff --git a/frontend/src/app/pages/sms-templates/SmsTemplatesPage.tsx b/frontend/src/app/pages/sms-templates/SmsTemplatesPage.tsx index b783fd12..7b57d2ab 100644 --- a/frontend/src/app/pages/sms-templates/SmsTemplatesPage.tsx +++ b/frontend/src/app/pages/sms-templates/SmsTemplatesPage.tsx @@ -19,13 +19,13 @@ export const SmsTemplatesPage = () => { const dispatch = useDispatch(); const app = useApp()!; const appId = app.id; - const creating = useSmsTemplates(x => x.creating); - const creatingError = useSmsTemplates(x => x.creatingError); - const deletingError = useSmsTemplates(x => x.deletingError); + const creating = useSmsTemplates(x => x.creating?.isRunning); + const creatingError = useSmsTemplates(x => x.creating?.error); + const deletingError = useSmsTemplates(x => x.deleting?.error); const smsTemplates = useSmsTemplates(x => x.templates); React.useEffect(() => { - dispatch(loadSmsTemplates(appId)); + dispatch(loadSmsTemplates({ appId })); }, [dispatch, appId]); React.useEffect(() => { diff --git a/frontend/src/app/pages/system-users/SystemUserDialog.tsx b/frontend/src/app/pages/system-users/SystemUserDialog.tsx index 3f0b4c31..4631964e 100644 --- a/frontend/src/app/pages/system-users/SystemUserDialog.tsx +++ b/frontend/src/app/pages/system-users/SystemUserDialog.tsx @@ -44,8 +44,8 @@ export const SystemUserDialog = (props: SystemUserDialogProps) => { const { onClose, user } = props; const dispatch = useDispatch(); - const upserting = useSystemUsers(x => x.upserting); - const upsertingError = useSystemUsers(x => x.upsertingError); + const upserting = useSystemUsers(x => x.upserting?.isRunning); + const upsertingError = useSystemUsers(x => x.upserting?.error); const [wasUpserting, setWasUpserting] = React.useState(false); React.useEffect(() => { diff --git a/frontend/src/app/pages/system-users/SystemUsersPage.tsx b/frontend/src/app/pages/system-users/SystemUsersPage.tsx index 8815992a..850d4379 100644 --- a/frontend/src/app/pages/system-users/SystemUsersPage.tsx +++ b/frontend/src/app/pages/system-users/SystemUsersPage.tsx @@ -24,15 +24,15 @@ export const SystemUsersPage = () => { const [currentSystemUser, setCurrentSystemUser] = React.useState(); React.useEffect(() => { - dispatch(loadSystemUsers({})); + dispatch(loadSystemUsers({ query: { search: '', page: 0 } })); }, [dispatch]); const doRefresh = useEventCallback(() => { - dispatch(loadSystemUsers()); + dispatch(loadSystemUsers({})); }); - const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadSystemUsers(q)); + const doLoad = useEventCallback((query?: Partial) => { + dispatch(loadSystemUsers({ query })); }); const doLock = useEventCallback((user: SystemUserDto) => { diff --git a/frontend/src/app/pages/templates/TemplateForm.tsx b/frontend/src/app/pages/templates/TemplateForm.tsx index 5aabbba5..302abe92 100644 --- a/frontend/src/app/pages/templates/TemplateForm.tsx +++ b/frontend/src/app/pages/templates/TemplateForm.tsx @@ -56,8 +56,8 @@ export const TemplateForm = (props: TemplateFormProps) => { const app = useApp()!; const appId = app.id; const appLanguages = app.languages; - const upserting = useTemplates(x => x.upserting); - const upsertingError = useTemplates(x => x.upsertingError); + const upserting = useTemplates(x => x.upserting?.isRunning); + const upsertingError = useTemplates(x => x.upserting?.error); const [viewFullscreen, setViewFullscreen] = React.useState(false); const doPublish = useEventCallback((params: TemplateDto) => { diff --git a/frontend/src/app/pages/templates/TemplatesList.tsx b/frontend/src/app/pages/templates/TemplatesList.tsx index 8cbc772e..68b7b314 100644 --- a/frontend/src/app/pages/templates/TemplatesList.tsx +++ b/frontend/src/app/pages/templates/TemplatesList.tsx @@ -29,11 +29,11 @@ export const TemplatesList = (props: TemplateListProps) => { const templates = useTemplates(x => x.templates); React.useEffect(() => { - dispatch(loadTemplates(appId)); + dispatch(loadTemplates({ appId, query: { search: '', page: 0 } })); }, [dispatch, appId]); const doRefresh = useEventCallback(() => { - dispatch(loadTemplates(appId)); + dispatch(loadTemplates({ appId })); }); const doNew = useEventCallback(() => { diff --git a/frontend/src/app/pages/templates/TemplatesPage.tsx b/frontend/src/app/pages/templates/TemplatesPage.tsx index a9f75cfa..817ab25c 100644 --- a/frontend/src/app/pages/templates/TemplatesPage.tsx +++ b/frontend/src/app/pages/templates/TemplatesPage.tsx @@ -28,7 +28,7 @@ export const TemplatesPage = () => { }, [templates, templateCode]); React.useEffect(() => { - dispatch(loadTemplates(appId)); + dispatch(loadTemplates({ appId })); }, [dispatch, appId]); return ( diff --git a/frontend/src/app/pages/topics/TopicDialog.tsx b/frontend/src/app/pages/topics/TopicDialog.tsx index f92b1953..affdca41 100644 --- a/frontend/src/app/pages/topics/TopicDialog.tsx +++ b/frontend/src/app/pages/topics/TopicDialog.tsx @@ -46,8 +46,8 @@ export const TopicDialog = (props: TopicDialogProps) => { const app = useApp()!; const appId = app.id; const appLanguages = app.languages; - const upserting = useTopics(x => x.upserting); - const upsertingError = useTopics(x => x.upsertingError); + const upserting = useTopics(x => x.upserting?.isRunning); + const upsertingError = useTopics(x => x.upserting?.error); const [language, setLanguage] = React.useState(appLanguages[0]); const [wasUpserting, setWasUpserting] = React.useState(false); diff --git a/frontend/src/app/pages/topics/TopicsPage.tsx b/frontend/src/app/pages/topics/TopicsPage.tsx index 53c5c41d..6b77afae 100644 --- a/frontend/src/app/pages/topics/TopicsPage.tsx +++ b/frontend/src/app/pages/topics/TopicsPage.tsx @@ -29,15 +29,15 @@ export const TopicsPage = () => { const [showCounters, setShowCounters] = useSavedState(false, 'show.counters'); React.useEffect(() => { - dispatch(loadTopics(appId, currentScope, {})); + dispatch(loadTopics({ appId, scope: currentScope, query: { search: '', page: 0 } })); }, [dispatch, appId, currentScope]); const doRefresh = useEventCallback(() => { - dispatch(loadTopics(appId, currentScope)); + dispatch(loadTopics({ appId, scope: currentScope })); }); - const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadTopics(appId, currentScope, q)); + const doLoad = useEventCallback((query?: Partial) => { + dispatch(loadTopics({ appId, scope: currentScope, query })); }); const doDelete = useEventCallback((topic: TopicDto) => { diff --git a/frontend/src/app/pages/user/Notifications.tsx b/frontend/src/app/pages/user/Notifications.tsx index 30e3102b..14519edc 100644 --- a/frontend/src/app/pages/user/Notifications.tsx +++ b/frontend/src/app/pages/user/Notifications.tsx @@ -37,15 +37,15 @@ export const Notifications = (props: NotificationsProps) => { const [channels, setChannels] = React.useState([]); React.useEffect(() => { - dispatch(loadNotifications(appId, userId, {}, undefined, channels)); + dispatch(loadNotifications({ appId, userId, channels, query: { search: '', page: 0 } })); }, [dispatch, appId, userId, channels]); const doRefresh = useEventCallback(() => { - dispatch(loadNotifications(appId, userId, undefined, undefined, channels)); + dispatch(loadNotifications({ appId, userId, channels })); }); - const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadNotifications(appId, userId, q, undefined, channels)); + const doLoad = useEventCallback((query?: Partial) => { + dispatch(loadNotifications({ appId, userId, channels, query })); }); return ( diff --git a/frontend/src/app/pages/user/SubscriptionDialog.tsx b/frontend/src/app/pages/user/SubscriptionDialog.tsx index 27a7a9ff..def1038b 100644 --- a/frontend/src/app/pages/user/SubscriptionDialog.tsx +++ b/frontend/src/app/pages/user/SubscriptionDialog.tsx @@ -40,8 +40,8 @@ export const SubscriptionDialog = (props: SubscriptionDialogProps) => { const dispatch = useDispatch(); const app = useApp()!; const appId = app.id; - const upserting = useSubscriptions(x => x.upserting); - const upsertingError = useSubscriptions(x => x.upsertingError); + const upserting = useSubscriptions(x => x.upserting?.isRunning); + const upsertingError = useSubscriptions(x => x.upserting?.error); const [wasUpserting, setWasUpserting] = React.useState(false); React.useEffect(() => { diff --git a/frontend/src/app/pages/user/Subscriptions.tsx b/frontend/src/app/pages/user/Subscriptions.tsx index ce356ac5..537593c8 100644 --- a/frontend/src/app/pages/user/Subscriptions.tsx +++ b/frontend/src/app/pages/user/Subscriptions.tsx @@ -35,15 +35,15 @@ export const Subscriptions = (props: SubscriptionsProps) => { const [editSubscription, setEditSubscription] = React.useState(); React.useEffect(() => { - dispatch(loadSubscriptions(appId, userId, {})); + dispatch(loadSubscriptions({ appId, userId, query: { search: '', page: 0 } })); }, [dispatch, appId, userId]); const doRefresh = useEventCallback(() => { - dispatch(loadSubscriptions(appId, userId)); + dispatch(loadSubscriptions({ appId, userId })); }); - const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadSubscriptions(appId, userId, q)); + const doLoad = useEventCallback((query: Partial) => { + dispatch(loadSubscriptions({ appId, userId, query })); }); const doDelete = useEventCallback((subscription: SubscriptionDto) => { diff --git a/frontend/src/app/pages/user/UserPage.tsx b/frontend/src/app/pages/user/UserPage.tsx index b0145270..42fd0ddd 100644 --- a/frontend/src/app/pages/user/UserPage.tsx +++ b/frontend/src/app/pages/user/UserPage.tsx @@ -23,7 +23,7 @@ export const UserPage = () => { const app = useApp()!; const appId = app.id; const dialogEdit = useBooleanObj(); - const loading = useUsers(x => x.loadingUser); + const loading = useUsers(x => x.loadingUser?.isRunning); const user = useUsers(x => x.user)!; const userId = useParams().userId!; const [activeTab, setActiveTab] = React.useState('notifications'); diff --git a/frontend/src/app/pages/users/UserDialog.tsx b/frontend/src/app/pages/users/UserDialog.tsx index 58c4e29c..ec596268 100644 --- a/frontend/src/app/pages/users/UserDialog.tsx +++ b/frontend/src/app/pages/users/UserDialog.tsx @@ -31,8 +31,8 @@ export const UserDialog = (props: UserDialogProps) => { const appId = app.id; const coreLanguages = useCore(x => x.languages); const coreTimezones = useCore(x => x.timezones); - const upserting = useUsers(x => x.upserting); - const upsertingError = useUsers(x => x.upsertingError); + const upserting = useUsers(x => x.upserting?.isRunning); + const upsertingError = useUsers(x => x.upserting?.error); const [dialogUser, setDialogUser] = React.useState(props.user); const [dialogTab, setDialogTab] = React.useState(0); const [wasUpserting, setWasUpserting] = React.useState(false); diff --git a/frontend/src/app/pages/users/UsersPage.tsx b/frontend/src/app/pages/users/UsersPage.tsx index 8589ca0c..5d3d7203 100644 --- a/frontend/src/app/pages/users/UsersPage.tsx +++ b/frontend/src/app/pages/users/UsersPage.tsx @@ -27,15 +27,15 @@ export const UsersPage = () => { const [showCounters, setShowCounters] = useSavedState(false, 'show.counters'); React.useEffect(() => { - dispatch(loadUsers(appId, {})); + dispatch(loadUsers({ appId, query: { search: '', page: 0 } })); }, [dispatch, appId]); const doRefresh = useEventCallback(() => { - dispatch(loadUsers(appId)); + dispatch(loadUsers({ appId })); }); - const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadUsers(appId, q)); + const doLoad = useEventCallback((query?: Partial) => { + dispatch(loadUsers({ appId, query })); }); const doDelete = useEventCallback((user: UserDto) => { diff --git a/frontend/src/app/service/service.ts b/frontend/src/app/service/service.ts index a51f0b96..e60de631 100644 --- a/frontend/src/app/service/service.ts +++ b/frontend/src/app/service/service.ts @@ -3838,7 +3838,7 @@ export class EmailTemplatesClient { /** * Get the HTML preview for a channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param id The template ID. * @return Channel template preview returned. */ @@ -3899,7 +3899,7 @@ export class EmailTemplatesClient { /** * Render a preview for a email template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param request The template to render. * @return Template rendered. */ @@ -3962,7 +3962,7 @@ export class EmailTemplatesClient { /** * Get the channel templates. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param query (optional) The optional query to search for items. * @param take (optional) The number of items to return. * @param skip (optional) The number of items to skip. @@ -4027,7 +4027,7 @@ export class EmailTemplatesClient { /** * Create a channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param request The request object. * @return Channel template created. */ @@ -4142,7 +4142,7 @@ export class EmailTemplatesClient { /** * Get the channel template by id. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param id The template ID. * @return Channel templates returned. */ @@ -4198,7 +4198,7 @@ export class EmailTemplatesClient { /** * Create an app template language. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param request The request object. * @return Channel template created. @@ -4265,7 +4265,7 @@ export class EmailTemplatesClient { /** * Update an app template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param request The request object. */ @@ -4331,7 +4331,7 @@ export class EmailTemplatesClient { /** * Delete a channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template ID. * @return Channel template deleted. */ @@ -4390,7 +4390,7 @@ export class EmailTemplatesClient { /** * Update a channel template language. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param language The language. * @param request The request object. @@ -4460,7 +4460,7 @@ export class EmailTemplatesClient { /** * Delete a language channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template ID. * @param language The language. */ @@ -4536,7 +4536,7 @@ export class MessagingTemplatesClient { /** * Get the channel templates. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param query (optional) The optional query to search for items. * @param take (optional) The number of items to return. * @param skip (optional) The number of items to skip. @@ -4601,7 +4601,7 @@ export class MessagingTemplatesClient { /** * Create a channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param request The request object. * @return Channel template created. */ @@ -4716,7 +4716,7 @@ export class MessagingTemplatesClient { /** * Get the channel template by id. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param id The template ID. * @return Channel templates returned. */ @@ -4772,7 +4772,7 @@ export class MessagingTemplatesClient { /** * Create an app template language. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param request The request object. * @return Channel template created. @@ -4839,7 +4839,7 @@ export class MessagingTemplatesClient { /** * Update an app template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param request The request object. */ @@ -4905,7 +4905,7 @@ export class MessagingTemplatesClient { /** * Delete a channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template ID. * @return Channel template deleted. */ @@ -4964,7 +4964,7 @@ export class MessagingTemplatesClient { /** * Update a channel template language. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param language The language. * @param request The request object. @@ -5034,7 +5034,7 @@ export class MessagingTemplatesClient { /** * Delete a language channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template ID. * @param language The language. */ @@ -5110,7 +5110,7 @@ export class SmsTemplatesClient { /** * Get the channel templates. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param query (optional) The optional query to search for items. * @param take (optional) The number of items to return. * @param skip (optional) The number of items to skip. @@ -5175,7 +5175,7 @@ export class SmsTemplatesClient { /** * Create a channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param request The request object. * @return Channel template created. */ @@ -5290,7 +5290,7 @@ export class SmsTemplatesClient { /** * Get the channel template by id. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param id The template ID. * @return Channel templates returned. */ @@ -5346,7 +5346,7 @@ export class SmsTemplatesClient { /** * Create an app template language. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param request The request object. * @return Channel template created. @@ -5413,7 +5413,7 @@ export class SmsTemplatesClient { /** * Update an app template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param request The request object. */ @@ -5479,7 +5479,7 @@ export class SmsTemplatesClient { /** * Delete a channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template ID. * @return Channel template deleted. */ @@ -5538,7 +5538,7 @@ export class SmsTemplatesClient { /** * Update a channel template language. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template code. * @param language The language. * @param request The request object. @@ -5608,7 +5608,7 @@ export class SmsTemplatesClient { /** * Delete a language channel template. - * @param appId The id of the app where the templates belong to. + * @param appId The ID of the app where the templates belong to. * @param code The template ID. * @param language The language. */ @@ -5782,9 +5782,9 @@ export class AppsClient { } /** - * Get app by id. - * @param appId The id of the app. - * @return Apps returned. + * Get app by ID. + * @param appId The ID of the app. + * @return App returned. */ getApp(appId: string, signal?: AbortSignal): Promise { let url_ = this.baseUrl + "/api/apps/{appId}"; @@ -5896,9 +5896,178 @@ export class AppsClient { return Promise.resolve(null as any); } + /** + * Get app auth settings by ID. + * @param appId The ID of the app. + * @return App auth settings returned. + */ + getAuthScheme(appId: string, signal?: AbortSignal): Promise { + let url_ = this.baseUrl + "/api/apps/{appId}/auth"; + if (appId === undefined || appId === null) + throw new Error("The parameter 'appId' must be defined."); + url_ = url_.replace("{appId}", encodeURIComponent("" + appId)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + signal, + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetAuthScheme(_response); + }); + } + + protected processGetAuthScheme(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AuthSchemeResponseDto; + return result200; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + return throwException("App not found.", status, _responseText, _headers); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + result500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ErrorDto; + return throwException("Operation failed.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * Updates the auth settings of the app. + * @param appId The ID of the app. + * @param request The request object. + * @return App auth settings returned. + */ + upsertAuthScheme(appId: string, request: AuthSchemeDto, signal?: AbortSignal): Promise { + let url_ = this.baseUrl + "/api/apps/{appId}/auth"; + if (appId === undefined || appId === null) + throw new Error("The parameter 'appId' must be defined."); + url_ = url_.replace("{appId}", encodeURIComponent("" + appId)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(request); + + let options_: RequestInit = { + body: content_, + method: "PUT", + signal, + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processUpsertAuthScheme(_response); + }); + } + + protected processUpsertAuthScheme(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as AuthSchemeResponseDto; + return result200; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + return throwException("App not found.", status, _responseText, _headers); + }); + } else if (status === 400) { + return response.text().then((_responseText) => { + let result400: any = null; + result400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ErrorDto; + return throwException("Validation error.", status, _responseText, _headers, result400); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + result500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ErrorDto; + return throwException("Operation failed.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * Deletes the auth settings of the app. + * @param appId The ID of the app. + */ + deleteAuthScheme(appId: string, signal?: AbortSignal): Promise { + let url_ = this.baseUrl + "/api/apps/{appId}/auth"; + if (appId === undefined || appId === null) + throw new Error("The parameter 'appId' must be defined."); + url_ = url_.replace("{appId}", encodeURIComponent("" + appId)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "DELETE", + signal, + headers: { + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processDeleteAuthScheme(_response); + }); + } + + protected processDeleteAuthScheme(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 204) { + return response.text().then((_responseText) => { + return; + }); + } else if (status === 404) { + return response.text().then((_responseText) => { + return throwException("App not found.", status, _responseText, _headers); + }); + } else if (status === 400) { + return response.text().then((_responseText) => { + let result400: any = null; + result400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ErrorDto; + return throwException("Validation error.", status, _responseText, _headers, result400); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + result500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ErrorDto; + return throwException("Operation failed.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + /** * Add an app contributor. - * @param appId The id of the app. + * @param appId The ID of the app. * @param request The request object. * @return Apps returned. */ @@ -5961,7 +6130,7 @@ export class AppsClient { /** * Delete an app contributor. - * @param appId The id of the app. + * @param appId The ID of the app. * @param contributorId The contributor to remove. * @return Apps returned. */ @@ -6023,7 +6192,7 @@ export class AppsClient { /** * Get the app integrations. - * @param appId The id of the app where the integrations belong to. + * @param appId The ID of the app where the integrations belong to. * @return App email templates returned. */ getIntegrations(appId: string, signal?: AbortSignal): Promise { @@ -6075,7 +6244,7 @@ export class AppsClient { /** * Create an app integrations. - * @param appId The id of the app where the integration belong to. + * @param appId The ID of the app where the integration belong to. * @param request The request object. * @return App integration created. */ @@ -6138,8 +6307,8 @@ export class AppsClient { /** * Update an app integration. - * @param appId The id of the app where the integration belong to. - * @param id The id of the integration. + * @param appId The ID of the app where the integration belong to. + * @param id The ID of the integration. * @param request The request object. * @return App integration updated. */ @@ -6202,8 +6371,8 @@ export class AppsClient { /** * Delete an app integration. - * @param appId The id of the app where the email templates belong to. - * @param id The id of the integration. + * @param appId The ID of the app where the email templates belong to. + * @param id The ID of the integration. * @return App integration deleted. */ deleteIntegration(appId: string, id: string, signal?: AbortSignal): Promise { @@ -6419,7 +6588,7 @@ export interface ListResponseDtoOfUserDto { } export interface UserDto { - /** The id of the user. */ + /** The ID of the user. */ id: string; /** The unique api key for the user. */ apiKey: string; @@ -6490,7 +6659,7 @@ export interface UpsertUsersDto { } export interface UpsertUserDto { - /** The id of the user. */ + /** The ID of the user. */ id?: string | undefined; /** The full name of the user. */ fullName?: string | undefined; @@ -6637,7 +6806,7 @@ export interface ListResponseDtoOfSystemUserDto { } export interface SystemUserDto { - /** The id of the user. */ + /** The ID of the user. */ id: string; /** The email of the user. Unique value. */ email: string; @@ -6680,7 +6849,7 @@ export interface ListResponseDtoOfUserNotificationDetailsDto { } export interface UserNotificationBaseDto { - /** The id of the notification. */ + /** The ID of the notification. */ id: string; /** The subject of the notification in the language of the user. */ subject: string; @@ -6792,9 +6961,9 @@ export interface UserNotificationDto extends UserNotificationBaseDto { export type DeviceNotificationsQueryScope = "Seen" | "Unseen" | "All"; export interface TrackNotificationDto { - /** The id of the noitifications to mark as confirmed. */ + /** The ID of the noitifications to mark as confirmed. */ confirmed?: string | undefined; - /** The id of the noitifications to mark as seen. */ + /** The ID of the noitifications to mark as seen. */ seen?: string[] | undefined; /** The channel name. */ channel?: string | undefined; @@ -6896,7 +7065,7 @@ export interface ListResponseDtoOfEventDto { } export interface EventDto { - /** The id of the event. */ + /** The ID of the event. */ id: string; /** The topic path. */ topic: string; @@ -7019,7 +7188,7 @@ export interface ListResponseDtoOfChannelTemplateDto { } export interface ChannelTemplateDto { - /** The id of the template. */ + /** The ID of the template. */ id: string; /** The optional name of the template. */ name?: string | undefined; @@ -7048,7 +7217,7 @@ export interface TemplatePropertyDto { export type LiquidPropertyType = "Array" | "String" | "Number" | "Boolean" | "Object"; export interface ChannelTemplateDetailsDtoOfEmailTemplateDto { - /** The id of the template. */ + /** The ID of the template. */ id: string; /** The optional name of the template. */ name?: string | undefined; @@ -7093,7 +7262,7 @@ export interface UpdateChannelTemplateDtoOfEmailTemplateDto { } export interface ChannelTemplateDetailsDtoOfMessagingTemplateDto { - /** The id of the template. */ + /** The ID of the template. */ id: string; /** The optional name of the template. */ name?: string | undefined; @@ -7122,7 +7291,7 @@ export interface UpdateChannelTemplateDtoOfMessagingTemplateDto { } export interface ChannelTemplateDetailsDtoOfSmsTemplateDto { - /** The id of the template. */ + /** The ID of the template. */ id: string; /** The optional name of the template. */ name?: string | undefined; @@ -7151,7 +7320,7 @@ export interface UpdateChannelTemplateDtoOfSmsTemplateDto { } export interface AppDto { - /** The id of the app. */ + /** The ID of the app. */ id: string; /** The app name. */ name: string; @@ -7170,7 +7339,7 @@ export interface AppDto { } export interface AppDetailsDto { - /** The id of the app. */ + /** The ID of the app. */ id: string; /** The app name. */ name: string; @@ -7193,7 +7362,7 @@ export interface AppDetailsDto { } export interface AppContributorDto { - /** The id of the user. */ + /** The ID of the user. */ userId: string; /** The name of the user. */ userName: string; @@ -7201,6 +7370,26 @@ export interface AppContributorDto { role: string; } +export interface AuthSchemeResponseDto { + /** The auth scheme if configured. */ + scheme?: AuthSchemeDto | undefined; +} + +export interface AuthSchemeDto { + /** The domain name of your user accounts. */ + domain?: string; + /** The display name for buttons. */ + displayName: string; + /** The client ID. */ + clientId: string; + /** The client secret. */ + clientSecret: string; + /** The authority URL. */ + authority: string; + /** The URL to redirect after a signout. */ + signoutRedirectUrl?: string | undefined; +} + export interface UpsertAppDto { /** The app name. */ name?: string | undefined; @@ -7302,7 +7491,7 @@ export interface IntegrationPropertyDto { export type PropertyType = "Text" | "Number" | "MultilineText" | "Password" | "Boolean"; export interface IntegrationCreatedDto { - /** The id of the integration. */ + /** The ID of the integration. */ id: string; /** The integration. */ integration: ConfiguredIntegrationDto; diff --git a/frontend/src/app/shared/components/EmailTemplateInput.tsx b/frontend/src/app/shared/components/EmailTemplateInput.tsx index e736bd43..ddf40dec 100644 --- a/frontend/src/app/shared/components/EmailTemplateInput.tsx +++ b/frontend/src/app/shared/components/EmailTemplateInput.tsx @@ -20,7 +20,7 @@ export const EmailTemplateInput = (props: FormEditorProps) => { React.useEffect(() => { if (!templates.isLoaded) { - dispatch(loadEmailTemplates(appId)); + dispatch(loadEmailTemplates({ appId })); } }, [dispatch, appId, templates.isLoaded]); diff --git a/frontend/src/app/shared/components/Forms.tsx b/frontend/src/app/shared/components/Forms.tsx index 7f1be305..cfd6c64d 100644 --- a/frontend/src/app/shared/components/Forms.tsx +++ b/frontend/src/app/shared/components/Forms.tsx @@ -35,7 +35,7 @@ export interface FormEditorProps { // The layout. vertical?: boolean; - // True if disabled. + // True, if disabled. disabled?: boolean; // True to hide the error. @@ -51,7 +51,7 @@ export interface ArrayFormProps extends FormEditorProps { } export interface BooleanFormProps extends FormEditorProps { - // True if 3 states are allowed. + // True, if 3 states are allowed. indeterminate?: boolean; // True to provide the value as string. diff --git a/frontend/src/app/shared/components/MediaCard.tsx b/frontend/src/app/shared/components/MediaCard.tsx index 0345e39b..335ee290 100644 --- a/frontend/src/app/shared/components/MediaCard.tsx +++ b/frontend/src/app/shared/components/MediaCard.tsx @@ -16,7 +16,7 @@ export interface MediaCardProps { // The media. media: MediaDto; - // True if selected. + // True, if selected. selected?: boolean; // When clicked. diff --git a/frontend/src/app/shared/components/MediaPicker.tsx b/frontend/src/app/shared/components/MediaPicker.tsx index 987f37bf..f321c845 100644 --- a/frontend/src/app/shared/components/MediaPicker.tsx +++ b/frontend/src/app/shared/components/MediaPicker.tsx @@ -40,15 +40,15 @@ export const MediaPicker = (props: MediaPickerProps) => { const [selection, setSelection] = React.useState(); React.useEffect(() => { - dispatch(loadMedia(appId)); + dispatch(loadMedia({ appId, query: { search: '', page: 0 } })); }, [dispatch, appId]); const doRefresh = useEventCallback(() => { - dispatch(loadMedia(appId)); + dispatch(loadMedia({ appId })); }); - const doLoad = useEventCallback((q?: Partial) => { - dispatch(loadMedia(appId, q)); + const doLoad = useEventCallback((query?: Partial) => { + dispatch(loadMedia({ appId, query })); }); const doSelectMedia = useEventCallback((media: MediaDto) => { diff --git a/frontend/src/app/shared/components/MessagingTemplateInput.tsx b/frontend/src/app/shared/components/MessagingTemplateInput.tsx index 21350835..813a7bb6 100644 --- a/frontend/src/app/shared/components/MessagingTemplateInput.tsx +++ b/frontend/src/app/shared/components/MessagingTemplateInput.tsx @@ -20,7 +20,7 @@ export const MessagingTemplateInput = (props: FormEditorProps) => { React.useEffect(() => { if (!templates.isLoaded) { - dispatch(loadMessagingTemplates(appId)); + dispatch(loadMessagingTemplates({ appId })); } }, [dispatch, appId, templates.isLoaded]); diff --git a/frontend/src/app/shared/components/SmsTemplateInput.tsx b/frontend/src/app/shared/components/SmsTemplateInput.tsx index abd0b345..d8379c92 100644 --- a/frontend/src/app/shared/components/SmsTemplateInput.tsx +++ b/frontend/src/app/shared/components/SmsTemplateInput.tsx @@ -20,7 +20,7 @@ export const SmsTemplateInput = (props: FormEditorProps) => { React.useEffect(() => { if (!templates.isLoaded) { - dispatch(loadSmsTemplates(appId)); + dispatch(loadSmsTemplates({ appId })); } }, [dispatch, appId, templates.isLoaded]); diff --git a/frontend/src/app/state/apps/actions.ts b/frontend/src/app/state/apps/actions.ts index 66241add..464be445 100644 --- a/frontend/src/app/state/apps/actions.ts +++ b/frontend/src/app/state/apps/actions.ts @@ -5,136 +5,122 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createAction, createReducer } from '@reduxjs/toolkit'; -import { ErrorInfo, listThunk } from '@app/framework'; -import { AddContributorDto, AppDto, Clients, UpsertAppDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; -import { AppsState, CreateAppParams } from './state'; +import { Draft } from '@reduxjs/toolkit'; +import { createExtendedReducer, createList, createMutation } from '@app/framework'; +import { AddContributorDto, AppDetailsDto, AuthSchemeDto, Clients, UpsertAppDto } from '@app/service'; +import { selectApp } from './../shared'; +import { AppsState, AppsStateInStore, CreateAppParams } from './state'; -const list = listThunk('apps', 'apps', async () => { - const items = await Clients.Apps.getApps(); - - return { items, total: items.length }; +export const loadApps = createList('apps', 'apps').with({ + name: 'apps/load', + queryFn: async () => { + const items = await Clients.Apps.getApps(); + + return { items, total: items.length }; + }, }); -export const loadApps = () => { - return list.action({}); -}; - -export const loadDetails = createApiThunk('apps/load', - (arg: { appId: string }) => { - return Clients.Apps.getApp(arg.appId); - }); +export const loadDetails = createMutation('loadingDetails').with({ + name: 'apps/loadOne', + mutateFn: async (arg: { appId: string }) => { + const [app, auth] = await Promise.all([ + Clients.Apps.getApp(arg.appId), + Clients.Apps.getAuthScheme(arg.appId), + ]); -export const createAppReset = createAction('apps/create/reset'); + return { app, auth }; + }, + updateFn(state, action) { + state.app = action.payload.app; + state.auth = action.payload.auth; + }, +}); -export const createApp = createApiThunk('apps/create', - (arg: { params: CreateAppParams }) => { +export const createApp = createMutation('creating').with({ + name: 'apps/create', + mutateFn: (arg: { params: CreateAppParams }) => { return Clients.Apps.postApp(arg.params); - }); + }, + updateFn(state, action) { + state.apps.items?.setOrUnshift(x => x.id, action.payload); + state.apps.total++; + }, +}); -export const upsertApp = createApiThunk('apps/upsert', - (arg: { appId: string; params: UpsertAppDto }) => { +export const updateApp = createMutation('updating').with({ + name: 'apps/upsert', + mutateFn: (arg: { appId: string; params: UpsertAppDto }) => { return Clients.Apps.putApp(arg.appId, arg.params); - }); + }, + updateFn(state, action) { + updateDetails(state, action.payload); + }, +}); -export const addContributor = createApiThunk('apps/contributors/add', - (arg: { appId: string; params: AddContributorDto }) => { +export const addContributor = createMutation('updatingContributors').with({ + name: 'apps/contributors/add', + mutateFn: (arg: { appId: string; params: AddContributorDto }) => { return Clients.Apps.postContributor(arg.appId, arg.params); - }); + }, + updateFn(state, action) { + updateDetails(state, action.payload); + }, +}); -export const removeContributor = createApiThunk('apps/contributors/remove', - (arg: { appId: string; id: string }) => { +export const removeContributor = createMutation('updatingContributors').with({ + name: 'apps/contributors/remove', + mutateFn: (arg: { appId: string; id: string }) => { return Clients.Apps.deleteContributor(arg.appId, arg.id); - }); + }, + updateFn(state, action) { + updateDetails(state, action.payload); + }, +}); + +export const upsertAuth = createMutation('updatingAuth').with({ + name: 'apps/auth/upsert', + mutateFn: (arg: { appId: string; params: AuthSchemeDto }) => { + return Clients.Apps.upsertAuthScheme(arg.appId, arg.params); + }, + updateFn(state, action) { + state.auth = action.payload; + }, +}); + +export const removeAuth = createMutation('updatingAuth').with({ + name: 'apps/auth/remove', + mutateFn: (arg: { appId: string }) =>{ + return Clients.Apps.deleteAuthScheme(arg.appId); + }, + updateFn(state) { + state.auth = undefined; + }, +}); const initialState: AppsState = { - apps: list.createInitial(), + apps: loadApps.createInitial(), }; -export const appsReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + addContributor, + createApp, + loadApps, + loadDetails, + removeAuth, + updateApp, + upsertAuth, +]; + +export const appsReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, (state, action) => { state.appId = action.payload.appId; - }) - .addCase(createAppReset, (state) => { - state.creating = false; - state.creatingError = undefined; - }) - .addCase(createApp.pending, (state) => { - state.creating = true; - state.creatingError = undefined; - }) - .addCase(createApp.rejected, (state, action) => { - state.creating = false; - state.creatingError = action.payload as ErrorInfo; - }) - .addCase(createApp.fulfilled, (state, action) => { - state.creating = false; - state.creatingError = undefined; - state.apps.items?.setOrUnshift(x => x.id, action.payload); - state.apps.total++; - }) - .addCase(loadDetails.pending, (state) => { - state.loadingDetails = true; - state.loadingDetailsError = undefined; - }) - .addCase(loadDetails.rejected, (state, action) => { - state.loadingDetails = false; - state.loadingDetailsError = action.payload as ErrorInfo; - }) - .addCase(loadDetails.fulfilled, (state, action) => { - state.loadingDetails = false; - state.loadingDetailsError = undefined; - state.app = action.payload; - }) - .addCase(upsertApp.pending, (state) => { - state.upserting = true; - state.upsertingError = undefined; - }) - .addCase(upsertApp.rejected, (state, action) => { - state.upserting = false; - state.upsertingError = action.payload as ErrorInfo; - }) - .addCase(upsertApp.fulfilled, (state, action) => { - state.upserting = false; - state.upsertingError = undefined; - state.apps.items?.set(x => x.id, action.payload); - - if (state.app && state.app.id === action.payload.id) { - state.app = action.payload; - } - }) - .addCase(addContributor.pending, (state) => { - state.contributorsUpdating = true; - state.contributorsError = undefined; - }) - .addCase(addContributor.rejected, (state, action) => { - state.contributorsUpdating = false; - state.contributorsError = action.payload as ErrorInfo; - }) - .addCase(addContributor.fulfilled, (state, action) => { - state.contributorsUpdating = false; - state.contributorsError = undefined; - state.apps.items?.set(x => x.id, action.payload); + }), +operations); - if (state.app && state.app.id === action.payload.id) { - state.app = action.payload; - } - }) - .addCase(removeContributor.pending, (state) => { - state.contributorsUpdating = true; - state.contributorsError = undefined; - }) - .addCase(removeContributor.rejected, (state, action) => { - state.contributorsUpdating = false; - state.contributorsError = action.payload as ErrorInfo; - }) - .addCase(removeContributor.fulfilled, (state, action) => { - state.contributorsUpdating = false; - state.contributorsError = undefined; - state.apps.items?.set(x => x.id, action.payload); +function updateDetails(state: Draft, details: AppDetailsDto) { + state.apps.items?.set(x => x.id, details); - if (state.app && state.app.id === action.payload.id) { - state.app = action.payload; - } - })); + if (state.app && state.app.id === details.id) { + state.app = details; + } +} diff --git a/frontend/src/app/state/apps/state.ts b/frontend/src/app/state/apps/state.ts index 6f890410..977ab317 100644 --- a/frontend/src/app/state/apps/state.ts +++ b/frontend/src/app/state/apps/state.ts @@ -5,9 +5,8 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo } from '@app/framework'; -import { ListState } from '@app/framework/model'; -import { AppDetailsDto, AppDto } from '@app/service'; +import { ListState, MutationState } from '@app/framework/model'; +import { AppDetailsDto, AppDto, AuthSchemeResponseDto } from '@app/service'; export interface AppsStateInStore { apps: AppsState; @@ -27,27 +26,21 @@ export interface AppsState { // The app details. app?: AppDetailsDto; - // True if loading details. - loadingDetails?: boolean; + // The auth details. + auth?: AuthSchemeResponseDto; - // The loading details error. - loadingDetailsError?: ErrorInfo; + // Mutation for loading details. + loadingDetails?: MutationState; - // True if creating. - creating?: boolean; + // Mutation for creating. + creating?: MutationState; - // The creating error. - creatingError?: ErrorInfo; + // Mutation for updating the app. + updating?: MutationState; - // True if upserting. - upserting?: boolean; + // Mutation for updating the app. + updatingContributors?: MutationState; - // The upserting error. - upsertingError?: ErrorInfo; - - // True if updating the contributors. - contributorsUpdating?: boolean; - - // The contributor error. - contributorsError?: ErrorInfo; + // Mutation for updating the auth settings. + updatingAuth?: MutationState; } diff --git a/frontend/src/app/state/core/actions.ts b/frontend/src/app/state/core/actions.ts index b8ba790a..4bc688f1 100644 --- a/frontend/src/app/state/core/actions.ts +++ b/frontend/src/app/state/core/actions.ts @@ -6,8 +6,8 @@ */ import { createReducer } from '@reduxjs/toolkit'; +import { createApiThunk } from '@app/framework'; import { Clients } from '@app/service'; -import { createApiThunk } from './../shared'; import { CoreState } from './state'; export const loadTimezones = createApiThunk('core/timezones', async () => { diff --git a/frontend/src/app/state/email-templates/actions.ts b/frontend/src/app/state/email-templates/actions.ts index 09c5f0f9..3942c364 100644 --- a/frontend/src/app/state/email-templates/actions.ts +++ b/frontend/src/app/state/email-templates/actions.ts @@ -5,62 +5,90 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer, Middleware } from '@reduxjs/toolkit'; +import { Draft, Middleware } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; -import { ErrorInfo, formatError, listThunk, Query } from '@app/framework'; -import { ChannelTemplateDto, Clients, EmailTemplateDto, UpdateChannelTemplateDtoOfEmailTemplateDto } from '@app/service'; +import { createExtendedReducer, createList, createMutation, formatError } from '@app/framework'; +import { ChannelTemplateDetailsDtoOfEmailTemplateDto, Clients, EmailTemplateDto, UpdateChannelTemplateDtoOfEmailTemplateDto } from '@app/service'; import { createApiThunk, selectApp } from './../shared'; -import { EmailTemplatesState } from './state'; - -const list = listThunk('emailTemplates', 'templates', async params => { - const { items, total } = await Clients.EmailTemplates.getTemplates(params.appId, params.search, params.take, params.skip); - - return { items, total }; +import { EmailTemplatesState, EmailTemplatesStateInStore } from './state'; + +export const loadEmailTemplates = createList('templates', 'emailTemplates').with({ + name: 'emailTemplates/load', + queryFn: async (p: { appId: string }, q) => { + const { items, total } = await Clients.EmailTemplates.getTemplates(p.appId, q.search, q.take, q.skip); + + return { items, total }; + }, }); -export const loadEmailTemplates = (appId: string, query?: Partial, reset = false) => { - return list.action({ appId, query, reset }); -}; - export const loadMjmlSchema = createApiThunk('emailTemplates/schema', () => { return Clients.EmailTemplates.getSchema(); }); -export const loadEmailTemplate = createApiThunk('emailTemplates/load', - (arg: { appId: string; id: string }) => { +export const loadEmailTemplate = createMutation('loadingTemplate').with({ + name: 'emailTemplates/loadOne', + mutateFn: (arg: { appId: string; id: string }) => { return Clients.EmailTemplates.getTemplate(arg.appId, arg.id); - }); + }, + updateFn(state, action) { + state.template = action.payload; + }, +}); -export const createEmailTemplate = createApiThunk('emailTemplates/create', - (arg: { appId: string; kind?: string }) => { +export const createEmailTemplate = createMutation('creating').with({ + name: 'emailTemplates/create', + mutateFn: (arg: { appId: string; kind?: string }) => { return Clients.EmailTemplates.postTemplate(arg.appId, { kind: arg.kind }); - }); + }, +}); -export const createEmailTemplateLanguage = createApiThunk('emailTemplates/createLanguage', - (arg: { appId: string; id: string; language: string }) => { +export const createEmailTemplateLanguage = createMutation('creatingLanguage').with({ + name: 'emailTemplates/createLanguage', + mutateFn: (arg: { appId: string; id: string; language: string }) => { return Clients.EmailTemplates.postTemplateLanguage(arg.appId, arg.id, { language: arg.language }); - }); + }, + updateFn(state, action) { + updateTemplate(state, action.meta.arg.id, action.payload); + }, +}); -export const updateEmailTemplate = createApiThunk('emailTemplates/update', - (arg: { appId: string; id: string; update: UpdateChannelTemplateDtoOfEmailTemplateDto }) => { +export const updateEmailTemplate = createMutation('updating').with({ + name: 'emailTemplates/update', + mutateFn: (arg: { appId: string; id: string; update: UpdateChannelTemplateDtoOfEmailTemplateDto }) => { return Clients.EmailTemplates.putTemplate(arg.appId, arg.id, arg.update); - }); + }, + updateFn(state, action) { + updateTemplate(state, action.meta.arg.id, action.payload); + }, +}); -export const updateEmailTemplateLanguage = createApiThunk('emailTemplates/updateLanguage', - (arg: { appId: string; id: string; language: string; template: EmailTemplateDto }) => { +export const updateEmailTemplateLanguage = createMutation('updatingLanguage').with({ + name: 'emailTemplates/updateLanguage', + mutateFn: (arg: { appId: string; id: string; language: string; template: EmailTemplateDto }) => { return Clients.EmailTemplates.putTemplateLanguage(arg.appId, arg.id, arg.language, arg.template); - }); + }, + updateFn(state, action) { + updateTemplate(state, action.meta.arg.id, action.payload); + }, +}); -export const deleteEmailTemplate = createApiThunk('emailTemplates/delete', - (arg: { appId: string; id: string }) => { +export const deleteEmailTemplate = createMutation('deleting').with({ + name: 'emailTemplates/delete', + mutateFn: (arg: { appId: string; id: string }) => { return Clients.EmailTemplates.deleteTemplate(arg.appId, arg.id); - }); + }, +}); -export const deleteEmailTemplateLanguage = createApiThunk('emailTemplates/deleteLanguage', - (arg: { appId: string; id: string; language: string }) => { +export const deleteEmailTemplateLanguage = createMutation('deletingLanguage').with({ + name: 'emailTemplates/deleteLanguage', + mutateFn: (arg: { appId: string; id: string; language: string }) => { return Clients.EmailTemplates.deleteTemplateLanguage(arg.appId, arg.id, arg.language); - }); + }, + updateFn(state, action) { + updateTemplate(state, action.meta.arg.id, action.payload); + }, +}); export const emailTemplatesMiddleware: Middleware = store => next => action => { const result = next(action); @@ -68,7 +96,7 @@ export const emailTemplatesMiddleware: Middleware = store => next => action => { if (createEmailTemplate.fulfilled.match(action) || deleteEmailTemplate.fulfilled.match(action)) { const { appId } = action.meta.arg; - store.dispatch(loadEmailTemplates(appId) as any); + store.dispatch(loadEmailTemplates({ appId }) as any); } else if ( deleteEmailTemplate.rejected.match(action) || deleteEmailTemplateLanguage.rejected.match(action)) { @@ -79,114 +107,31 @@ export const emailTemplatesMiddleware: Middleware = store => next => action => { }; const initialState: EmailTemplatesState = { - templates: list.createInitial(), + templates: loadEmailTemplates.createInitial(), }; -export const emailTemplatesReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + createEmailTemplate, + createEmailTemplateLanguage, + deleteEmailTemplate, + deleteEmailTemplateLanguage, + loadEmailTemplate, + loadEmailTemplates, + updateEmailTemplate, + updateEmailTemplateLanguage, +]; + +export const emailTemplatesReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; }) .addCase(loadMjmlSchema.fulfilled, (state, action) => { state.schema = action.payload; - }) - .addCase(loadEmailTemplate.pending, (state) => { - state.loadingTemplate = true; - state.loadingTemplateError = undefined; - }) - .addCase(loadEmailTemplate.rejected, (state, action) => { - state.loadingTemplate = false; - state.loadingTemplateError = action.payload as ErrorInfo; - }) - .addCase(loadEmailTemplate.fulfilled, (state, action) => { - state.loadingTemplate = false; - state.loadingTemplateError = undefined; - state.template = action.payload; - }) - .addCase(createEmailTemplate.pending, (state) => { - state.creating = true; - state.creatingError = undefined; - }) - .addCase(createEmailTemplate.rejected, (state, action) => { - state.creating = false; - state.creatingError = action.payload as ErrorInfo; - }) - .addCase(createEmailTemplate.fulfilled, (state) => { - state.creating = false; - state.creatingError = undefined; - }) - .addCase(createEmailTemplateLanguage.pending, (state) => { - state.creatingLanguage = true; - state.creatingLanguageError = undefined; - }) - .addCase(createEmailTemplateLanguage.rejected, (state, action) => { - state.creatingLanguage = false; - state.creatingLanguageError = action.payload as ErrorInfo; - }) - .addCase(createEmailTemplateLanguage.fulfilled, (state, action) => { - state.creatingLanguage = false; - state.creatingLanguageError = undefined; - - if (state.template && state.template.id === action.meta.arg.id) { - state.template = action.payload; - } - }) - .addCase(updateEmailTemplate.pending, (state) => { - state.updating = true; - state.updatingError = undefined; - }) - .addCase(updateEmailTemplate.rejected, (state, action) => { - state.updating = false; - state.updatingError = action.payload as ErrorInfo; - }) - .addCase(updateEmailTemplate.fulfilled, (state, action) => { - state.updating = false; - state.updatingError = undefined; + }), +operations); - if (state.template && state.template.id === action.meta.arg.id) { - state.template = action.payload; - } - }) - .addCase(updateEmailTemplateLanguage.pending, (state) => { - state.updatingLanguage = true; - state.updatingLanguageError = undefined; - }) - .addCase(updateEmailTemplateLanguage.rejected, (state, action) => { - state.updatingLanguage = false; - state.updatingLanguageError = action.payload as ErrorInfo; - }) - .addCase(updateEmailTemplateLanguage.fulfilled, (state, action) => { - state.updatingLanguage = false; - state.updatingLanguageError = undefined; - - if (state.template && state.template.id === action.meta.arg.id) { - state.template = action.payload; - } - }) - .addCase(deleteEmailTemplate.pending, (state) => { - state.deleting = true; - state.deletingError = undefined; - }) - .addCase(deleteEmailTemplate.rejected, (state, action) => { - state.deleting = false; - state.deletingError = action.payload as ErrorInfo; - }) - .addCase(deleteEmailTemplate.fulfilled, (state) => { - state.deleting = false; - state.deletingError = undefined; - }) - .addCase(deleteEmailTemplateLanguage.pending, (state) => { - state.deletingLanguage = true; - state.deletingLanguageError = undefined; - }) - .addCase(deleteEmailTemplateLanguage.rejected, (state, action) => { - state.deletingLanguage = false; - state.deletingLanguageError = action.payload as ErrorInfo; - }) - .addCase(deleteEmailTemplateLanguage.fulfilled, (state, action) => { - state.deletingLanguage = false; - state.deletingLanguageError = undefined; - - if (state.template && state.template.id === action.meta.arg.id) { - state.template = action.payload; - } - })); +function updateTemplate(state: Draft, id: string, template: ChannelTemplateDetailsDtoOfEmailTemplateDto) { + if (state.template && state.template.id === id) { + state.template = template; + } +} diff --git a/frontend/src/app/state/email-templates/state.ts b/frontend/src/app/state/email-templates/state.ts index d41dfd15..d4a115e0 100644 --- a/frontend/src/app/state/email-templates/state.ts +++ b/frontend/src/app/state/email-templates/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo, ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { ChannelTemplateDetailsDtoOfEmailTemplateDto, ChannelTemplateDto, MjmlSchema } from '@app/service'; export interface EmailTemplatesStateInStore { @@ -22,45 +22,24 @@ export interface EmailTemplatesState { // The template details. template?: ChannelTemplateDetailsDtoOfEmailTemplateDto; - // True if loading the email templates. - loadingTemplate?: boolean; + // Mutation for loading the email templates. + loadingTemplate?: MutationState; - // The email templates loading error. - loadingTemplateError?: ErrorInfo; + // Mutation for creating an email template. + creating?: MutationState; - // True if creating an email template. - creating?: boolean; + // Mutation for creating an email template language. + creatingLanguage?: MutationState; - // The error if creating an email template fails. - creatingError?: ErrorInfo; + // Mutation for updating an email template. + updating?: MutationState; - // True if creating an email template language. - creatingLanguage?: boolean; + // Mutation for updating an email template language. + updatingLanguage?: MutationState; - // The error if creating an email template language fails. - creatingLanguageError?: ErrorInfo; + // Mutation for deleting an email template. + deleting?: MutationState; - // True if updating an email template. - updating?: boolean; - - // The error if updating an email template fails. - updatingError?: ErrorInfo; - - // True if updating an email template language. - updatingLanguage?: boolean; - - // The error if updating an email template language fails. - updatingLanguageError?: ErrorInfo; - - // True if deleting an email template. - deleting?: boolean; - - // The error if deleting an email template language. - deletingError?: ErrorInfo; - - // True if deleting an email template language. - deletingLanguage?: boolean; - - // The error if deleting an email template language fails. - deletingLanguageError?: ErrorInfo; + // Mutation for deleting an email template language. + deletingLanguage?: MutationState; } diff --git a/frontend/src/app/state/events/actions.ts b/frontend/src/app/state/events/actions.ts index f63f5cb9..7c0bfcf3 100644 --- a/frontend/src/app/state/events/actions.ts +++ b/frontend/src/app/state/events/actions.ts @@ -5,27 +5,29 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer } from '@reduxjs/toolkit'; -import { listThunk, Query } from '@app/framework'; -import { Clients, EventDto } from '@app/service'; +import { createExtendedReducer, createList } from '@app/framework'; +import { Clients } from '@app/service'; import { selectApp } from './../shared'; -import { EventsState } from './state'; +import { EventsState, EventsStateInStore } from './state'; -const list = listThunk('events', 'events', async params => { - const { items, total } = await Clients.Events.getEvents(params.appId, params.channels, params.search, params.take, params.skip); +export const loadEvents = createList('events', 'events').with({ + name: 'events/load', + queryFn: async (p: { appId: string; channels?: string[] }, q) => { + const { items, total } = await Clients.Events.getEvents(p.appId, p.channels, q.search, q.take, q.skip); - return { items, total }; + return { items, total }; + }, }); -export const loadEvents = (appId: string, query?: Partial, reset = false, channels?: string[]) => { - return list.action({ appId, query, reset, channels }); -}; - const initialState: EventsState = { - events: list.createInitial(), + events: loadEvents.createInitial(), }; -export const eventsReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + loadEvents, +]; + +export const eventsReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - })); + }), operations); diff --git a/frontend/src/app/state/integrations/actions.ts b/frontend/src/app/state/integrations/actions.ts index a27eeb29..83842ebc 100644 --- a/frontend/src/app/state/integrations/actions.ts +++ b/frontend/src/app/state/integrations/actions.ts @@ -5,32 +5,59 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer, Middleware } from '@reduxjs/toolkit'; +import { Middleware } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; -import { ErrorInfo, formatError } from '@app/framework'; +import { createExtendedReducer, createMutation, formatError } from '@app/framework'; import { Clients, CreateIntegrationDto, UpdateIntegrationDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; +import { selectApp } from './../shared'; import { IntegrationsState } from './state'; -export const loadIntegrations = createApiThunk('integrations/load', - async (arg: { appId: string }) => { +export const loadIntegrations = createMutation('loading').with({ + name: 'integrations/loading', + mutateFn: async (arg: { appId: string }) => { return await Clients.Apps.getIntegrations(arg.appId); - }); + }, + updateFn(state, action) { + state.configured = action.payload.configured; + state.supported = action.payload.supported; + }, +}); -export const createIntegration = createApiThunk('integrations/create', - async (arg: { appId: string; params: CreateIntegrationDto }) => { +export const createIntegration = createMutation('upserting').with({ + name: 'integrations/create', + mutateFn: async (arg: { appId: string; params: CreateIntegrationDto }) => { return await Clients.Apps.postIntegration(arg.appId, arg.params); - }); + }, + updateFn(state, action) { + const id = action.payload.id; -export const updateIntegration = createApiThunk('integrations/update', - async (arg: { appId: string; id: string; params: UpdateIntegrationDto }) => { + state.configured[id] = action.payload.integration; + }, +}); + +export const updateIntegration = createMutation('upserting').with({ + name: 'integrations/update', + mutateFn: async (arg: { appId: string; id: string; params: UpdateIntegrationDto }) => { return await Clients.Apps.putIntegration(arg.appId, arg.id, arg.params); - }); + }, + updateFn(state, action) { + const id = action.meta.arg.id; + + state.configured[id] = { id, ...action.meta.arg.params } as any; + }, +}); -export const deleteIntegration = createApiThunk('integrations/delete', - async (arg: { appId: string; id: string }) => { +export const deleteIntegration = createMutation('upserting').with({ + name: 'integrations/delete', + mutateFn: async (arg: { appId: string; id: string }) => { await Clients.Apps.deleteIntegration(arg.appId, arg.id); - }); + }, + updateFn(state, action) { + const id = action.meta.arg.id; + + delete state.configured[id]; + }, +}); export const integrationsMiddleware: Middleware = store => next => action => { const result = next(action); @@ -46,64 +73,20 @@ export const integrationsMiddleware: Middleware = store => next => action => { return result; }; -const initialState: IntegrationsState = {}; +const initialState: IntegrationsState = { + configured: {}, + supported: {}, +}; + +const operations = [ + loadIntegrations, + createIntegration, + updateIntegration, + deleteIntegration, +]; -export const integrationsReducer = createReducer(initialState, builder => builder +export const integrationsReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - }) - .addCase(loadIntegrations.pending, (state) => { - state.loading = true; - state.loadingError = undefined; - }) - .addCase(loadIntegrations.rejected, (state, action) => { - state.loading = false; - state.loadingError = action.payload as ErrorInfo; - }) - .addCase(loadIntegrations.fulfilled, (state, action) => { - state.configured = action.payload.configured; - state.loading = false; - state.loadingError = undefined; - state.supported = action.payload.supported; - }) - .addCase(createIntegration.pending, (state) => { - state.upserting = true; - state.upsertingError = undefined; - }) - .addCase(createIntegration.rejected, (state, action) => { - state.upserting = false; - state.upsertingError = action.payload as ErrorInfo; - }) - .addCase(createIntegration.fulfilled, (state, action) => { - state.upserting = false; - state.upsertingError = undefined; - - if (state.configured) { - state.configured[action.payload.id] = action.payload.integration; - } - }) - .addCase(updateIntegration.pending, (state) => { - state.upserting = true; - state.upsertingError = undefined; - }) - .addCase(updateIntegration.rejected, (state, action) => { - state.upserting = false; - state.upsertingError = action.payload as ErrorInfo; - }) - .addCase(updateIntegration.fulfilled, (state, action) => { - state.upserting = false; - state.upsertingError = undefined; - - if (state.configured) { - const { id, params } = action.meta.arg; - - state.configured[id] = { id, ...params } as any; - } - }) - .addCase(deleteIntegration.fulfilled, (state, action) => { - if (state.configured) { - const { id } = action.meta.arg; - - delete state.configured[id]; - } - })); + }), +operations); diff --git a/frontend/src/app/state/integrations/state.ts b/frontend/src/app/state/integrations/state.ts index f93bdb38..0c237b8d 100644 --- a/frontend/src/app/state/integrations/state.ts +++ b/frontend/src/app/state/integrations/state.ts @@ -5,25 +5,19 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo } from '@app/framework'; +import { MutationState } from '@app/framework'; import { ConfiguredIntegrationDto, ConfiguredIntegrationsDto, IntegrationDefinitionDto } from '@app/service'; export interface IntegrationsStateInStore { integrations: IntegrationsState; } -export interface IntegrationsState extends Partial { - // True if loading integrations. - loading?: boolean; +export interface IntegrationsState extends ConfiguredIntegrationsDto { + // Mutation for loading integrations. + loading?: MutationState; - // The loading integrations error. - loadingError?: ErrorInfo; - - // True if upserting integrations. - upserting?: boolean; - - // The upserting integrations error. - upsertingError?: ErrorInfo; + // Mutation for upserting integrations. + upserting?: MutationState; } export function getSummaryProperties(definition: IntegrationDefinitionDto, configured: ConfiguredIntegrationDto) { diff --git a/frontend/src/app/state/log/actions.ts b/frontend/src/app/state/log/actions.ts index 221f2929..423ad1e3 100644 --- a/frontend/src/app/state/log/actions.ts +++ b/frontend/src/app/state/log/actions.ts @@ -5,27 +5,29 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer } from '@reduxjs/toolkit'; -import { listThunk, Query } from '@app/framework'; -import { Clients, LogEntryDto } from '@app/service'; +import { createExtendedReducer, createList } from '@app/framework'; +import { Clients } from '@app/service'; import { selectApp } from './../shared'; -import { LogState } from './state'; +import { LogState, LogStateInStore } from './state'; -const list = listThunk('log', 'entries', async params => { - const { items, total } = await Clients.Logs.getLogs(params.appId, params.systems, params.userId, undefined, params.search, params.take, params.skip); +export const loadLog = createList('log', 'log').with({ + name: 'log/load', + queryFn: async (p: { appId: string; systems?: string[]; userId?: string }, q ) => { + const { items, total } = await Clients.Logs.getLogs(p.appId, p.systems, p.userId, undefined, q.search, q.take, q.skip); - return { items, total }; + return { items, total }; + }, }); -export const loadLog = (appId: string, query?: Partial, reset = false, systems?: string[], userId?: string) => { - return list.action({ appId, query, reset, systems, userId }); -}; - const initialState: LogState = { - entries: list.createInitial(), + log: loadLog.createInitial(), }; -export const logReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + loadLog, +]; + +export const logReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - })); + }), operations); diff --git a/frontend/src/app/state/log/state.ts b/frontend/src/app/state/log/state.ts index cd099219..8041cf93 100644 --- a/frontend/src/app/state/log/state.ts +++ b/frontend/src/app/state/log/state.ts @@ -14,5 +14,5 @@ export interface LogStateInStore { export interface LogState { // All log entries. - entries: ListState; + log: ListState; } diff --git a/frontend/src/app/state/media/actions.ts b/frontend/src/app/state/media/actions.ts index 33afa158..e021cf0d 100644 --- a/frontend/src/app/state/media/actions.ts +++ b/frontend/src/app/state/media/actions.ts @@ -5,33 +5,38 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; import { Middleware } from 'redux'; -import { formatError, listThunk, Query } from '@app/framework'; -import { Clients, MediaDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; -import { MediaState } from './state'; +import { createExtendedReducer, createList, createMutation, formatError } from '@app/framework'; +import { Clients } from '@app/service'; +import { selectApp } from './../shared'; +import { MediaState, MediaStateInStore } from './state'; -const list = listThunk('media', 'media', async params => { - const { items, total } = await Clients.Media.getMedias(params.appId, params.search, params.take, params.skip); +export const loadMedia = createList('media', 'media').with({ + name: 'media/load', + queryFn: async (p: { appId: string }, q) => { + const { items, total } = await Clients.Media.getMedias(p.appId, q.search, q.take, q.skip); - return { items, total }; + return { items, total }; + }, }); -export const loadMedia = (appId: string, query?: Partial, reset = false) => { - return list.action({ appId, query, reset }); -}; - -export const uploadMedia = createApiThunk('media/upload', - async (arg: { appId: string; file: File }) => { +export const uploadMedia = createMutation('uploading').with({ + name: 'media/upload', + mutateFn: async (arg: { appId: string; file: File }) => { await Clients.Media.upload(arg.appId, { data: arg.file, fileName: arg.file.name }); - }); + }, +}); -export const deleteMedia = createApiThunk('media/delete', - async (arg: { appId: string; fileName: string }) => { +export const deleteMedia = createMutation('deleting').with({ + name: 'media/delete', + mutateFn: async (arg: { appId: string; fileName: string }) => { await Clients.Media.delete(arg.appId, arg.fileName); - }); + }, + updateFn(state, action) { + state.uploadingFiles.removeByValue(x => x.name, action.meta.arg.fileName); + }, +}); export const mediaMiddleware: Middleware = store => next => action => { const result = next(action); @@ -39,7 +44,7 @@ export const mediaMiddleware: Middleware = store => next => action => { if (uploadMedia.fulfilled.match(action) || deleteMedia.fulfilled.match(action)) { const { appId } = action.meta.arg; - store.dispatch(loadMedia(appId) as any); + store.dispatch(loadMedia({ appId }) as any); } else if (deleteMedia.rejected.match(action)) { toast.error(formatError(action.payload as any)); } @@ -48,13 +53,17 @@ export const mediaMiddleware: Middleware = store => next => action => { }; const initialState: MediaState = { - media: list.createInitial(), uploadingFiles: [], + media: loadMedia.createInitial(), uploadingFiles: [], }; -export const mediaReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + deleteMedia, + loadMedia, + uploadMedia, +]; + +export const mediaReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - }) - .addCase(deleteMedia.fulfilled, (state, action) => { - state.uploadingFiles.removeByValue(x => x.name, action.meta.arg.fileName); - })); + }), +operations); diff --git a/frontend/src/app/state/media/state.ts b/frontend/src/app/state/media/state.ts index 163162dc..f09219ef 100644 --- a/frontend/src/app/state/media/state.ts +++ b/frontend/src/app/state/media/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { MediaDto } from '@app/service/service'; export interface MediaStateInStore { @@ -18,4 +18,10 @@ export interface MediaState { // The uploading files. uploadingFiles: ReadonlyArray; + + // The mutation for uploading media files. + uploading?: MutationState; + + // The mutation for deleting media files. + deleting?: MutationState; } diff --git a/frontend/src/app/state/messaging-templates/actions.ts b/frontend/src/app/state/messaging-templates/actions.ts index 5d53a59f..419b8dda 100644 --- a/frontend/src/app/state/messaging-templates/actions.ts +++ b/frontend/src/app/state/messaging-templates/actions.ts @@ -5,47 +5,70 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer, Middleware } from '@reduxjs/toolkit'; +import { Middleware } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; -import { ErrorInfo, formatError, listThunk, Query } from '@app/framework'; -import { ChannelTemplateDto, Clients, UpdateChannelTemplateDtoOfMessagingTemplateDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; -import { MessagingTemplatesState } from './state'; - -const list = listThunk('messagingTemplates', 'templates', async params => { - const { items, total } = await Clients.MessagingTemplates.getTemplates(params.appId, params.search, params.take, params.skip); - - return { items, total }; +import { createExtendedReducer, createList, createMutation, formatError } from '@app/framework'; +import { Clients, UpdateChannelTemplateDtoOfMessagingTemplateDto } from '@app/service'; +import { selectApp } from './../shared'; +import { MessagingTemplatesState, MessagingTemplatesStateInStore } from './state'; + +export const loadMessagingTemplates = createList('templates', 'messagingTemplates').with({ + name: 'messagingTemplates/load', + queryFn: async (p: { appId: string }, q) => { + const { items, total } = await Clients.MessagingTemplates.getTemplates(p.appId, q.search, q.take, q.skip); + + return { items, total }; + }, }); -export const loadMessagingTemplates = (appId: string, query?: Partial, reset = false) => { - return list.action({ appId, query, reset }); -}; - -export const loadMessagingTemplate = createApiThunk('messagingTemplates/load', - (arg: { appId: string; id: string }) => { +export const loadMessagingTemplate = createMutation('loadingTemplate').with({ + name: 'messagingTemplates/loadOne', + mutateFn: (arg: { appId: string; id: string }) => { return Clients.MessagingTemplates.getTemplate(arg.appId, arg.id); - }); + }, + updateFn(state, action) { + state.template = action.payload; + }, +}); -export const createMessagingTemplate = createApiThunk('messagingTemplates/create', - (arg: { appId: string }) => { +export const createMessagingTemplate = createMutation('loadingTemplate').with({ + name: 'messagingTemplates/create', + mutateFn: (arg: { appId: string }) => { return Clients.MessagingTemplates.postTemplate(arg.appId, { }); - }); + }, + updateFn(state, action) { + state.template = action.payload; + }, +}); -export const createMessagingTemplateLanguage = createApiThunk('messagingTemplates/createLanguage', - (arg: { appId: string; id: string; language: string }) => { +export const createMessagingTemplateLanguage = createMutation('loadingTemplate').with({ + name: 'messagingTemplates/createLanguage', + mutateFn: (arg: { appId: string; id: string; language: string }) => { return Clients.MessagingTemplates.postTemplateLanguage(arg.appId, arg.id, { language: arg.language }); - }); + }, + updateFn(state, action) { + state.template = action.payload; + }, +}); -export const updateMessagingTemplate = createApiThunk('messagingTemplates/update', - (arg: { appId: string; id: string; update: UpdateChannelTemplateDtoOfMessagingTemplateDto }) => { +export const updateMessagingTemplate = createMutation('loadingTemplate').with({ + name: 'messagingTemplates/update', + mutateFn: (arg: { appId: string; id: string; update: UpdateChannelTemplateDtoOfMessagingTemplateDto }) => { return Clients.MessagingTemplates.putTemplate(arg.appId, arg.id, arg.update); - }); + }, + updateFn(state, action) { + if (state.template && state.template.id === action.meta.arg.id) { + state.template = { ...state.template, ...action.meta.arg.update }; + } + }, +}); -export const deleteMessagingTemplate = createApiThunk('messagingTemplates/delete', - (arg: { appId: string; id: string }) => { +export const deleteMessagingTemplate = createMutation('loadingTemplate').with({ + name: 'messagingTemplates/delete', + mutateFn: (arg: { appId: string; id: string }) => { return Clients.MessagingTemplates.deleteTemplate(arg.appId, arg.id); - }); + }, +}); export const messagingTemplatesMiddleware: Middleware = store => next => action => { const result = next(action); @@ -53,7 +76,7 @@ export const messagingTemplatesMiddleware: Middleware = store => next => action if (createMessagingTemplate.fulfilled.match(action) || deleteMessagingTemplate.fulfilled.match(action)) { const { appId } = action.meta.arg; - store.dispatch(loadMessagingTemplates(appId) as any); + store.dispatch(loadMessagingTemplates({ appId }) as any); } else if (deleteMessagingTemplate.rejected.match(action)) { toast.error(formatError(action.payload as any)); } @@ -62,63 +85,19 @@ export const messagingTemplatesMiddleware: Middleware = store => next => action }; const initialState: MessagingTemplatesState = { - templates: list.createInitial(), + templates: loadMessagingTemplates.createInitial(), }; -export const messagingTemplatesReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + createMessagingTemplate, + createMessagingTemplateLanguage, + deleteMessagingTemplate, + loadMessagingTemplate, + loadMessagingTemplates, + updateMessagingTemplate, +]; + +export const messagingTemplatesReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - }) - .addCase(loadMessagingTemplate.pending, (state) => { - state.loadingTemplate = true; - state.loadingTemplateError = undefined; - }) - .addCase(loadMessagingTemplate.rejected, (state, action) => { - state.loadingTemplate = false; - state.loadingTemplateError = action.payload as ErrorInfo; - }) - .addCase(loadMessagingTemplate.fulfilled, (state, action) => { - state.loadingTemplate = false; - state.loadingTemplateError = undefined; - state.template = action.payload; - }) - .addCase(createMessagingTemplate.pending, (state) => { - state.creating = true; - state.creatingError = undefined; - }) - .addCase(createMessagingTemplate.rejected, (state, action) => { - state.creating = false; - state.creatingError = action.payload as ErrorInfo; - }) - .addCase(createMessagingTemplate.fulfilled, (state) => { - state.creating = false; - state.creatingError = undefined; - }) - .addCase(updateMessagingTemplate.pending, (state) => { - state.updating = true; - state.updatingError = undefined; - }) - .addCase(updateMessagingTemplate.rejected, (state, action) => { - state.updating = false; - state.updatingError = action.payload as ErrorInfo; - }) - .addCase(updateMessagingTemplate.fulfilled, (state, action) => { - state.updating = false; - state.updatingError = undefined; - - if (state.template && state.template.id === action.meta.arg.id) { - state.template = { ...state.template, ...action.meta.arg.update }; - } - }) - .addCase(deleteMessagingTemplate.pending, (state) => { - state.deleting = true; - state.deletingError = undefined; - }) - .addCase(deleteMessagingTemplate.rejected, (state, action) => { - state.deleting = false; - state.deletingError = action.payload as ErrorInfo; - }) - .addCase(deleteMessagingTemplate.fulfilled, (state) => { - state.deleting = false; - state.deletingError = undefined; - })); + }), operations); diff --git a/frontend/src/app/state/messaging-templates/state.ts b/frontend/src/app/state/messaging-templates/state.ts index 0b960fce..00362cca 100644 --- a/frontend/src/app/state/messaging-templates/state.ts +++ b/frontend/src/app/state/messaging-templates/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo, ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { ChannelTemplateDetailsDtoOfMessagingTemplateDto, ChannelTemplateDto } from '@app/service'; export interface MessagingTemplatesStateInStore { @@ -19,27 +19,15 @@ export interface MessagingTemplatesState { // The template details. template?: ChannelTemplateDetailsDtoOfMessagingTemplateDto; - // True if loading the messaging templates. - loadingTemplate?: boolean; + // Mutation for loading the messaging templates. + loadingTemplate?: MutationState; - // The messaging templates loading error. - loadingTemplateError?: ErrorInfo; + // Mutation for creating an messaging template. + creating?: MutationState; - // True if creating an messaging template. - creating?: boolean; + // Mutation for updating an messaging template. + updating?: MutationState; - // The error if creating an messaging template fails. - creatingError?: ErrorInfo; - - // True if updating an messaging template. - updating?: boolean; - - // The error if updating an messaging template fails. - updatingError?: ErrorInfo; - - // True if deleting an messaging template. - deleting?: boolean; - - // The error if deleting an messaging template language. - deletingError?: ErrorInfo; + // Mutation for deleting an messaging template. + deleting?: MutationState; } diff --git a/frontend/src/app/state/notifications/actions.ts b/frontend/src/app/state/notifications/actions.ts index ae32cfe5..2abcde86 100644 --- a/frontend/src/app/state/notifications/actions.ts +++ b/frontend/src/app/state/notifications/actions.ts @@ -5,27 +5,29 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer } from '@reduxjs/toolkit'; -import { listThunk, Query } from '@app/framework'; -import { Clients, UserNotificationDetailsDto } from '@app/service'; +import { createExtendedReducer, createList } from '@app/framework'; +import { Clients } from '@app/service'; import { selectApp } from './../shared'; -import { NotificationsState } from './state'; +import { NotificationsState, NotificationsStateInStore } from './state'; -const list = listThunk('notifications', 'notifications', async (params) => { - const { items, total } = await Clients.Notifications.getNotifications(params.appId, params.userId, params.channels, undefined, undefined, params.search, params.take, params.skip); +export const loadNotifications = createList('notifications', 'notifications').with({ + name: 'notifications/load', + queryFn: async (p: { appId: string; userId: string; channels: string[] }, q) => { + const { items, total } = await Clients.Notifications.getNotifications(p.appId, p.userId, p.channels, undefined, undefined, q.search, q.take, q.skip); - return { items, total }; + return { items, total }; + }, }); -export const loadNotifications = (appId: string, userId: string, query?: Partial, reset = false, channels?: string[]) => { - return list.action({ appId, userId, query, reset, channels }); -}; - const initialState: NotificationsState = { - notifications: list.createInitial(), + notifications: loadNotifications.createInitial(), }; -export const notificationsReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + loadNotifications, +]; + +export const notificationsReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - })); + }), operations); diff --git a/frontend/src/app/state/publish/actions.ts b/frontend/src/app/state/publish/actions.ts index cf716de5..63ef041f 100644 --- a/frontend/src/app/state/publish/actions.ts +++ b/frontend/src/app/state/publish/actions.ts @@ -5,37 +5,30 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createAction, createReducer } from '@reduxjs/toolkit'; -import { ErrorInfo } from '@app/framework'; +import { createAction } from '@reduxjs/toolkit'; +import { createExtendedReducer, createMutation } from '@app/framework'; import { Clients, PublishDto } from '@app/service'; -import { createApiThunk } from './../shared'; import { PublishState } from './state'; export const togglePublishDialog = createAction<{ open: boolean; values?: Partial }>('publish/dialog'); -export const publish = createApiThunk('publish/publish', - async (arg: { appId: string; params: PublishDto }) => { +export const publish = createMutation('publishing').with({ + name: 'publish/publish', + mutateFn: async (arg: { appId: string; params: PublishDto }) => { await Clients.Events.postEvents(arg.appId, { requests: [arg.params] }); - }); + }, +}); const initialState: PublishState = {}; -export const publishReducer = createReducer(initialState, builder => builder +const operations = [ + publish, +]; + +export const publishReducer = createExtendedReducer(initialState, builder => builder .addCase(togglePublishDialog, (state, action) => { state.dialogOpen = action.payload.open; state.dialogValues = action.payload?.values; - state.publishing = false; - state.publishingError = undefined; - }) - .addCase(publish.pending, (state) => { - state.publishing = true; - state.publishingError = undefined; - }) - .addCase(publish.rejected, (state, action) => { - state.publishing = false; - state.publishingError = action.payload as ErrorInfo; - }) - .addCase(publish.fulfilled, (state) => { - state.publishing = false; - state.publishingError = undefined; - })); + state.publishing = {}; + }), +operations); diff --git a/frontend/src/app/state/publish/state.ts b/frontend/src/app/state/publish/state.ts index ff78e479..17827d13 100644 --- a/frontend/src/app/state/publish/state.ts +++ b/frontend/src/app/state/publish/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo } from '@app/framework'; +import { MutationState } from '@app/framework'; import { PublishDto } from '@app/service'; export interface PublishStateInStore { @@ -13,11 +13,8 @@ export interface PublishStateInStore { } export interface PublishState { - // True when publishing. - publishing?: boolean; - - // The publishing error. - publishingError?: ErrorInfo; + // The mutation for publishing. + publishing?: MutationState; // True when the dialog is open. dialogOpen?: boolean; diff --git a/frontend/src/app/state/sms-templates/actions.ts b/frontend/src/app/state/sms-templates/actions.ts index 40a01139..2b88136a 100644 --- a/frontend/src/app/state/sms-templates/actions.ts +++ b/frontend/src/app/state/sms-templates/actions.ts @@ -5,47 +5,70 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer, Middleware } from '@reduxjs/toolkit'; +import { Middleware } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; -import { ErrorInfo, formatError, listThunk, Query } from '@app/framework'; -import { ChannelTemplateDto, Clients, UpdateChannelTemplateDtoOfSmsTemplateDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; -import { SmsTemplatesState } from './state'; - -const list = listThunk('smsTemplates', 'templates', async params => { - const { items, total } = await Clients.SmsTemplates.getTemplates(params.appId, params.search, params.take, params.skip); - - return { items, total }; +import { createExtendedReducer, createList, createMutation, formatError } from '@app/framework'; +import { Clients, UpdateChannelTemplateDtoOfSmsTemplateDto } from '@app/service'; +import { selectApp } from './../shared'; +import { SmsTemplatesState, SmsTemplatesStateInStore } from './state'; + +export const loadSmsTemplates = createList('templates', 'smsTemplates').with({ + name: 'smsTemplates/load', + queryFn: async (p: { appId: string }, q) => { + const { items, total } = await Clients.SmsTemplates.getTemplates(p.appId, q.search, q.take, q.skip); + + return { items, total }; + }, }); -export const loadSmsTemplates = (appId: string, query?: Partial, reset = false) => { - return list.action({ appId, query, reset }); -}; - -export const loadSmsTemplate = createApiThunk('smsTemplates/load', - (arg: { appId: string; id: string }) => { +export const loadSmsTemplate = createMutation('loadingTemplate').with({ + name: 'smsTemplates/loadOne', + mutateFn: (arg: { appId: string; id: string }) => { return Clients.SmsTemplates.getTemplate(arg.appId, arg.id); - }); + }, + updateFn(state, action) { + state.template = action.payload; + }, +}); -export const createSmsTemplate = createApiThunk('smsTemplates/create', - (arg: { appId: string }) => { +export const createSmsTemplate = createMutation('loadingTemplate').with({ + name: 'smsTemplates/create', + mutateFn: (arg: { appId: string }) => { return Clients.SmsTemplates.postTemplate(arg.appId, { }); - }); + }, + updateFn(state, action) { + state.template = action.payload; + }, +}); -export const createSmsTemplateLanguage = createApiThunk('smsTemplates/createLanguage', - (arg: { appId: string; id: string; language: string }) => { +export const createSmsTemplateLanguage = createMutation('loadingTemplate').with({ + name: 'smsTemplates/createLanguage', + mutateFn: (arg: { appId: string; id: string; language: string }) => { return Clients.SmsTemplates.postTemplateLanguage(arg.appId, arg.id, { language: arg.language }); - }); + }, + updateFn(state, action) { + state.template = action.payload; + }, +}); -export const updateSmsTemplate = createApiThunk('smsTemplates/update', - (arg: { appId: string; id: string; update: UpdateChannelTemplateDtoOfSmsTemplateDto }) => { +export const updateSmsTemplate = createMutation('loadingTemplate').with({ + name: 'smsTemplates/update', + mutateFn: (arg: { appId: string; id: string; update: UpdateChannelTemplateDtoOfSmsTemplateDto }) => { return Clients.SmsTemplates.putTemplate(arg.appId, arg.id, arg.update); - }); + }, + updateFn(state, action) { + if (state.template && state.template.id === action.meta.arg.id) { + state.template = { ...state.template, ...action.meta.arg.update }; + } + }, +}); -export const deleteSmsTemplate = createApiThunk('smsTemplates/delete', - (arg: { appId: string; id: string }) => { +export const deleteSmsTemplate = createMutation('loadingTemplate').with({ + name: 'smsTemplates/delete', + mutateFn: (arg: { appId: string; id: string }) => { return Clients.SmsTemplates.deleteTemplate(arg.appId, arg.id); - }); + }, +}); export const smsTemplatesMiddleware: Middleware = store => next => action => { const result = next(action); @@ -53,7 +76,7 @@ export const smsTemplatesMiddleware: Middleware = store => next => action => { if (createSmsTemplate.fulfilled.match(action) || deleteSmsTemplate.fulfilled.match(action)) { const { appId } = action.meta.arg; - store.dispatch(loadSmsTemplates(appId) as any); + store.dispatch(loadSmsTemplates({ appId }) as any); } else if (deleteSmsTemplate.rejected.match(action)) { toast.error(formatError(action.payload as any)); } @@ -62,63 +85,19 @@ export const smsTemplatesMiddleware: Middleware = store => next => action => { }; const initialState: SmsTemplatesState = { - templates: list.createInitial(), + templates: loadSmsTemplates.createInitial(), }; -export const smsTemplatesReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + createSmsTemplate, + createSmsTemplateLanguage, + deleteSmsTemplate, + loadSmsTemplate, + loadSmsTemplates, + updateSmsTemplate, +]; + +export const smsTemplatesReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - }) - .addCase(loadSmsTemplate.pending, (state) => { - state.loadingTemplate = true; - state.loadingTemplateError = undefined; - }) - .addCase(loadSmsTemplate.rejected, (state, action) => { - state.loadingTemplate = false; - state.loadingTemplateError = action.payload as ErrorInfo; - }) - .addCase(loadSmsTemplate.fulfilled, (state, action) => { - state.loadingTemplate = false; - state.loadingTemplateError = undefined; - state.template = action.payload; - }) - .addCase(createSmsTemplate.pending, (state) => { - state.creating = true; - state.creatingError = undefined; - }) - .addCase(createSmsTemplate.rejected, (state, action) => { - state.creating = false; - state.creatingError = action.payload as ErrorInfo; - }) - .addCase(createSmsTemplate.fulfilled, (state) => { - state.creating = false; - state.creatingError = undefined; - }) - .addCase(updateSmsTemplate.pending, (state) => { - state.updating = true; - state.updatingError = undefined; - }) - .addCase(updateSmsTemplate.rejected, (state, action) => { - state.updating = false; - state.updatingError = action.payload as ErrorInfo; - }) - .addCase(updateSmsTemplate.fulfilled, (state, action) => { - state.updating = false; - state.updatingError = undefined; - - if (state.template && state.template.id === action.meta.arg.id) { - state.template = { ...state.template, ...action.meta.arg.update }; - } - }) - .addCase(deleteSmsTemplate.pending, (state) => { - state.deleting = true; - state.deletingError = undefined; - }) - .addCase(deleteSmsTemplate.rejected, (state, action) => { - state.deleting = false; - state.deletingError = action.payload as ErrorInfo; - }) - .addCase(deleteSmsTemplate.fulfilled, (state) => { - state.deleting = false; - state.deletingError = undefined; - })); + }), operations); diff --git a/frontend/src/app/state/sms-templates/state.ts b/frontend/src/app/state/sms-templates/state.ts index 3211fc4f..7b89bcb3 100644 --- a/frontend/src/app/state/sms-templates/state.ts +++ b/frontend/src/app/state/sms-templates/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo, ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { ChannelTemplateDetailsDtoOfSmsTemplateDto, ChannelTemplateDto } from '@app/service'; export interface SmsTemplatesStateInStore { @@ -19,27 +19,15 @@ export interface SmsTemplatesState { // The template details. template?: ChannelTemplateDetailsDtoOfSmsTemplateDto; - // True if loading the Sms templates. - loadingTemplate?: boolean; + // Mutation for loading the Sms templates. + loadingTemplate?: MutationState; - // The Sms templates loading error. - loadingTemplateError?: ErrorInfo; + // Mutation for creating an Sms template. + creating?: MutationState; - // True if creating an Sms template. - creating?: boolean; + // Mutation for updating an Sms template. + updating?: MutationState; - // The error if creating an Sms template fails. - creatingError?: ErrorInfo; - - // True if updating an Sms template. - updating?: boolean; - - // The error if updating an Sms template fails. - updatingError?: ErrorInfo; - - // True if deleting an Sms template. - deleting?: boolean; - - // The error if deleting an Sms template language. - deletingError?: ErrorInfo; + // Mutation for deleting an Sms template. + deleting?: MutationState; } diff --git a/frontend/src/app/state/subscriptions/actions.ts b/frontend/src/app/state/subscriptions/actions.ts index 280389e7..86a3fdd8 100644 --- a/frontend/src/app/state/subscriptions/actions.ts +++ b/frontend/src/app/state/subscriptions/actions.ts @@ -5,33 +5,35 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; import { Middleware } from 'redux'; -import { ErrorInfo, formatError, listThunk, Query } from '@app/framework'; -import { Clients, SubscribeDto, SubscriptionDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; -import { SubscriptionsState } from './state'; +import { createExtendedReducer, createList, createMutation, formatError } from '@app/framework'; +import { Clients, SubscribeDto } from '@app/service'; +import { selectApp } from './../shared'; +import { SubscriptionsState, SubscriptionsStateInStore } from './state'; -const list = listThunk('subscriptions', 'subscriptions', async (params) => { - const { items, total } = await Clients.Users.getSubscriptions(params.appId, params.userId, params.search, params.take, params.skip); +export const loadSubscriptions = createList('subscriptions', 'subscriptions').with({ + name: 'subscriptions/load', + queryFn: async (p: { appId: string; userId: string }, q) => { + const { items, total } = await Clients.Users.getSubscriptions(p.appId, p.userId, q.search, q.take, q.skip); - return { items, total }; + return { items, total }; + }, }); -export const loadSubscriptions = (appId: string, userId: string, query?: Partial, reset = false) => { - return list.action({ appId, userId, query, reset }); -}; - -export const upsertSubscription = createApiThunk('subscriptions/upsert', - (arg: { appId: string; userId: string; params: SubscribeDto }) => { +export const upsertSubscription = createMutation('upserting').with({ + name: 'subscriptions/upsert', + mutateFn: (arg: { appId: string; userId: string; params: SubscribeDto }) => { return Clients.Users.postSubscriptions(arg.appId, arg.userId, { subscribe: [arg.params] }); - }); + }, +}); -export const deleteSubscription = createApiThunk('subscriptions/delete', - (arg: { appId: string; userId: string; topicPrefix: string }) => { +export const deleteSubscription = createMutation('upserting').with({ + name: 'subscriptions/delete', + mutateFn: (arg: { appId: string; userId: string; topicPrefix: string }) => { return Clients.Users.postSubscriptions(arg.appId, arg.userId, { unsubscribe: [arg.topicPrefix] }); - }); + }, +}); export const subscriptionsMiddleware: Middleware = store => next => action => { const result = next(action); @@ -39,7 +41,7 @@ export const subscriptionsMiddleware: Middleware = store => next => action => { if (upsertSubscription.fulfilled.match(action) || deleteSubscription.fulfilled.match(action)) { const { appId, userId } = action.meta.arg; - store.dispatch(loadSubscriptions(appId, userId) as any); + store.dispatch(loadSubscriptions({ appId, userId }) as any); } else if (deleteSubscription.rejected.match(action)) { toast.error(formatError(action.payload as any)); } @@ -48,22 +50,16 @@ export const subscriptionsMiddleware: Middleware = store => next => action => { }; const initialState: SubscriptionsState = { - subscriptions: list.createInitial(), + subscriptions: loadSubscriptions.createInitial(), }; -export const subscriptionsReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + upsertSubscription, + deleteSubscription, +]; + +export const subscriptionsReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - }) - .addCase(upsertSubscription.pending, (state) => { - state.upserting = true; - state.upsertingError = undefined; - }) - .addCase(upsertSubscription.rejected, (state, action) => { - state.upserting = false; - state.upsertingError = action.payload as ErrorInfo; - }) - .addCase(upsertSubscription.fulfilled, (state) => { - state.upserting = false; - state.upsertingError = undefined; - })); + }), +operations); diff --git a/frontend/src/app/state/subscriptions/state.ts b/frontend/src/app/state/subscriptions/state.ts index 838072db..acbbf279 100644 --- a/frontend/src/app/state/subscriptions/state.ts +++ b/frontend/src/app/state/subscriptions/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo, ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { SubscriptionDto } from '@app/service'; export interface SubscriptionsStateInStore { @@ -16,9 +16,6 @@ export interface SubscriptionsState { // All subscriptions. subscriptions: ListState; - // True if upserting. - upserting?: boolean; - - // The creating error. - upsertingError?: ErrorInfo; + // Mutation for upserting. + upserting?: MutationState; } diff --git a/frontend/src/app/state/system-users/actions.ts b/frontend/src/app/state/system-users/actions.ts index 2f93bec6..cb4847ec 100644 --- a/frontend/src/app/state/system-users/actions.ts +++ b/frontend/src/app/state/system-users/actions.ts @@ -5,44 +5,44 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; import { Middleware } from 'redux'; -import { ErrorInfo, formatError, listThunk, Query } from '@app/framework'; -import { Clients, SystemUserDto, UpdateSystemUserDto } from '@app/service'; +import { createExtendedReducer, createList, createMutation, formatError } from '@app/framework'; +import { Clients, UpdateSystemUserDto } from '@app/service'; import { createApiThunk } from './../shared'; -import { SystemUsersState } from './state'; +import { SystemUsersState, SystemUsersStateInStore } from './state'; -const list = listThunk('systemUsers', 'systemUsers', async params => { - const { items, total } = await Clients.SystemUsers.getUsers(params.search, params.take, params.skip); +export const loadSystemUsers = createList('systemUsers', 'systemUsers').with({ + name: 'systemUsers/load', + queryFn: async (_, p) => { + const { items, total } = await Clients.SystemUsers.getUsers(p.search, p.take, p.skip); - return { items, total }; + return { items, total }; + }, }); -export const loadSystemUsers = (query?: Partial, reset = false) => { - return list.action({ query, reset }); -}; - -export const upsertSystemUser = createApiThunk('system-users/upsert', - async (arg: { params: UpdateSystemUserDto; userId?: string }) => { +export const upsertSystemUser = createMutation('upserting').with({ + name: 'systemUsers/upsert', + mutateFn: async (arg: { params: UpdateSystemUserDto; userId?: string }) => { if (arg.userId) { return await Clients.SystemUsers.putUser(arg.userId, arg.params); } else { - return await Clients.SystemUsers.postUser(arg.params); + return await Clients.SystemUsers.postUser(arg.params as any); } - }); + }, +}); -export const lockSystemUser = createApiThunk('system-users/lock', +export const lockSystemUser = createApiThunk('systemUsers/lock', (arg: { userId: string }) => { return Clients.SystemUsers.lockUser(arg.userId); }); -export const unlockSystemUser = createApiThunk('system-users/unlock', +export const unlockSystemUser = createApiThunk('systemUsers/unlock', (arg: { userId: string }) => { return Clients.SystemUsers.unlockUser(arg.userId); }); -export const deleteSystemUser = createApiThunk('system-users/delete', +export const deleteSystemUser = createApiThunk('systemUsers/delete', (arg: { userId: string }) => { return Clients.SystemUsers.deleteUser(arg.userId); }); @@ -51,7 +51,7 @@ export const systemUsersMiddleware: Middleware = store => next => action => { const result = next(action); if (upsertSystemUser.fulfilled.match(action) || deleteSystemUser.fulfilled.match(action)) { - store.dispatch(loadSystemUsers() as any); + store.dispatch(loadSystemUsers({}) as any); } else if (deleteSystemUser.rejected.match(action)) { toast.error(formatError(action.payload as any)); } @@ -60,25 +60,19 @@ export const systemUsersMiddleware: Middleware = store => next => action => { }; const initialState: SystemUsersState = { - systemUsers: list.createInitial(), + systemUsers: loadSystemUsers.createInitial(), }; -export const systemUsersReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + loadSystemUsers, + upsertSystemUser, +]; + +export const systemUsersReducer = createExtendedReducer(initialState, builder => builder .addCase(lockSystemUser.fulfilled, (state, action) => { state.systemUsers.items?.replaceBy('id', action.payload); }) .addCase(unlockSystemUser.fulfilled, (state, action) => { state.systemUsers.items?.replaceBy('id', action.payload); - }) - .addCase(upsertSystemUser.pending, (state) => { - state.upserting = true; - state.upsertingError = undefined; - }) - .addCase(upsertSystemUser.rejected, (state, action) => { - state.upserting = false; - state.upsertingError = action.payload as ErrorInfo; - }) - .addCase(upsertSystemUser.fulfilled, (state) => { - state.upserting = false; - state.upsertingError = undefined; - })); + }), +operations); diff --git a/frontend/src/app/state/system-users/state.ts b/frontend/src/app/state/system-users/state.ts index ae51dbcf..1bdcd027 100644 --- a/frontend/src/app/state/system-users/state.ts +++ b/frontend/src/app/state/system-users/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo, ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { SystemUserDto } from '@app/service'; export interface SystemUsersStateInStore { @@ -16,12 +16,6 @@ export interface SystemUsersState { // All users. systemUsers: ListState; - // The current user. - systemUser?: SystemUserDto; - - // True if upserting. - upserting?: boolean; - - // The error if upserting fails. - upsertingError?: ErrorInfo; + // Mutation for upserting. + upserting?: MutationState; } diff --git a/frontend/src/app/state/templates/actions.ts b/frontend/src/app/state/templates/actions.ts index 107220e9..b5e6dab3 100644 --- a/frontend/src/app/state/templates/actions.ts +++ b/frontend/src/app/state/templates/actions.ts @@ -5,60 +5,60 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createAction, createReducer } from '@reduxjs/toolkit'; -import { ErrorInfo, listThunk } from '@app/framework'; +import { createAction } from '@reduxjs/toolkit'; +import { createExtendedReducer, createList, createMutation } from '@app/framework'; import { Clients, TemplateDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; -import { TemplatesState } from './state'; +import { selectApp } from './../shared'; +import { TemplatesState, TemplatesStateInStore } from './state'; -const list = listThunk('templates', 'templates', async params => { - const { items, total } = await Clients.Templates.getTemplates(params.appId, undefined, 1000, 0); +export const loadTemplates = createList('templates', 'templates').with({ + name: 'templates/load', + queryFn: async (p: { appId: string }) => { + const { items, total } = await Clients.Templates.getTemplates(p.appId, undefined, 1000, 0); - return { items, total }; + return { items, total }; + }, }); export const selectTemplate = createAction<{ code: string | undefined }>('templates/select'); -export const loadTemplates = (appId: string, reset = false) => { - return list.action({ appId, reset }); -}; - -export const upsertTemplate = createApiThunk('templates/upsert', - async (arg: { appId: string; params: TemplateDto }) => { +export const upsertTemplate = createMutation('upserting').with({ + name: 'templates/upsert', + mutateFn: async (arg: { appId: string; params: TemplateDto }) => { const response = await Clients.Templates.postTemplates(arg.appId, { requests: [arg.params] }); return response[0]; - }); + }, + updateFn(state, action) { + state.templates.items?.setOrPush(x => x.code, action.payload); + }, +}); -export const deleteTemplate = createApiThunk('templates/delete', - (arg: { appId: string; code: string }) => { +export const deleteTemplate = createMutation('upserting').with({ + name: 'templates/delete', + mutateFn: (arg: { appId: string; code: string }) => { return Clients.Templates.deleteTemplate(arg.appId, arg.code); - }); + }, + updateFn(state, action) { + state.templates.items?.removeByValue(x => x.code, action.meta.arg.code); + }, +}); const initialState: TemplatesState = { - templates: list.createInitial(), + templates: loadTemplates.createInitial(), }; -export const templatesReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + deleteTemplate, + loadTemplates, + upsertTemplate, +]; + +export const templatesReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; }) .addCase(selectTemplate, (state, action) => { state.currentTemplateCode = action.payload.code; - }) - .addCase(upsertTemplate.pending, (state) => { - state.upserting = true; - state.upsertingError = undefined; - }) - .addCase(upsertTemplate.rejected, (state, action) => { - state.upserting = true; - state.upsertingError = action.payload as ErrorInfo; - }) - .addCase(upsertTemplate.fulfilled, (state, action) => { - state.upserting = false; - state.upsertingError = undefined; - state.templates.items?.setOrPush(x => x.code, action.payload); - }) - .addCase(deleteTemplate.fulfilled, (state, action) => { - state.templates.items?.removeByValue(x => x.code, action.meta.arg.code); - })); + }), +operations); diff --git a/frontend/src/app/state/templates/state.ts b/frontend/src/app/state/templates/state.ts index 965bbcde..42fd71fa 100644 --- a/frontend/src/app/state/templates/state.ts +++ b/frontend/src/app/state/templates/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo, ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { TemplateDto } from '@app/service'; export interface TemplatesStateInStore { @@ -19,9 +19,6 @@ export interface TemplatesState { // The current template code. currentTemplateCode?: string; - // True if upserting. - upserting?: boolean; - - // The creating error. - upsertingError?: ErrorInfo; + // Mutation for upserting. + upserting?: MutationState; } diff --git a/frontend/src/app/state/topics/actions.ts b/frontend/src/app/state/topics/actions.ts index 18edf7e6..2c5f6619 100644 --- a/frontend/src/app/state/topics/actions.ts +++ b/frontend/src/app/state/topics/actions.ts @@ -5,34 +5,37 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer, Middleware } from '@reduxjs/toolkit'; +import { Middleware } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; -import { ErrorInfo, formatError, listThunk, Query } from '@app/framework'; -import { Clients, TopicDto, TopicQueryScope, UpsertTopicDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; -import { TopicsState } from './state'; +import { createExtendedReducer, createList, createMutation, formatError } from '@app/framework'; +import { Clients, TopicQueryScope, UpsertTopicDto } from '@app/service'; +import { selectApp } from './../shared'; +import { TopicsState, TopicsStateInStore } from './state'; -const list = listThunk('topics', 'topics', async params => { - const { items, total } = await Clients.Topics.getTopics(params.appId, params.scope, params.search, params.take, params.skip); +export const loadTopics = createList('topics', 'topics').with({ + name: 'topics/load', + queryFn: async (p: { appId: string; scope: TopicQueryScope }, q) => { + const { items, total } = await Clients.Topics.getTopics(p.appId, p.scope, q.search, q.take, q.skip); - return { items, total }; + return { items, total }; + }, }); -export const loadTopics = (appId: string, scope: TopicQueryScope, query?: Partial, reset = false) => { - return list.action({ appId, scope, query, reset }); -}; - -export const upsertTopic = createApiThunk('topics/upsert', - async (arg: { appId: string; scope: TopicQueryScope; params: UpsertTopicDto }) => { +export const upsertTopic = createMutation('upserting').with({ + name: 'topics/upsert', + mutateFn: async (arg: { appId: string; scope: TopicQueryScope; params: UpsertTopicDto }) => { const response = await Clients.Topics.postTopics(arg.appId, { requests: [arg.params] }); return response[0]; - }); + }, +}); -export const deleteTopic = createApiThunk('topics/delete', - (arg: { appId: string; path: string; scope: TopicQueryScope }) => { +export const deleteTopic = createMutation('upserting').with({ + name: 'topics/delete', + mutateFn: (arg: { appId: string; path: string; scope: TopicQueryScope }) => { return Clients.Topics.deleteTopic(arg.appId, arg.path); - }); + }, +}); export const topicsMiddleware: Middleware = store => next => action => { const result = next(action); @@ -40,7 +43,7 @@ export const topicsMiddleware: Middleware = store => next => action => { if (upsertTopic.fulfilled.match(action) || deleteTopic.fulfilled.match(action)) { const { appId, scope } = action.meta.arg; - store.dispatch(loadTopics(appId, scope) as any); + store.dispatch(loadTopics({ appId, scope }) as any); } else if (deleteTopic.rejected.match(action)) { toast.error(formatError(action.payload as any)); } @@ -49,22 +52,17 @@ export const topicsMiddleware: Middleware = store => next => action => { }; const initialState: TopicsState = { - topics: list.createInitial(), + topics: loadTopics.createInitial(), }; -export const topicsReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + deleteTopic, + loadTopics, + upsertTopic, +]; + +export const topicsReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - }) - .addCase(upsertTopic.pending, (state) => { - state.upserting = true; - state.upsertingError = undefined; - }) - .addCase(upsertTopic.rejected, (state, action) => { - state.upserting = false; - state.upsertingError = action.payload as ErrorInfo; - }) - .addCase(upsertTopic.fulfilled, (state) => { - state.upserting = false; - state.upsertingError = undefined; - })); + }), +operations); diff --git a/frontend/src/app/state/topics/state.ts b/frontend/src/app/state/topics/state.ts index 44d790ba..54053576 100644 --- a/frontend/src/app/state/topics/state.ts +++ b/frontend/src/app/state/topics/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo, ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { TopicDto } from '@app/service'; export interface TopicsStateInStore { @@ -16,9 +16,6 @@ export interface TopicsState { // All topics. topics: ListState; - // True if upserting. - upserting?: boolean; - - // The error if upserting fails. - upsertingError?: ErrorInfo; + // Mutation for upserting. + upserting?: MutationState; } diff --git a/frontend/src/app/state/users/actions.ts b/frontend/src/app/state/users/actions.ts index 49a028b4..504ff86e 100644 --- a/frontend/src/app/state/users/actions.ts +++ b/frontend/src/app/state/users/actions.ts @@ -5,50 +5,66 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { createReducer } from '@reduxjs/toolkit'; import { toast } from 'react-toastify'; import { Middleware } from 'redux'; -import { ErrorInfo, formatError, listThunk, Query } from '@app/framework'; -import { Clients, UpsertUserDto, UserDto } from '@app/service'; -import { createApiThunk, selectApp } from './../shared'; -import { UsersState } from './state'; - -const list = listThunk('users', 'users', async params => { - const { items, total } = await Clients.Users.getUsers(params.appId, params.search, params.take, params.skip, true); - - return { items, total }; +import { createExtendedReducer, createList, createMutation, formatError } from '@app/framework'; +import { Clients, UpsertUserDto } from '@app/service'; +import { selectApp } from './../shared'; +import { UsersState, UsersStateInStore } from './state'; + +export const loadUsers = createList('users', 'users').with({ + name: 'users/load', + queryFn: async (p: { appId: string }, q) => { + const { items, total } = await Clients.Users.getUsers(p.appId, q.search, q.take, q.skip, true); + + return { items, total }; + }, }); -export const loadUsers = (appId: string, query?: Partial, reset = false) => { - return list.action({ appId, query, reset }); -}; - -export const loadUser = createApiThunk('users/load', - (arg: { appId: string; userId: string }) => { +export const loadUser = createMutation('loadingUser').with({ + name: 'users/loadOne', + mutateFn: (arg: { appId: string; userId: string }) => { return Clients.Users.getUser(arg.appId, arg.userId, true); - }); + }, + updateFn(state, action) { + state.user = action.payload; + }, +}); -export const upsertUser = createApiThunk('users/upsert', - async (arg: { appId: string; params: UpsertUserDto }) => { +export const upsertUser = createMutation('upserting').with({ + name: 'users/upsert', + mutateFn: async (arg: { appId: string; params: UpsertUserDto }) => { const response = await Clients.Users.postUsers(arg.appId, { requests: [arg.params] }); return response[0]; - }); + }, + updateFn(state, action) { + if (!state.user || state.user.id === action.payload.id) { + state.user = action.payload; + } + }, +}); -export const deleteUserMobilePushToken = createApiThunk('users/mobilepush/delete', - async (arg: { appId: string; userId: string; token: string }) => { +export const deleteUserMobilePushToken = createMutation('upserting').with({ + name: 'users/mobilepush/delete', + mutateFn: async (arg: { appId: string; userId: string; token: string }) => { return await Clients.Users.deleteMobilePushToken(arg.appId, arg.userId, arg.token); - }); + }, +}); -export const deleteUserWebPushSubscription = createApiThunk('users/webpush/delete', - async (arg: { appId: string; userId: string; endpoint: string }) => { +export const deleteUserWebPushSubscription = createMutation('upserting').with({ + name: 'users/webpush/delete', + mutateFn: async (arg: { appId: string; userId: string; endpoint: string }) => { return await Clients.Users.deleteWebPushSubscription(arg.appId, arg.userId, arg.endpoint); - }); + }, +}); -export const deleteUser = createApiThunk('users/delete', - (arg: { appId: string; userId: string }) => { +export const deleteUser = createMutation('upserting').with({ + name: 'users/delete', + mutateFn: (arg: { appId: string; userId: string }) => { return Clients.Users.deleteUser(arg.appId, arg.userId); - }); + }, +}); export const usersMiddleware: Middleware = store => next => action => { const result = next(action); @@ -56,7 +72,7 @@ export const usersMiddleware: Middleware = store => next => action => { if (upsertUser.fulfilled.match(action) || deleteUser.fulfilled.match(action)) { const { appId } = action.meta.arg; - store.dispatch(loadUsers(appId) as any); + store.dispatch(loadUsers({ appId }) as any); } else if ( deleteUserMobilePushToken.rejected.match(action) || deleteUserWebPushSubscription.rejected.match(action) || @@ -74,40 +90,20 @@ export const usersMiddleware: Middleware = store => next => action => { }; const initialState: UsersState = { - users: list.createInitial(), + users: loadUsers.createInitial(), }; -export const usersReducer = createReducer(initialState, builder => list.initialize(builder) +const operations = [ + deleteUser, + deleteUserMobilePushToken, + deleteUserWebPushSubscription, + loadUser, + loadUsers, + upsertUser, +]; + +export const usersReducer = createExtendedReducer(initialState, builder => builder .addCase(selectApp, () => { return initialState; - }) - .addCase(loadUser.pending, (state) => { - state.loadingUser = true; - state.loadingUsersError = undefined; - }) - .addCase(loadUser.rejected, (state, action) => { - state.loadingUser = false; - state.loadingUsersError = action.payload as ErrorInfo; - state.user = undefined; - }) - .addCase(loadUser.fulfilled, (state, action) => { - state.loadingUser = false; - state.loadingUsersError = undefined; - state.user = action.payload as any; - }) - .addCase(upsertUser.pending, (state) => { - state.upserting = true; - state.upsertingError = undefined; - }) - .addCase(upsertUser.rejected, (state, action) => { - state.upserting = false; - state.upsertingError = action.payload as ErrorInfo; - }) - .addCase(upsertUser.fulfilled, (state, action) => { - state.upserting = false; - state.upsertingError = undefined; - - if (!state.user || state.user.id === action.payload.id) { - state.user = action.payload; - } - })); + }), +operations); diff --git a/frontend/src/app/state/users/state.ts b/frontend/src/app/state/users/state.ts index 42a688fe..2626d671 100644 --- a/frontend/src/app/state/users/state.ts +++ b/frontend/src/app/state/users/state.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved. */ -import { ErrorInfo, ListState } from '@app/framework'; +import { ListState, MutationState } from '@app/framework'; import { UserDto } from '@app/service'; export interface UsersStateInStore { @@ -19,15 +19,9 @@ export interface UsersState { // The current user. user?: UserDto; - // True if loading user. - loadingUser?: boolean; + // Mutation for loading user. + loadingUser?: MutationState; - // The user loading error. - loadingUsersError?: any; - - // True if upserting. - upserting?: boolean; - - // The error if upserting fails. - upsertingError?: ErrorInfo; + // Mutation for upserting. + upserting?: MutationState; } diff --git a/frontend/src/app/style/_common.scss b/frontend/src/app/style/_common.scss index 2ff6eba6..75fce699 100644 --- a/frontend/src/app/style/_common.scss +++ b/frontend/src/app/style/_common.scss @@ -569,6 +569,10 @@ $small-gutters: 4px; } } +.transition-all { + transition: all .2s ease-in-out; +} + .alert { position: relative; @@ -590,10 +594,17 @@ $small-gutters: 4px; background: lighten($color-background, 2%); border: 0; border-radius: $border-radius; + line-height: 1.7; padding-left: 4rem; padding-top: 1.25rem; padding-bottom: 1.25rem; + &-bordered { + background: $color-white; + border: 1px solid $color-border-dark; + border-radius: $border-radius; + } + & > i { @include absolute(1rem, null, null, 1rem); font-size: 1.75rem; @@ -829,6 +840,10 @@ div { } } +.react-tooltip { + z-index: 9000; +} + // // Error tooltip. // diff --git a/frontend/src/app/style/_static.scss b/frontend/src/app/style/_static.scss index d0f72847..2f6f29c1 100644 --- a/frontend/src/app/style/_static.scss +++ b/frontend/src/app/style/_static.scss @@ -158,6 +158,14 @@ $width: 500px; text-align: center; text-transform: uppercase; + &:first-child { + display: none; + } + + &:last-child { + display: none; + } + span { background: #fff; display: inline-block; diff --git a/frontend/src/app/style/icons/demo-files/demo.js b/frontend/src/app/style/icons/demo-files/demo.js index 6f45f1c4..a06d9781 100644 --- a/frontend/src/app/style/icons/demo-files/demo.js +++ b/frontend/src/app/style/icons/demo-files/demo.js @@ -2,15 +2,15 @@ if (!('boxShadow' in document.body.style)) { document.body.setAttribute('class', 'noBoxShadow'); } -document.body.addEventListener("click", function(e) { +document.body.addEventListener('click', function (e) { var target = e.target; - if (target.tagName === "INPUT" && + if (target.tagName === 'INPUT' && target.getAttribute('class').indexOf('liga') === -1) { target.select(); } }); -(function() { +(function () { var fontSize = document.getElementById('fontSize'), testDrive = document.getElementById('testDrive'), testText = document.getElementById('testText'); diff --git a/frontend/src/app/texts/en.ts b/frontend/src/app/texts/en.ts index cf0b0e60..1e56724c 100644 --- a/frontend/src/app/texts/en.ts +++ b/frontend/src/app/texts/en.ts @@ -31,6 +31,19 @@ export const EN = { header: 'Apps', notFound: 'App not found', }, + auth: { + authority: 'Authority', + clientId: 'Client ID', + clientSecret: 'Client Secret', + description: 'Define the configuration to your custom OIDC server.', + displayName: 'Display Name', + domain: 'Domain', + enable: 'Use custom OIDC server', + redirectUrl: 'Redirect URL', + redirectUrlHint: 'You have to allow this URL in your authentication server.', + signoutRedirectUrl: 'Signout Redirect URL', + title: 'Custom Auth', + }, code: 'en', common: { actions: 'Actions', diff --git a/frontend/src/sdk/sdk-worker.ts b/frontend/src/sdk/sdk-worker.ts index ec780e23..8de836ff 100644 --- a/frontend/src/sdk/sdk-worker.ts +++ b/frontend/src/sdk/sdk-worker.ts @@ -136,8 +136,8 @@ async function showNotification(self: ServiceWorkerGlobalScope, notification: No if (notification.confirmUrl && notification.confirmText && !notification.isConfirmed) { options.requireInteraction = true; - options.actions ||= []; - options.actions.push({ action: 'confirm', title: notification.confirmText }); + (options as any).actions ||= []; + (options as any).actions.push({ action: 'confirm', title: notification.confirmText }); } if (notification.body) { @@ -149,7 +149,7 @@ async function showNotification(self: ServiceWorkerGlobalScope, notification: No } if (notification.imageLarge) { - options.image = withPreset(notification.imageLarge, 'WebPushLarge'); + (options as any).image = withPreset(notification.imageLarge, 'WebPushLarge'); } await self.registration.showNotification(notification.subject, options); diff --git a/frontend/src/sdk/ui/components/Loader.tsx b/frontend/src/sdk/ui/components/Loader.tsx index 6b751c0f..4ca329d1 100644 --- a/frontend/src/sdk/ui/components/Loader.tsx +++ b/frontend/src/sdk/ui/components/Loader.tsx @@ -11,7 +11,7 @@ import { useEffect, useState } from 'preact/hooks'; import { Icon } from './Icon'; export interface LoaderProps { - // True if visible. + // True, if visible. visible: boolean; // The class. diff --git a/frontend/src/sdk/ui/components/Toggle.tsx b/frontend/src/sdk/ui/components/Toggle.tsx index 7d633e83..59a09732 100644 --- a/frontend/src/sdk/ui/components/Toggle.tsx +++ b/frontend/src/sdk/ui/components/Toggle.tsx @@ -20,7 +20,7 @@ export interface ToggleProps { // The field name. name: string; - // True if disabled. + // True, if disabled. disabled?: boolean; // Triggered when the value is changed. diff --git a/tools/TestSuite/TestSuite.ApiTests/SmsTests.cs b/tools/TestSuite/TestSuite.ApiTests/SmsTests.cs index 257af345..6f25ca8e 100644 --- a/tools/TestSuite/TestSuite.ApiTests/SmsTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/SmsTests.cs @@ -135,7 +135,7 @@ public async Task Should_send_sms_with_template() { var messages = await messageBird.GetMessagesAsync(200); - if (messages.Items.Any(x => x.Body == text && x.Recipients.Items[0].Status == "delivered")) + if (messages.Items.Any(x => x.Body == text && x.Recipients.Items.Any(y => y.Status == "delivered"))) { return; } @@ -239,7 +239,7 @@ public async Task Should_send_sms_without_template() { var messages = await messageBird.GetMessagesAsync(200); - if (messages.Items.Any(x => x.Body == subjectId && x.Recipients.Items[0].Status == "delivered")) + if (messages.Items.Any(x => x.Body == subjectId && x.Recipients.Items.Any(y => y.Status == "delivered"))) { return; } @@ -486,7 +486,7 @@ private static async Task AssertDeliveredAsync(string subjectId, int more) if (messages.Items.Any(x => x.Body.Contains(subjectId, StringComparison.OrdinalIgnoreCase) && x.Body.Contains(subjectMore, StringComparison.OrdinalIgnoreCase) && - x.Recipients.Items[0].Status == "delivered")) + x.Recipients.Items.Any(y => y.Status == "delivered"))) { return; } diff --git a/tools/sdk/Notifo.SDK/Generated.cs b/tools/sdk/Notifo.SDK/Generated.cs index fdf2ddef..49f6ba7c 100644 --- a/tools/sdk/Notifo.SDK/Generated.cs +++ b/tools/sdk/Notifo.SDK/Generated.cs @@ -9096,7 +9096,7 @@ public partial interface IEmailTemplatesClient /// /// Get the HTML preview for a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel template preview returned. /// A server side error occurred. @@ -9106,7 +9106,7 @@ public partial interface IEmailTemplatesClient /// /// Render a preview for a email template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template to render. /// Template rendered. /// A server side error occurred. @@ -9116,7 +9116,7 @@ public partial interface IEmailTemplatesClient /// /// Get the channel templates. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The optional query to search for items. /// The number of items to return. /// The number of items to skip. @@ -9128,7 +9128,7 @@ public partial interface IEmailTemplatesClient /// /// Create a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The request object. /// Channel template created. /// A server side error occurred. @@ -9147,7 +9147,7 @@ public partial interface IEmailTemplatesClient /// /// Get the channel template by id. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel templates returned. /// A server side error occurred. @@ -9157,7 +9157,7 @@ public partial interface IEmailTemplatesClient /// /// Create an app template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// Channel template created. @@ -9168,7 +9168,7 @@ public partial interface IEmailTemplatesClient /// /// Update an app template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// A server side error occurred. @@ -9178,7 +9178,7 @@ public partial interface IEmailTemplatesClient /// /// Delete a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel template deleted. /// A server side error occurred. @@ -9188,7 +9188,7 @@ public partial interface IEmailTemplatesClient /// /// Update a channel template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The language. /// The request object. @@ -9199,7 +9199,7 @@ public partial interface IEmailTemplatesClient /// /// Delete a language channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// The language. /// A server side error occurred. @@ -9321,7 +9321,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Get the HTML preview for a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel template preview returned. /// A server side error occurred. @@ -9422,7 +9422,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Render a preview for a email template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template to render. /// Template rendered. /// A server side error occurred. @@ -9537,7 +9537,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Get the channel templates. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The optional query to search for items. /// The number of items to return. /// The number of items to skip. @@ -9651,7 +9651,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Create a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The request object. /// Channel template created. /// A server side error occurred. @@ -9863,7 +9863,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Get the channel template by id. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel templates returned. /// A server side error occurred. @@ -9965,7 +9965,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Create an app template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// Channel template created. @@ -10085,7 +10085,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Update an app template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// A server side error occurred. @@ -10204,7 +10204,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Delete a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel template deleted. /// A server side error occurred. @@ -10310,7 +10310,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Update a channel template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The language. /// The request object. @@ -10435,7 +10435,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Delete a language channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// The language. /// A server side error occurred. @@ -10668,7 +10668,7 @@ public partial interface IMessagingTemplatesClient /// /// Get the channel templates. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The optional query to search for items. /// The number of items to return. /// The number of items to skip. @@ -10680,7 +10680,7 @@ public partial interface IMessagingTemplatesClient /// /// Create a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The request object. /// Channel template created. /// A server side error occurred. @@ -10699,7 +10699,7 @@ public partial interface IMessagingTemplatesClient /// /// Get the channel template by id. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel templates returned. /// A server side error occurred. @@ -10709,7 +10709,7 @@ public partial interface IMessagingTemplatesClient /// /// Create an app template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// Channel template created. @@ -10720,7 +10720,7 @@ public partial interface IMessagingTemplatesClient /// /// Update an app template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// A server side error occurred. @@ -10730,7 +10730,7 @@ public partial interface IMessagingTemplatesClient /// /// Delete a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel template deleted. /// A server side error occurred. @@ -10740,7 +10740,7 @@ public partial interface IMessagingTemplatesClient /// /// Update a channel template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The language. /// The request object. @@ -10751,7 +10751,7 @@ public partial interface IMessagingTemplatesClient /// /// Delete a language channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// The language. /// A server side error occurred. @@ -10789,7 +10789,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Get the channel templates. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The optional query to search for items. /// The number of items to return. /// The number of items to skip. @@ -10903,7 +10903,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Create a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The request object. /// Channel template created. /// A server side error occurred. @@ -11115,7 +11115,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Get the channel template by id. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel templates returned. /// A server side error occurred. @@ -11217,7 +11217,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Create an app template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// Channel template created. @@ -11337,7 +11337,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Update an app template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// A server side error occurred. @@ -11456,7 +11456,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Delete a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel template deleted. /// A server side error occurred. @@ -11562,7 +11562,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Update a channel template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The language. /// The request object. @@ -11687,7 +11687,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Delete a language channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// The language. /// A server side error occurred. @@ -11920,7 +11920,7 @@ public partial interface ISmsTemplatesClient /// /// Get the channel templates. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The optional query to search for items. /// The number of items to return. /// The number of items to skip. @@ -11932,7 +11932,7 @@ public partial interface ISmsTemplatesClient /// /// Create a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The request object. /// Channel template created. /// A server side error occurred. @@ -11951,7 +11951,7 @@ public partial interface ISmsTemplatesClient /// /// Get the channel template by id. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel templates returned. /// A server side error occurred. @@ -11961,7 +11961,7 @@ public partial interface ISmsTemplatesClient /// /// Create an app template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// Channel template created. @@ -11972,7 +11972,7 @@ public partial interface ISmsTemplatesClient /// /// Update an app template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// A server side error occurred. @@ -11982,7 +11982,7 @@ public partial interface ISmsTemplatesClient /// /// Delete a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel template deleted. /// A server side error occurred. @@ -11992,7 +11992,7 @@ public partial interface ISmsTemplatesClient /// /// Update a channel template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The language. /// The request object. @@ -12003,7 +12003,7 @@ public partial interface ISmsTemplatesClient /// /// Delete a language channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// The language. /// A server side error occurred. @@ -12041,7 +12041,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Get the channel templates. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The optional query to search for items. /// The number of items to return. /// The number of items to skip. @@ -12155,7 +12155,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Create a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The request object. /// Channel template created. /// A server side error occurred. @@ -12367,7 +12367,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Get the channel template by id. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel templates returned. /// A server side error occurred. @@ -12469,7 +12469,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Create an app template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// Channel template created. @@ -12589,7 +12589,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Update an app template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The request object. /// A server side error occurred. @@ -12708,7 +12708,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Delete a channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// Channel template deleted. /// A server side error occurred. @@ -12814,7 +12814,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Update a channel template language. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template code. /// The language. /// The request object. @@ -12939,7 +12939,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Delete a language channel template. /// - /// The id of the app where the templates belong to. + /// The ID of the app where the templates belong to. /// The template ID. /// The language. /// A server side error occurred. @@ -13187,10 +13187,10 @@ public partial interface IAppsClient /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// - /// Get app by id. + /// Get app by ID. /// - /// The id of the app. - /// Apps returned. + /// The ID of the app. + /// App returned. /// A server side error occurred. System.Threading.Tasks.Task GetAppAsync(string appId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); @@ -13204,11 +13204,38 @@ public partial interface IAppsClient /// A server side error occurred. System.Threading.Tasks.Task PutAppAsync(string appId, UpsertAppDto request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get app auth settings by ID. + /// + /// The ID of the app. + /// App auth settings returned. + /// A server side error occurred. + System.Threading.Tasks.Task GetAuthSchemeAsync(string appId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Updates the auth settings of the app. + /// + /// The ID of the app. + /// The request object. + /// App auth settings returned. + /// A server side error occurred. + System.Threading.Tasks.Task UpsertAuthSchemeAsync(string appId, AuthSchemeDto request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Deletes the auth settings of the app. + /// + /// The ID of the app. + /// A server side error occurred. + System.Threading.Tasks.Task DeleteAuthSchemeAsync(string appId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// /// Add an app contributor. /// - /// The id of the app. + /// The ID of the app. /// The request object. /// Apps returned. /// A server side error occurred. @@ -13218,7 +13245,7 @@ public partial interface IAppsClient /// /// Delete an app contributor. /// - /// The id of the app. + /// The ID of the app. /// The contributor to remove. /// Apps returned. /// A server side error occurred. @@ -13228,7 +13255,7 @@ public partial interface IAppsClient /// /// Get the app integrations. /// - /// The id of the app where the integrations belong to. + /// The ID of the app where the integrations belong to. /// App email templates returned. /// A server side error occurred. System.Threading.Tasks.Task GetIntegrationsAsync(string appId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); @@ -13237,7 +13264,7 @@ public partial interface IAppsClient /// /// Create an app integrations. /// - /// The id of the app where the integration belong to. + /// The ID of the app where the integration belong to. /// The request object. /// App integration created. /// A server side error occurred. @@ -13247,8 +13274,8 @@ public partial interface IAppsClient /// /// Update an app integration. /// - /// The id of the app where the integration belong to. - /// The id of the integration. + /// The ID of the app where the integration belong to. + /// The ID of the integration. /// The request object. /// App integration updated. /// A server side error occurred. @@ -13258,8 +13285,8 @@ public partial interface IAppsClient /// /// Delete an app integration. /// - /// The id of the app where the email templates belong to. - /// The id of the integration. + /// The ID of the app where the email templates belong to. + /// The ID of the integration. /// App integration deleted. /// A server side error occurred. System.Threading.Tasks.Task DeleteIntegrationAsync(string appId, string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); @@ -13482,10 +13509,10 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// - /// Get app by id. + /// Get app by ID. /// - /// The id of the app. - /// Apps returned. + /// The ID of the app. + /// App returned. /// A server side error occurred. public virtual async System.Threading.Tasks.Task GetAppAsync(string appId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { @@ -13690,11 +13717,323 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() } } + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get app auth settings by ID. + /// + /// The ID of the app. + /// App auth settings returned. + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetAuthSchemeAsync(string appId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (appId == null) + throw new System.ArgumentNullException("appId"); + + var client_ = _httpClientProvider.Get(); + #pragma warning disable CS0219 // Variable is assigned but its value is never used + var disposeClient_ = false; + #pragma warning restore CS0219 // Variable is assigned but its value is never used + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/apps/{appId}/auth" + urlBuilder_.Append("api/apps/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(appId, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/auth"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200 || status_ == 201) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new NotifoException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 404) + { + string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new NotifoException("App not found.", status_, responseText_, headers_, null); + } + else + if (status_ == 500) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new NotifoException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new NotifoException("Operation failed.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new NotifoException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + _httpClientProvider.Return(client_); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Updates the auth settings of the app. + /// + /// The ID of the app. + /// The request object. + /// App auth settings returned. + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task UpsertAuthSchemeAsync(string appId, AuthSchemeDto request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (appId == null) + throw new System.ArgumentNullException("appId"); + + if (request == null) + throw new System.ArgumentNullException("request"); + + var client_ = _httpClientProvider.Get(); + #pragma warning disable CS0219 // Variable is assigned but its value is never used + var disposeClient_ = false; + #pragma warning restore CS0219 // Variable is assigned but its value is never used + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = Newtonsoft.Json.JsonConvert.SerializeObject(request, _settings.Value); + var content_ = new System.Net.Http.StringContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/apps/{appId}/auth" + urlBuilder_.Append("api/apps/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(appId, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/auth"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200 || status_ == 201) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new NotifoException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 404) + { + string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new NotifoException("App not found.", status_, responseText_, headers_, null); + } + else + if (status_ == 400) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new NotifoException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new NotifoException("Validation error.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 500) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new NotifoException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new NotifoException("Operation failed.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new NotifoException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + _httpClientProvider.Return(client_); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Deletes the auth settings of the app. + /// + /// The ID of the app. + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task DeleteAuthSchemeAsync(string appId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (appId == null) + throw new System.ArgumentNullException("appId"); + + var client_ = _httpClientProvider.Get(); + #pragma warning disable CS0219 // Variable is assigned but its value is never used + var disposeClient_ = false; + #pragma warning restore CS0219 // Variable is assigned but its value is never used + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/apps/{appId}/auth" + urlBuilder_.Append("api/apps/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(appId, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/auth"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 204) + { + return; + } + else + if (status_ == 404) + { + string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new NotifoException("App not found.", status_, responseText_, headers_, null); + } + else + if (status_ == 400) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new NotifoException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new NotifoException("Validation error.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 500) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new NotifoException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new NotifoException("Operation failed.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new NotifoException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + _httpClientProvider.Return(client_); + } + } + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// /// Add an app contributor. /// - /// The id of the app. + /// The ID of the app. /// The request object. /// Apps returned. /// A server side error occurred. @@ -13809,7 +14148,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Delete an app contributor. /// - /// The id of the app. + /// The ID of the app. /// The contributor to remove. /// Apps returned. /// A server side error occurred. @@ -13922,7 +14261,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Get the app integrations. /// - /// The id of the app where the integrations belong to. + /// The ID of the app where the integrations belong to. /// App email templates returned. /// A server side error occurred. public virtual async System.Threading.Tasks.Task GetIntegrationsAsync(string appId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) @@ -14019,7 +14358,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Create an app integrations. /// - /// The id of the app where the integration belong to. + /// The ID of the app where the integration belong to. /// The request object. /// App integration created. /// A server side error occurred. @@ -14134,8 +14473,8 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Update an app integration. /// - /// The id of the app where the integration belong to. - /// The id of the integration. + /// The ID of the app where the integration belong to. + /// The ID of the integration. /// The request object. /// App integration updated. /// A server side error occurred. @@ -14248,8 +14587,8 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// /// Delete an app integration. /// - /// The id of the app where the email templates belong to. - /// The id of the integration. + /// The ID of the app where the email templates belong to. + /// The ID of the integration. /// App integration deleted. /// A server side error occurred. public virtual async System.Threading.Tasks.Task DeleteIntegrationAsync(string appId, string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) @@ -14979,7 +15318,7 @@ public partial class ListResponseDtoOfUserDto public partial class UserDto { /// - /// The id of the user. + /// The ID of the user. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -15195,7 +15534,7 @@ public partial class UpsertUsersDto public partial class UpsertUserDto { /// - /// The id of the user. + /// The ID of the user. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Id { get; set; } @@ -15613,7 +15952,7 @@ public partial class ListResponseDtoOfSystemUserDto public partial class SystemUserDto { /// - /// The id of the user. + /// The ID of the user. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -15896,7 +16235,7 @@ public partial class HandledInfoDto public abstract partial class UserNotificationBaseDto { /// - /// The id of the notification. + /// The ID of the notification. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -16069,13 +16408,13 @@ public enum DeviceNotificationsQueryScope public partial class TrackNotificationDto { /// - /// The id of the noitifications to mark as confirmed. + /// The ID of the noitifications to mark as confirmed. /// [Newtonsoft.Json.JsonProperty("confirmed", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Confirmed { get; set; } /// - /// The id of the noitifications to mark as seen. + /// The ID of the noitifications to mark as seen. /// [Newtonsoft.Json.JsonProperty("seen", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public System.Collections.Generic.List Seen { get; set; } @@ -16391,7 +16730,7 @@ public partial class ListResponseDtoOfEventDto public partial class EventDto { /// - /// The id of the event. + /// The ID of the event. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -16746,7 +17085,7 @@ public partial class ListResponseDtoOfChannelTemplateDto public partial class ChannelTemplateDto { /// - /// The id of the template. + /// The ID of the template. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -16842,7 +17181,7 @@ public enum LiquidPropertyType public partial class ChannelTemplateDetailsDtoOfEmailTemplateDto { /// - /// The id of the template. + /// The ID of the template. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -16965,7 +17304,7 @@ public partial class UpdateChannelTemplateDtoOfEmailTemplateDto public partial class ChannelTemplateDetailsDtoOfMessagingTemplateDto { /// - /// The id of the template. + /// The ID of the template. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -17045,7 +17384,7 @@ public partial class UpdateChannelTemplateDtoOfMessagingTemplateDto public partial class ChannelTemplateDetailsDtoOfSmsTemplateDto { /// - /// The id of the template. + /// The ID of the template. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -17125,7 +17464,7 @@ public partial class UpdateChannelTemplateDtoOfSmsTemplateDto public partial class AppDto { /// - /// The id of the app. + /// The ID of the app. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -17186,7 +17525,7 @@ public partial class AppDto public partial class AppDetailsDto { /// - /// The id of the app. + /// The ID of the app. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -17260,7 +17599,7 @@ public partial class AppDetailsDto public partial class AppContributorDto { /// - /// The id of the user. + /// The ID of the user. /// [Newtonsoft.Json.JsonProperty("userId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] @@ -17282,6 +17621,62 @@ public partial class AppContributorDto } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AuthSchemeResponseDto + { + /// + /// The auth scheme if configured. + /// + [Newtonsoft.Json.JsonProperty("scheme", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public AuthSchemeDto Scheme { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class AuthSchemeDto + { + /// + /// The domain name of your user accounts. + /// + [Newtonsoft.Json.JsonProperty("domain", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Domain { get; set; } + + /// + /// The display name for buttons. + /// + [Newtonsoft.Json.JsonProperty("displayName", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [System.ComponentModel.DataAnnotations.Required] + public string DisplayName { get; set; } + + /// + /// The client ID. + /// + [Newtonsoft.Json.JsonProperty("clientId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [System.ComponentModel.DataAnnotations.Required] + public string ClientId { get; set; } + + /// + /// The client secret. + /// + [Newtonsoft.Json.JsonProperty("clientSecret", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [System.ComponentModel.DataAnnotations.Required] + public string ClientSecret { get; set; } + + /// + /// The authority URL. + /// + [Newtonsoft.Json.JsonProperty("authority", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [System.ComponentModel.DataAnnotations.Required] + public string Authority { get; set; } + + /// + /// The URL to redirect after a signout. + /// + [Newtonsoft.Json.JsonProperty("signoutRedirectUrl", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string SignoutRedirectUrl { get; set; } + + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpsertAppDto { @@ -17603,7 +17998,7 @@ public enum PropertyType public partial class IntegrationCreatedDto { /// - /// The id of the integration. + /// The ID of the integration. /// [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]