diff --git a/Start/Client/App.razor b/Start/Client/App.razor index 80f6b85..d30c0d9 100644 --- a/Start/Client/App.razor +++ b/Start/Client/App.razor @@ -1,3 +1,5 @@ + + diff --git a/Start/Client/Components/BookmarkContainer.razor b/Start/Client/Components/BookmarkContainer.razor index 7fbb2ef..35aafe4 100644 --- a/Start/Client/Components/BookmarkContainer.razor +++ b/Start/Client/Components/BookmarkContainer.razor @@ -1,6 +1,4 @@ -@using Start.Shared - -
+
@if (this.Container == null) {
diff --git a/Start/Client/Components/BookmarkGroup.razor b/Start/Client/Components/BookmarkGroup.razor index f231941..64a7381 100644 --- a/Start/Client/Components/BookmarkGroup.razor +++ b/Start/Client/Components/BookmarkGroup.razor @@ -1,6 +1,4 @@ -@using Start.Shared - -

+

@this.Group.Title

    diff --git a/Start/Client/Components/CreateContainer.razor b/Start/Client/Components/CreateContainer.razor index 4a7da0e..bf013f6 100644 --- a/Start/Client/Components/CreateContainer.razor +++ b/Start/Client/Components/CreateContainer.razor @@ -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 - - - @if (displayError) +@inject IDispatcher dispatch +@inject IState state + + + + @if (this.state.Value.CreateContainerErrorMessage != null) { - There was an error creating the container + @this.state.Value.CreateContainerErrorMessage }
    @@ -19,7 +21,7 @@
    + class="form-input" @bind-Value="this.Model.Title" />
@@ -28,9 +30,18 @@
- + @if (this.state.Value.IsLoadingCreateContainer) + { + + } + else + { + + }
@@ -42,34 +53,16 @@ @code { [Parameter] public EventCallback 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 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()); } } diff --git a/Start/Client/Components/DeleteContainer.razor b/Start/Client/Components/DeleteContainer.razor index 6e94d27..5a5c43c 100644 --- a/Start/Client/Components/DeleteContainer.razor +++ b/Start/Client/Components/DeleteContainer.razor @@ -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 state - - @if (this.ShowAlert) +@{ string title = $"Delete Container \"{this.state.Value.BookmarkContainerTitleToDelete}\""; } + + + @if (this.state.Value.DeleteContainerErrorMessage != null) { -
- There was an error deleting the bookmark container -
+ + @this.state.Value.DeleteContainerErrorMessage + } -

Are you sure you want to delete the bookmark container "@this.ContainerTitle"?

+ +

+ Are you sure you want to delete the bookmark container + "@this.state.Value.BookmarkContainerTitleToDelete"? +

- - + @if (!this.state.Value.IsLoadingDeleteContainer) + { + + + } + else + { + + + }
@code { - [Parameter] - public int BookmarkContainerId { get; set; } - [Parameter] - public string ContainerTitle { get; set; } = null!; - [Parameter] - public bool Active { get; set; } - [Parameter] - public EventCallback 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)); } } diff --git a/Start/Client/Pages/Index.razor b/Start/Client/Pages/Index.razor index 6f6c51e..a1804ec 100644 --- a/Start/Client/Pages/Index.razor +++ b/Start/Client/Pages/Index.razor @@ -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 state +@inject IDispatcher dispatch + +@if (this.state.Value.ContainerListState.ErrorMessage != null) { + + Error @this.state.Value.ContainerListState.ErrorMessage + +} + +@if (this.state.Value.ContainerListState.IsLoadingContainersList) {
@@ -24,10 +36,10 @@ else {
    - @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";
  • @@ -47,132 +59,41 @@ else
- + - - + + } @code { - private IList? 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> 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 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 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("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; } diff --git a/Start/Client/Program.cs b/Start/Client/Program.cs index 5769dff..c80353b 100644 --- a/Start/Client/Program.cs +++ b/Start/Client/Program.cs @@ -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(); } } diff --git a/Start/Client/Start.Client.csproj b/Start/Client/Start.Client.csproj index d269baa..e8c81b8 100644 --- a/Start/Client/Start.Client.csproj +++ b/Start/Client/Start.Client.csproj @@ -19,6 +19,9 @@ + + + @@ -33,8 +36,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Start/Client/Store/Features/ContainersList/ContainerListActions.cs b/Start/Client/Store/Features/ContainersList/ContainerListActions.cs new file mode 100644 index 0000000..7d74cfb --- /dev/null +++ b/Start/Client/Store/Features/ContainersList/ContainerListActions.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Start.Shared; + +namespace Start.Client.Store.Features.ContainersList { + /// Dispatch before sending an API request + public class FetchContainerListAction { } + + /// Dispatch after recieving the container list from an API request + public class RecievedContainerListAction { + public IList Containers { get; set; } + + public RecievedContainerListAction(IList 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; + } + } +} diff --git a/Start/Client/Store/Features/ContainersList/ContainerListEffects.cs b/Start/Client/Store/Features/ContainersList/ContainerListEffects.cs new file mode 100644 index 0000000..a5712d0 --- /dev/null +++ b/Start/Client/Store/Features/ContainersList/ContainerListEffects.cs @@ -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> response = await this + .BookmarkContainersApi + .GetAllBookmarkContainers(); + + List? 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(); + } + } + } +} diff --git a/Start/Client/Store/Features/ContainersList/ContainerListReducers.cs b/Start/Client/Store/Features/ContainersList/ContainerListReducers.cs new file mode 100644 index 0000000..c5d8459 --- /dev/null +++ b/Start/Client/Store/Features/ContainersList/ContainerListReducers.cs @@ -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.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() + } + }; + } + } +} diff --git a/Start/Client/Store/Features/CreateContainer/CreateContainerActions.cs b/Start/Client/Store/Features/CreateContainer/CreateContainerActions.cs new file mode 100644 index 0000000..b54af20 --- /dev/null +++ b/Start/Client/Store/Features/CreateContainer/CreateContainerActions.cs @@ -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; + } + } +} diff --git a/Start/Client/Store/Features/CreateContainer/CreateContainerEffects.cs b/Start/Client/Store/Features/CreateContainer/CreateContainerEffects.cs new file mode 100644 index 0000000..4e9593f --- /dev/null +++ b/Start/Client/Store/Features/CreateContainer/CreateContainerEffects.cs @@ -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 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(); + } + } + } +} diff --git a/Start/Client/Store/Features/CreateContainer/CreateContainerFeature.cs b/Start/Client/Store/Features/CreateContainer/CreateContainerFeature.cs new file mode 100644 index 0000000..797ccb0 --- /dev/null +++ b/Start/Client/Store/Features/CreateContainer/CreateContainerFeature.cs @@ -0,0 +1,12 @@ +using Fluxor; +using Start.Client.Store.State; + +namespace Start.Client.Store.Features.CreateContainer { + public class CreateContainerFeature : Feature { + public override string GetName() => "Create Container"; + + protected override CreateContainerState GetInitialState() { + return new CreateContainerState(); + } + } +} diff --git a/Start/Client/Store/Features/CreateContainer/CreateContainerReducers.cs b/Start/Client/Store/Features/CreateContainer/CreateContainerReducers.cs new file mode 100644 index 0000000..63bc3d5 --- /dev/null +++ b/Start/Client/Store/Features/CreateContainer/CreateContainerReducers.cs @@ -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 + }; + } + } +} diff --git a/Start/Client/Store/Features/CreateContainer/CreateContainerState.cs b/Start/Client/Store/Features/CreateContainer/CreateContainerState.cs new file mode 100644 index 0000000..c556868 --- /dev/null +++ b/Start/Client/Store/Features/CreateContainer/CreateContainerState.cs @@ -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; + } + } +} diff --git a/Start/Client/Store/Features/CurrentContainer/CurrentContainerActions.cs b/Start/Client/Store/Features/CurrentContainer/CurrentContainerActions.cs new file mode 100644 index 0000000..89069f0 --- /dev/null +++ b/Start/Client/Store/Features/CurrentContainer/CurrentContainerActions.cs @@ -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 { } +} diff --git a/Start/Client/Store/Features/CurrentContainer/CurrentContainerEffects.cs b/Start/Client/Store/Features/CurrentContainer/CurrentContainerEffects.cs new file mode 100644 index 0000000..fa7afba --- /dev/null +++ b/Start/Client/Store/Features/CurrentContainer/CurrentContainerEffects.cs @@ -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 { get; set; } + + public CurrentContainerEffects(IBookmarkContainersApi bookmarkContainersApi, + ILocalStorageService localStorage, IState 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 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 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)); + } + } +} diff --git a/Start/Client/Store/Features/CurrentContainer/CurrentContainerReducers.cs b/Start/Client/Store/Features/CurrentContainer/CurrentContainerReducers.cs new file mode 100644 index 0000000..ac15570 --- /dev/null +++ b/Start/Client/Store/Features/CurrentContainer/CurrentContainerReducers.cs @@ -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 + } + }; + } + } +} diff --git a/Start/Client/Store/Features/DeleteContainer/DeleteContainerActions.cs b/Start/Client/Store/Features/DeleteContainer/DeleteContainerActions.cs new file mode 100644 index 0000000..d403733 --- /dev/null +++ b/Start/Client/Store/Features/DeleteContainer/DeleteContainerActions.cs @@ -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; + } + } +} diff --git a/Start/Client/Store/Features/DeleteContainer/DeleteContainerEffects.cs b/Start/Client/Store/Features/DeleteContainer/DeleteContainerEffects.cs new file mode 100644 index 0000000..83d56c7 --- /dev/null +++ b/Start/Client/Store/Features/DeleteContainer/DeleteContainerEffects.cs @@ -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 { get; set; } + + public DeleteContainerEffects(IBookmarkContainersApi bookmarkContainersApi, + IState 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(); + } + } + } +} diff --git a/Start/Client/Store/Features/DeleteContainer/DeleteContainerFeature.cs b/Start/Client/Store/Features/DeleteContainer/DeleteContainerFeature.cs new file mode 100644 index 0000000..d63183d --- /dev/null +++ b/Start/Client/Store/Features/DeleteContainer/DeleteContainerFeature.cs @@ -0,0 +1,11 @@ +using Fluxor; + +namespace Start.Client.Store.Features.DeleteContainer { + public class DeleteContainerFeature : Feature { + public override string GetName() => "Delete Container"; + + protected override DeleteContainerState GetInitialState() { + return new DeleteContainerState(); + } + } +} diff --git a/Start/Client/Store/Features/DeleteContainer/DeleteContainerReducers.cs b/Start/Client/Store/Features/DeleteContainer/DeleteContainerReducers.cs new file mode 100644 index 0000000..bf5db8b --- /dev/null +++ b/Start/Client/Store/Features/DeleteContainer/DeleteContainerReducers.cs @@ -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 + }; + } + } +} diff --git a/Start/Client/Store/Features/DeleteContainer/DeleteContainerState.cs b/Start/Client/Store/Features/DeleteContainer/DeleteContainerState.cs new file mode 100644 index 0000000..3ed1eff --- /dev/null +++ b/Start/Client/Store/Features/DeleteContainer/DeleteContainerState.cs @@ -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; + } + } +} diff --git a/Start/Client/Store/State/RootState.cs b/Start/Client/Store/State/RootState.cs new file mode 100644 index 0000000..ffce2fa --- /dev/null +++ b/Start/Client/Store/State/RootState.cs @@ -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 Containers { get; init; } + public bool IsLoadingContainersList { get; init; } + public string? ErrorMessage { get; init; } + + public ContainerListState() { + this.Containers = ImmutableList.Empty; + this.IsLoadingContainersList = false; + this.ErrorMessage = null; + } + + public ContainerListState(ImmutableList 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; + } + } +} diff --git a/Start/Client/_Imports.razor b/Start/Client/_Imports.razor index 3dc6864..1d6eb43 100644 --- a/Start/Client/_Imports.razor +++ b/Start/Client/_Imports.razor @@ -9,3 +9,4 @@ @using Microsoft.JSInterop @using Start.Client @using Start.Client.Shared +@using Start.Shared; diff --git a/Start/Client/wwwroot/index.html b/Start/Client/wwwroot/index.html index 1b7d496..3c4ee5d 100644 --- a/Start/Client/wwwroot/index.html +++ b/Start/Client/wwwroot/index.html @@ -6,11 +6,15 @@ Start + + + +