Use Fluxor for state management

This commit is contained in:
Neil Brommer 2021-12-03 16:44:02 -08:00
parent 437e90039f
commit 560c25b4e8
27 changed files with 823 additions and 210 deletions

View file

@ -1,3 +1,5 @@
<Fluxor.Blazor.Web.StoreInitializer />
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">

View file

@ -1,6 +1,4 @@
@using Start.Shared
<div class="activeBookmarkContainer">
<div class="activeBookmarkContainer">
@if (this.Container == null)
{
<div class="empty">

View file

@ -1,6 +1,4 @@
@using Start.Shared
<h2 class="bookmarkGroupTitle" style="background-color: #@this.Group.Color">
<h2 class="bookmarkGroupTitle" style="background-color: #@this.Group.Color">
@this.Group.Title
</h2>
<ul class="bookmarkGroupList">

View file

@ -1,15 +1,17 @@
@using Start.Shared
@using Start.Shared.Api
@using Refit
@using Start.Client.Store.Features.CreateContainer
@using Fluxor
@inject IBookmarkContainersApi bookmarkContainersApi
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
<Dialog Title="Create Container" Active="this.IsOpen" OnClose="this.OnDialogClose">
<EditForm Model="this.model" OnValidSubmit="this.OnSubmit">
@if (displayError)
@inject IDispatcher dispatch
@inject IState<CreateContainerState> state
<Dialog Title="Create Container" Active="this.state.Value.ShowCreateContainerForm" OnClose="this.OnDialogClose">
<EditForm Model="this.Model" OnValidSubmit="this.OnSubmit">
@if (this.state.Value.CreateContainerErrorMessage != null)
{
<Alert Type="Alert.AlertType.Error">
There was an error creating the container
@this.state.Value.CreateContainerErrorMessage
</Alert>
}
<div class="form-group">
@ -19,7 +21,7 @@
<div>
<label for="createBookmarkContainerTitle" class="form-label">Title</label>
<InputText id="createBookmarkContainerTitle" name="createBookmarkContainerTitle"
class="form-input" @bind-Value="this.model.Title" />
class="form-input" @bind-Value="this.Model.Title" />
</div>
</div>
</div>
@ -28,9 +30,18 @@
<div class="columns">
<div class="column col-12 text-right">
<div>
<button type="submit" class="btn btn-primary">
<i class="icon icon-plus"></i> Create
</button>
@if (this.state.Value.IsLoadingCreateContainer)
{
<button type="submit" disabled class="btn btn-primary loading">
<i class="icon icon-plus"></i> Create
</button>
}
else
{
<button type="submit" class="btn btn-primary">
<i class="icon icon-plus"></i> Create
</button>
}
</div>
</div>
</div>
@ -42,34 +53,16 @@
@code {
[Parameter]
public EventCallback<BookmarkContainerDto> OnCreated { get; set; }
[Parameter]
public bool IsOpen { get; set; }
[Parameter]
public EventCallback OnClose { get; set; }
private BookmarkContainerDto model = new("");
private bool displayError = false;
private BookmarkContainerDto Model { get; set; } = new BookmarkContainerDto("");
protected async void OnSubmit()
protected void OnSubmit()
{
ApiResponse<BookmarkContainerDto?> apiResponse = await bookmarkContainersApi
.CreateBookmarkContainer(model.Title);
BookmarkContainerDto? container = apiResponse.Content;
if (container == null)
{
this.displayError = true;
}
else
{
await this.OnCreated.InvokeAsync(container);
}
dispatch.Dispatch(new SubmitCreateContainerAction(this.Model));
}
protected async void OnDialogClose()
protected void OnDialogClose()
{
this.IsOpen = false;
await this.OnClose.InvokeAsync();
dispatch.Dispatch(new HideCreateContainerFormAction());
}
}

View file

@ -1,66 +1,48 @@
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Start.Shared.Api
@using Start.Client.Store.Features.DeleteContainer
@using Fluxor
@inject HttpClient Http
@inject IBookmarkContainersApi bookmarkContainersApi
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@{ string title = $"Delete Container \"{this.ContainerTitle}\""; }
@inject IDispatcher dispatch
@inject IState<DeleteContainerState> state
<Dialog Title="@title" Active="this.Active">
@if (this.ShowAlert)
@{ string title = $"Delete Container \"{this.state.Value.BookmarkContainerTitleToDelete}\""; }
<Dialog Title="@title" Active="this.state.Value.ShowDeleteContainerForm" OnClose="this.OnDialogClose">
@if (this.state.Value.DeleteContainerErrorMessage != null)
{
<div class="toast toast-error">
There was an error deleting the bookmark container
</div>
<Alert Type="Alert.AlertType.Error">
@this.state.Value.DeleteContainerErrorMessage
</Alert>
}
<p>Are you sure you want to delete the bookmark container "@this.ContainerTitle"?</p>
<p>
Are you sure you want to delete the bookmark container
"@this.state.Value.BookmarkContainerTitleToDelete"?
</p>
<div class="text-right">
<button class="btn" @onclick="this.OnDialogClose">Cancel</button>
<button class="btn btn-error" @onclick="this.OnConfirmDelete">Delete</button>
@if (!this.state.Value.IsLoadingDeleteContainer)
{
<button type="button" class="btn" @onclick="this.OnDialogClose">Cancel</button>
<button type="submit" class="btn btn-error" @onclick="this.OnConfirmDelete">Delete</button>
}
else
{
<button type="button" disabled class="btn" @onclick="this.OnDialogClose">Cancel</button>
<button type="submit" disabled class="btn btn-error loading" @onclick="this.OnConfirmDelete">Delete</button>
}
</div>
</Dialog>
@code {
[Parameter]
public int BookmarkContainerId { get; set; }
[Parameter]
public string ContainerTitle { get; set; } = null!;
[Parameter]
public bool Active { get; set; }
[Parameter]
public EventCallback<int> OnDeleted { get; set; }
[Parameter]
public EventCallback OnClose { get; set; }
public bool ShowAlert { get; set; } = false;
public async Task OnDialogClose()
public void OnDialogClose()
{
this.Active = false;
await this.OnClose.InvokeAsync();
this.dispatch.Dispatch(new HideDeleteContainerFormAction());
}
public async Task OnConfirmDelete()
public void OnConfirmDelete()
{
try
{
HttpResponseMessage result = await bookmarkContainersApi
.DeleteBookmarkContainer(this.BookmarkContainerId);
if (result.StatusCode == System.Net.HttpStatusCode.OK)
{
await this.OnDeleted.InvokeAsync(BookmarkContainerId);
this.ShowAlert = false;
this.Active = false;
}
else
{
this.ShowAlert = true;
}
}
catch (AccessTokenNotAvailableException e)
{
e.Redirect();
}
this.dispatch.Dispatch(new SubmitDeleteContainerAction(
this.state.Value.BookmarkContainerIdToDelete));
}
}

View file

@ -1,18 +1,30 @@
@page "/"
@using System.Collections.Generic
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@using System.Linq
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Start.Client.Components
@using Start.Shared
@using Start.Shared.Api
@using Refit
@using Start.Client.Store.State
@using Start.Client.Store.Features.ContainersList
@using Start.Client.Store.Features.CurrentContainer
@using Start.Client.Store.Features.CreateContainer
@using Start.Client.Store.Features.DeleteContainer
@using Fluxor
@* Distinguish from Refit.Authorize *@
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject IBookmarkContainersApi bookmarkContainersApi
@if (bookmarkContainers == null)
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject IState<RootState> state
@inject IDispatcher dispatch
@if (this.state.Value.ContainerListState.ErrorMessage != null) {
<Alert Type="Alert.AlertType.Error">
<b>Error</b> @this.state.Value.ContainerListState.ErrorMessage
</Alert>
}
@if (this.state.Value.ContainerListState.IsLoadingContainersList)
{
<div class="empty">
<div class="empty-icon">
@ -24,10 +36,10 @@
else
{
<ul class="containerList tab">
@foreach (BookmarkContainerDto container in this.bookmarkContainers)
@foreach (BookmarkContainerDto container in this.state.Value.ContainerListState.Containers)
{
string itemClasses = "tab-item";
if (container.BookmarkContainerId == this.selectedBookmarkContainer?.BookmarkContainerId)
if (container.BookmarkContainerId == this.state.Value.CurrentContainerState.Container?.BookmarkContainerId)
itemClasses += " active";
<li class="@itemClasses">
@ -47,132 +59,41 @@ else
</li>
</ul>
<BookmarkContainer Container="this.selectedBookmarkContainer" />
<BookmarkContainer Container="this.state.Value.CurrentContainerState.Container" />
<CreateContainer IsOpen="showCreateContainerForm" OnClose="this.OnCloseCreateContainer"
OnCreated="this.OnContainerCreated" />
<DeleteContainer Active="this.showDeleteContainerForm" OnClose="this.OnCloseDeleteContainer"
BookmarkContainerId="this.bookmarkContainerToDelete?.BookmarkContainerId ?? 0"
ContainerTitle="@(this.bookmarkContainerToDelete?.Title ?? "")"
OnDeleted="this.OnContainerDeleted" />
<CreateContainer />
<DeleteContainer />
}
@code
{
private IList<BookmarkContainerDto>? bookmarkContainers;
private BookmarkContainerDto? selectedBookmarkContainer;
private bool showCreateContainerForm = false;
private bool showDeleteContainerForm = false;
private BookmarkContainerDto? bookmarkContainerToDelete;
private bool showCreateGroupForm = false;
private bool showCreateBookmarkForm = false;
protected override async Task OnInitializedAsync()
{
await LoadContainers();
this.dispatch.Dispatch(new LoadContainerListAction());
this.dispatch.Dispatch(new LoadCurrentContainerAction(await this.GetSelectedContainerId()));
}
protected async Task LoadContainers()
protected void OnContainerSelected(int bookmarkContainerId)
{
try
{
ApiResponse<IEnumerable<BookmarkContainerDto>> response = await bookmarkContainersApi
.GetAllBookmarkContainers();
this.bookmarkContainers = response.Content?.ToList();
if (this.bookmarkContainers == null || !this.bookmarkContainers.Any())
{
await this.CreateDefaultContainer();
}
await this.OnContainerSelected(await this.GetSelectedContainerId());
}
catch (AccessTokenNotAvailableException e)
{
e.Redirect();
}
}
protected async Task CreateDefaultContainer()
{
ApiResponse<BookmarkContainerDto?> response = await bookmarkContainersApi
.CreateBookmarkContainer("Default");
BookmarkContainerDto? container = response.Content;
if (container != null)
await this.OnContainerSelected(container.BookmarkContainerId);
}
protected async Task OnContainerSelected(int bookmarkContainerId)
{
try
{
if (!this.bookmarkContainers?.Any(bc => bc.BookmarkContainerId == bookmarkContainerId) ?? false)
bookmarkContainerId = this.bookmarkContainers?.First().BookmarkContainerId ?? bookmarkContainerId;
ApiResponse<BookmarkContainerDto?> response = await bookmarkContainersApi
.GetBookmarkContainer(bookmarkContainerId);
BookmarkContainerDto? container = response.Content;
await this.SetSelectedContainer(bookmarkContainerId);
this.selectedBookmarkContainer = container;
}
catch (AccessTokenNotAvailableException e)
{
e.Redirect();
}
dispatch.Dispatch(new LoadCurrentContainerAction(bookmarkContainerId));
}
protected void OnDeleteContainerClicked(int bookmarkContainerId)
{
this.bookmarkContainerToDelete = this.bookmarkContainers
?.First(bc => bc.BookmarkContainerId == bookmarkContainerId);
this.showDeleteContainerForm = true;
BookmarkContainerDto? bookmarkContainerToDelete = this.state.Value.ContainerListState
.Containers
?.FirstOrDefault(bc => bc.BookmarkContainerId == bookmarkContainerId);
if (bookmarkContainerToDelete == null)
return;
this.dispatch.Dispatch(new ShowDeleteContainerFormAction(
bookmarkContainerToDelete.BookmarkContainerId, bookmarkContainerToDelete.Title));
}
protected void OnCreateContainerClicked()
{
this.showCreateContainerForm = true;
}
protected void OnCloseCreateContainer()
{
this.showCreateContainerForm = false;
}
protected async Task OnContainerCreated(BookmarkContainerDto newContainer)
{
if (this.bookmarkContainers == null)
return;
this.bookmarkContainers.Add(newContainer);
this.showCreateContainerForm = false;
await OnContainerSelected(newContainer.BookmarkContainerId);
}
protected void OnCloseDeleteContainer()
{
this.showDeleteContainerForm = false;
}
protected async Task OnContainerDeleted(int bookmarkContainerId)
{
if (!this.bookmarkContainers?.Any(bc => bc.BookmarkContainerId != bookmarkContainerId) ?? false)
await this.CreateDefaultContainer();
if (await this.GetSelectedContainerId() == bookmarkContainerId)
await this.OnContainerSelected(
this.bookmarkContainers?.First().BookmarkContainerId ?? bookmarkContainerId);
this.bookmarkContainers = this.bookmarkContainers
?.Where(bc => bc.BookmarkContainerId != bookmarkContainerId)
.ToList();
dispatch.Dispatch(new ShowCreateContainerFormAction());
}
// Save the currently selected container in LocalStorage so that the same container remains
@ -186,7 +107,8 @@ else
return await localStorage.GetItemAsync<int>("SelectedContainer");
// Default to the first container
int firstContainer = this.bookmarkContainers!.First().BookmarkContainerId;
int firstContainer = this.state.Value.ContainerListState.Containers
.First().BookmarkContainerId;
await this.SetSelectedContainer(firstContainer);
return firstContainer;
}

View file

@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using Blazored.LocalStorage;
using Refit;
using Start.Shared.Api;
using Fluxor;
namespace Start.Client {
public class Program {
@ -40,6 +41,13 @@ namespace Start.Client {
builder.Services.AddApiAuthorization();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddFluxor(opt => {
opt.ScanAssemblies(typeof(Program).Assembly);
#if DEBUG
opt.UseReduxDevTools();
#endif
});
await builder.Build().RunAsync();
}
}

View file

@ -19,6 +19,9 @@
<PackageReference Include="Blazored.LocalStorage" Version="4.1.5" />
<PackageReference Include="Refit" Version="6.1.15" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.1.15" />
<PackageReference Include="Fluxor" Version="4.2.1" />
<PackageReference Include="Fluxor.Blazor.Web" Version="4.2.1" />
<PackageReference Include="Fluxor.Blazor.Web.ReduxDevTools" Version="4.2.1" />
</ItemGroup>
<ItemGroup>
@ -33,8 +36,27 @@
<None Remove="Blazored.LocalStorage" />
<None Remove="Refit" />
<None Remove="Refit.HttpClientFactory" />
<None Remove="Fluxor" />
<None Remove="Fluxor.Blazor.Web" />
<None Remove="Fluxor.Blazor.Web.ReduxDevTools" />
<None Remove="Store\" />
<None Remove="Store\Features\" />
<None Remove="Store\State\" />
<None Remove="Store\Features\CreateContainer\" />
<None Remove="Store\Features\DeleteContainer\" />
<None Remove="Store\Features\ContainersList\" />
<None Remove="Store\Features\CurrentContainer\" />
</ItemGroup>
<ItemGroup>
<Content Remove="wwwroot\css\Spectre\" />
</ItemGroup>
<ItemGroup>
<Folder Include="Store\" />
<Folder Include="Store\Features\" />
<Folder Include="Store\State\" />
<Folder Include="Store\Features\CreateContainer\" />
<Folder Include="Store\Features\DeleteContainer\" />
<Folder Include="Store\Features\ContainersList\" />
<Folder Include="Store\Features\CurrentContainer\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,42 @@
using System.Collections.Generic;
using Start.Shared;
namespace Start.Client.Store.Features.ContainersList {
/// <summary>Dispatch before sending an API request</summary>
public class FetchContainerListAction { }
/// <summary>Dispatch after recieving the container list from an API request</summary>
public class RecievedContainerListAction {
public IList<BookmarkContainerDto> Containers { get; set; }
public RecievedContainerListAction(IList<BookmarkContainerDto> containers) {
this.Containers = containers;
}
}
public class ErrorFetchingContainerListAction {
public string ErrorMessage { get; set; }
public ErrorFetchingContainerListAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
public class LoadContainerListAction { }
public class RemoveContainerFromListAction {
public int ContainerIdToRemove { get; set; }
public RemoveContainerFromListAction(int containerIdToRemove) {
this.ContainerIdToRemove = containerIdToRemove;
}
}
public class AddContainerToListAction {
public BookmarkContainerDto NewContainer { get; set; }
public AddContainerToListAction(BookmarkContainerDto newContainer) {
this.NewContainer = newContainer;
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Refit;
using Start.Shared;
using Start.Shared.Api;
namespace Start.Client.Store.Features.ContainersList {
public class ContainerListEffects {
public IBookmarkContainersApi BookmarkContainersApi { get; init; }
public ContainerListEffects(IBookmarkContainersApi bookmarkContainersApi) {
this.BookmarkContainersApi = bookmarkContainersApi;
}
[EffectMethod(typeof(LoadContainerListAction))]
public async Task LoadContainerList(IDispatcher dispatch) {
dispatch.Dispatch(new FetchContainerListAction());
try {
ApiResponse<IEnumerable<BookmarkContainerDto>> response = await this
.BookmarkContainersApi
.GetAllBookmarkContainers();
List<BookmarkContainerDto>? bookmarkContainers = response.Content?.ToList();
if (bookmarkContainers == null) {
dispatch.Dispatch(new ErrorFetchingContainerListAction(
"Failed to fetch containers list"));
return;
}
if (!bookmarkContainers.Any())
throw new NotImplementedException("Create bookmark effect has not been created");
dispatch.Dispatch(new RecievedContainerListAction(bookmarkContainers));
}
catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
}
}

View file

@ -0,0 +1,63 @@
using System.Collections.Immutable;
using System.Linq;
using Fluxor;
using Start.Client.Store.State;
using Start.Shared;
namespace Start.Client.Store.Features.ContainersList {
public static class ContainerListReducers {
[ReducerMethod(typeof(FetchContainerListAction))]
public static RootState OnFetchContainerList(RootState state) {
return state with {
ContainerListState = state.ContainerListState with {
Containers = ImmutableList<BookmarkContainerDto>.Empty,
IsLoadingContainersList = true
}
};
}
[ReducerMethod]
public static RootState OnRecievedContainerList(RootState state,
RecievedContainerListAction action) {
return state with {
ContainerListState = state.ContainerListState with {
Containers = action.Containers.ToImmutableList(),
IsLoadingContainersList = false,
ErrorMessage = null
}
};
}
[ReducerMethod]
public static RootState OnErrorFetchingContainerList(RootState state,
ErrorFetchingContainerListAction action) {
return state with {
ContainerListState = state.ContainerListState with {
ErrorMessage = action.ErrorMessage
}
};
}
[ReducerMethod]
public static RootState AddContainerToList(RootState state,
AddContainerToListAction action) {
return state with {
ContainerListState = state.ContainerListState with {
Containers = state.ContainerListState.Containers.Add(action.NewContainer)
}
};
}
[ReducerMethod]
public static RootState RemoveContainerFromList(RootState state,
RemoveContainerFromListAction action) {
return state with {
ContainerListState = state.ContainerListState with {
Containers = state.ContainerListState.Containers
.Where(c => c.BookmarkContainerId != action.ContainerIdToRemove)
.ToImmutableList()
}
};
}
}
}

View file

@ -0,0 +1,27 @@
using Start.Shared;
namespace Start.Client.Store.Features.CreateContainer {
public class ShowCreateContainerFormAction { }
public class HideCreateContainerFormAction { }
public class FetchCreateContainerAction { }
public class ReceivedCreateContainerAction { }
public class ErrorFetchingCreateContainerAction {
public string ErrorMessage { get; set; }
public ErrorFetchingCreateContainerAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
public class SubmitCreateContainerAction {
public BookmarkContainerDto NewContainer { get; set; }
public SubmitCreateContainerAction(BookmarkContainerDto container) {
this.NewContainer = container;
}
}
}

View file

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Refit;
using Start.Shared;
using Start.Shared.Api;
using Start.Client.Store.Features.CurrentContainer;
using Start.Client.Store.Features.ContainersList;
namespace Start.Client.Store.Features.CreateContainer {
public class CreateContainerEffects {
public IBookmarkContainersApi BookmarkContainersApi { get; set; }
public CreateContainerEffects(IBookmarkContainersApi bookmarkContainersApi) {
this.BookmarkContainersApi = bookmarkContainersApi;
}
[EffectMethod]
public async Task SubmitCreateContainer(SubmitCreateContainerAction action,
IDispatcher dispatch) {
dispatch.Dispatch(new FetchCreateContainerAction());
try {
ApiResponse<BookmarkContainerDto?> apiResponse = await this.BookmarkContainersApi
.CreateBookmarkContainer(action.NewContainer.Title);
BookmarkContainerDto? container = apiResponse.Content;
if (container == null)
dispatch.Dispatch(new ErrorFetchingCreateContainerAction(
"Failed to create container"));
else {
dispatch.Dispatch(new AddContainerToListAction(container));
dispatch.Dispatch(new ReceivedCreateContainerAction());
dispatch.Dispatch(new LoadCurrentContainerAction(
container.BookmarkContainerId));
}
} catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
}
}

View file

@ -0,0 +1,12 @@
using Fluxor;
using Start.Client.Store.State;
namespace Start.Client.Store.Features.CreateContainer {
public class CreateContainerFeature : Feature<CreateContainerState> {
public override string GetName() => "Create Container";
protected override CreateContainerState GetInitialState() {
return new CreateContainerState();
}
}
}

View file

@ -0,0 +1,44 @@
using Fluxor;
namespace Start.Client.Store.Features.CreateContainer {
public static class CreateContainerReducers {
[ReducerMethod(typeof(ShowCreateContainerFormAction))]
public static CreateContainerState ShowCreateContainerForm(CreateContainerState state) {
return state with {
ShowCreateContainerForm = true
};
}
[ReducerMethod(typeof(HideCreateContainerFormAction))]
public static CreateContainerState HideCreateContainerForm(CreateContainerState state) {
return state with {
ShowCreateContainerForm = false
};
}
[ReducerMethod(typeof(FetchCreateContainerAction))]
public static CreateContainerState FetchCreateContainer(CreateContainerState state) {
return state with {
IsLoadingCreateContainer = true
};
}
[ReducerMethod(typeof(ReceivedCreateContainerAction))]
public static CreateContainerState ReceivedCreateContainer(CreateContainerState state) {
return state with {
IsLoadingCreateContainer = false,
CreateContainerErrorMessage = null,
ShowCreateContainerForm = false
};
}
[ReducerMethod]
public static CreateContainerState ErrorFetchingCreateContainer(CreateContainerState state,
ErrorFetchingCreateContainerAction action) {
return state with {
IsLoadingCreateContainer = false,
CreateContainerErrorMessage = action.ErrorMessage
};
}
}
}

View file

@ -0,0 +1,20 @@
using Start.Client.Store.State;
namespace Start.Client.Store.Features.CreateContainer {
public record CreateContainerState : RootState {
public bool ShowCreateContainerForm { get; init; }
public bool IsLoadingCreateContainer { get; init; }
public string? CreateContainerErrorMessage { get; init; }
public CreateContainerState() { }
public CreateContainerState(ContainerListState containerList,
CurrentContainerState currentContainer, bool showCreateContainer, bool isLoading,
string? errorMessage)
: base(containerList, currentContainer) {
this.ShowCreateContainerForm = showCreateContainer;
this.IsLoadingCreateContainer = isLoading;
this.CreateContainerErrorMessage = errorMessage;
}
}
}

View file

@ -0,0 +1,31 @@
using Start.Shared;
namespace Start.Client.Store.Features.CurrentContainer {
public class FetchCurrentContainerAction { }
public class ReceivedCurrentContainerAction {
public BookmarkContainerDto BookmarkContainer { get; init; }
public ReceivedCurrentContainerAction(BookmarkContainerDto bookmarkContainer) {
this.BookmarkContainer = bookmarkContainer;
}
}
public class ErrorFetchingCurrentContainerAction {
public string ErrorMessage { get; init; }
public ErrorFetchingCurrentContainerAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
public class LoadCurrentContainerAction {
public int BookmarkContainerId { get; init; }
public LoadCurrentContainerAction(int bookmarkContainerId) {
this.BookmarkContainerId = bookmarkContainerId;
}
}
public class FixCurrentContainerAction { }
}

View file

@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Blazored.LocalStorage;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Refit;
using Start.Client.Store.State;
using Start.Client.Store.Features.CreateContainer;
using Start.Shared;
using Start.Shared.Api;
namespace Start.Client.Store.Features.CurrentContainer {
public class CurrentContainerEffects {
public IBookmarkContainersApi BookmarkContainersApi { get; set; }
public ILocalStorageService LocalStorage { get; set; }
public IState<RootState> RootState { get; set; }
public CurrentContainerEffects(IBookmarkContainersApi bookmarkContainersApi,
ILocalStorageService localStorage, IState<RootState> rootState) {
this.BookmarkContainersApi = bookmarkContainersApi;
this.LocalStorage = localStorage;
this.RootState = rootState;
}
[EffectMethod]
public async Task LoadCurrentContainer(LoadCurrentContainerAction action,
IDispatcher dispatch) {
dispatch.Dispatch(new FetchCurrentContainerAction());
try {
ApiResponse<BookmarkContainerDto?> response = await this.BookmarkContainersApi
.GetBookmarkContainer(action.BookmarkContainerId);
BookmarkContainerDto? container = response.Content;
if (container == null) {
dispatch.Dispatch(new ErrorFetchingCurrentContainerAction(
"Failed to get current bookmark container"));
return;
}
dispatch.Dispatch(new ReceivedCurrentContainerAction(container));
await this.LocalStorage
.SetItemAsync("SelectedContainer", action.BookmarkContainerId);
} catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
[EffectMethod(typeof(FixCurrentContainerAction))]
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async Task FixCurrentContainer(IDispatcher dispatch) {
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
if (!this.RootState.Value.ContainerListState.Containers.Any()) {
dispatch.Dispatch(new SubmitCreateContainerAction(
new BookmarkContainerDto("Default")));
return;
}
IEnumerable<int?> containerIds = this.RootState.Value.ContainerListState.Containers
.Select(c => (int?)c.BookmarkContainerId);
int? currentContainerId = this.RootState.Value.CurrentContainerState.Container
?.BookmarkContainerId;
if (containerIds.Contains(currentContainerId))
return;
int firstContainerId = this.RootState.Value.ContainerListState.Containers
.First().BookmarkContainerId;
dispatch.Dispatch(new LoadCurrentContainerAction(firstContainerId));
}
}
}

View file

@ -0,0 +1,41 @@
using Fluxor;
using Start.Client.Store.State;
namespace Start.Client.Store.Features.CurrentContainer {
public static class CurrentContainerReducers {
[ReducerMethod(typeof(FetchCurrentContainerAction))]
public static RootState FetchCurrentContainer(RootState state) {
return state with {
CurrentContainerState = state.CurrentContainerState with {
Container = null,
IsLoadingCurrentContainer = true,
ErrorMessage = null
}
};
}
[ReducerMethod]
public static RootState ReceivedCurrentContainer(RootState state,
ReceivedCurrentContainerAction action) {
return state with {
CurrentContainerState = state.CurrentContainerState with {
Container = action.BookmarkContainer,
IsLoadingCurrentContainer = false,
ErrorMessage = null
}
};
}
[ReducerMethod]
public static RootState ErrorFetchingCurrentContainer(RootState state,
ErrorFetchingCurrentContainerAction action) {
return state with {
CurrentContainerState = state.CurrentContainerState with {
Container = null,
IsLoadingCurrentContainer = false,
ErrorMessage = action.ErrorMessage
}
};
}
}
}

View file

@ -0,0 +1,34 @@
namespace Start.Client.Store.Features.DeleteContainer {
public class ShowDeleteContainerFormAction {
public int ContainerIdToDelete { get; init; }
public string ContainerTitleToDelete { get; init; }
public ShowDeleteContainerFormAction(int containerIdToDelete,
string containerTitleToDelete) {
this.ContainerIdToDelete = containerIdToDelete;
this.ContainerTitleToDelete = containerTitleToDelete;
}
}
public class HideDeleteContainerFormAction { }
public class FetchDeleteContainerFormAction { }
public class SubmitDeleteContainerAction {
public int ContainerIdToDelete { get; init; }
public SubmitDeleteContainerAction(int containerIdToDelete) {
this.ContainerIdToDelete = containerIdToDelete;
}
}
public class RecievedDeleteContainerAction { }
public class ErrorFetchingDeleteContainerAction {
public string ErrorMessage { get; init; }
public ErrorFetchingDeleteContainerAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
}

View file

@ -0,0 +1,56 @@
using System.Threading.Tasks;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Start.Client.Store.Features.CurrentContainer;
using Start.Shared.Api;
using System.Net;
using Start.Client.Store.State;
using Start.Client.Store.Features.ContainersList;
namespace Start.Client.Store.Features.DeleteContainer {
public class DeleteContainerEffects {
public IBookmarkContainersApi BookmarkContainersApi { get; init; }
public IState<RootState> RootState { get; set; }
public DeleteContainerEffects(IBookmarkContainersApi bookmarkContainersApi,
IState<RootState> rootState) {
this.BookmarkContainersApi = bookmarkContainersApi;
this.RootState = rootState;
}
[EffectMethod]
public async Task SubmitDeleteContainer(SubmitDeleteContainerAction action,
IDispatcher dispatch) {
dispatch.Dispatch(new FetchDeleteContainerFormAction());
try {
System.Net.Http.HttpResponseMessage? apiResponse = await this.BookmarkContainersApi
.DeleteBookmarkContainer(action.ContainerIdToDelete);
if (apiResponse == null) {
dispatch.Dispatch(
new ErrorFetchingDeleteContainerAction("Failed to submit request"));
return;
}
if (apiResponse.StatusCode == HttpStatusCode.NotFound) {
dispatch.Dispatch(new ErrorFetchingDeleteContainerAction(
"The bookmark container to delete doesn't exist"));
return;
}
if (!apiResponse.IsSuccessStatusCode) {
dispatch.Dispatch(new ErrorFetchingDeleteContainerAction(
"There was an error deleting the bookmark container"));
return;
}
dispatch.Dispatch(new RemoveContainerFromListAction(action.ContainerIdToDelete));
dispatch.Dispatch(new FixCurrentContainerAction());
dispatch.Dispatch(new RecievedDeleteContainerAction());
} catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
}
}

View file

@ -0,0 +1,11 @@
using Fluxor;
namespace Start.Client.Store.Features.DeleteContainer {
public class DeleteContainerFeature : Feature<DeleteContainerState> {
public override string GetName() => "Delete Container";
protected override DeleteContainerState GetInitialState() {
return new DeleteContainerState();
}
}
}

View file

@ -0,0 +1,50 @@
using Fluxor;
namespace Start.Client.Store.Features.DeleteContainer {
public static class DeleteContainerReducers {
[ReducerMethod]
public static DeleteContainerState ShowDeleteContainerForm(DeleteContainerState state,
ShowDeleteContainerFormAction action) {
return state with {
ShowDeleteContainerForm = true,
BookmarkContainerIdToDelete = action.ContainerIdToDelete,
BookmarkContainerTitleToDelete = action.ContainerTitleToDelete,
DeleteContainerErrorMessage = null
};
}
[ReducerMethod(typeof(HideDeleteContainerFormAction))]
public static DeleteContainerState HideDeleteContainerForm(DeleteContainerState state) {
return state with {
ShowDeleteContainerForm = false,
DeleteContainerErrorMessage = null
};
}
[ReducerMethod(typeof(FetchDeleteContainerFormAction))]
public static DeleteContainerState FetchDeleteContainerForm(DeleteContainerState state) {
return state with {
IsLoadingDeleteContainer = true,
DeleteContainerErrorMessage = null
};
}
[ReducerMethod(typeof(RecievedDeleteContainerAction))]
public static DeleteContainerState ReceivedDeleteContainer(DeleteContainerState state) {
return state with {
IsLoadingDeleteContainer = false,
DeleteContainerErrorMessage = null,
ShowDeleteContainerForm = false
};
}
[ReducerMethod]
public static DeleteContainerState ErrorFetchingDeleteContainer(DeleteContainerState state,
ErrorFetchingDeleteContainerAction action) {
return state with {
DeleteContainerErrorMessage = action.ErrorMessage,
IsLoadingDeleteContainer = false
};
}
}
}

View file

@ -0,0 +1,28 @@
using Start.Client.Store.State;
namespace Start.Client.Store.Features.DeleteContainer {
public record DeleteContainerState : RootState {
public bool ShowDeleteContainerForm { get; set; }
public int BookmarkContainerIdToDelete { get; set; }
public string BookmarkContainerTitleToDelete { get; set; }
public bool IsLoadingDeleteContainer { get; set; }
public string? DeleteContainerErrorMessage { get; set; }
public DeleteContainerState() {
this.BookmarkContainerIdToDelete = 0;
this.BookmarkContainerTitleToDelete = "";
}
public DeleteContainerState(ContainerListState containerList,
CurrentContainerState currentContainer, bool showDeleteContainerForm,
int containerIdToDelete, string containerTitleToDelete, bool isLoadingDeleteContainer,
string? errorMessage)
: base(containerList, currentContainer) {
this.ShowDeleteContainerForm = showDeleteContainerForm;
this.BookmarkContainerIdToDelete = containerIdToDelete;
this.BookmarkContainerTitleToDelete = containerTitleToDelete;
this.IsLoadingDeleteContainer = isLoadingDeleteContainer;
this.DeleteContainerErrorMessage = errorMessage;
}
}
}

View file

@ -0,0 +1,59 @@
using System.Collections.Immutable;
using Fluxor;
using Start.Shared;
namespace Start.Client.Store.State {
[FeatureState]
public record RootState {
public ContainerListState ContainerListState { get; init; }
public CurrentContainerState CurrentContainerState { get; init; }
public RootState() {
this.ContainerListState = new ContainerListState();
this.CurrentContainerState = new CurrentContainerState();
}
public RootState(ContainerListState containerList, CurrentContainerState currentContainer) {
this.ContainerListState = containerList;
this.CurrentContainerState = currentContainer;
}
}
public record ContainerListState {
public ImmutableList<BookmarkContainerDto> Containers { get; init; }
public bool IsLoadingContainersList { get; init; }
public string? ErrorMessage { get; init; }
public ContainerListState() {
this.Containers = ImmutableList<BookmarkContainerDto>.Empty;
this.IsLoadingContainersList = false;
this.ErrorMessage = null;
}
public ContainerListState(ImmutableList<BookmarkContainerDto> containers,
bool isLoadingContainersList, string? errorMessage) {
this.Containers = containers;
this.IsLoadingContainersList = isLoadingContainersList;
this.ErrorMessage = errorMessage;
}
}
public record CurrentContainerState {
public BookmarkContainerDto? Container { get; init; }
public bool IsLoadingCurrentContainer { get; init; }
public string? ErrorMessage { get; init; }
public CurrentContainerState() {
this.Container = null;
this.IsLoadingCurrentContainer = false;
this.ErrorMessage = null;
}
public CurrentContainerState(BookmarkContainerDto? currentContainer,
bool isLoadingCurrentContainer, string? errorMessage) {
this.Container = currentContainer;
this.IsLoadingCurrentContainer = isLoadingCurrentContainer;
this.ErrorMessage = errorMessage;
}
}
}

View file

@ -9,3 +9,4 @@
@using Microsoft.JSInterop
@using Start.Client
@using Start.Client.Shared
@using Start.Shared;

View file

@ -6,11 +6,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Start</title>
<base href="/" />
<script src="_content/Fluxor.Blazor.Web/scripts/index.js"></script>
<!--<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />-->
<link href="css/spectre/spectre.min.css" rel="stylesheet" />
<link href="css/spectre/spectre-icons.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="Start.Client.styles.css" rel="stylesheet" />
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
</head>