Skip to content

Commit

Permalink
App bar action pinning (#85)
Browse files Browse the repository at this point in the history
* Resolve CodeQL findings

* Add support for pinning "actions" to the app bar

* Add support for pinning items and item collections to the app bar

* Fix text wrapping in app bar

* Support for searching for item stores

* When searching, prioritise results that match the type being described (e.g. "halloween store" will show "store" results first)

* Add support for pinning item lists to the app bar

* App bar notifications "last viewed" timestamp is now stored in local storage instead of as a cookie
  • Loading branch information
rhyskoedijk authored Apr 28, 2024
1 parent a35514d commit fb29e97
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 53 deletions.
4 changes: 4 additions & 0 deletions SCMM.Web.Client/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Blazored.LocalStorage;
using Ljbc1994.Blazor.IntersectionObserver;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
Expand Down Expand Up @@ -75,12 +76,15 @@ public static void AddUIServices(this IServiceCollection services, string syncfu
services.AddScoped<AppState>();

services.AddScoped<ExternalNavigationManager>();
services.AddScoped<PinnedActionManager>();
services.AddScoped<DocumentManager>();

services.AddTransient<VirtualisedItemsMemoryCache>();

services.AddIntersectionObserver();

services.AddBlazoredLocalStorage();

services.AddMudServices(config =>
{
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.TopRight;
Expand Down
1 change: 1 addition & 0 deletions SCMM.Web.Client/SCMM.Web.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="BlazorIntersectionObserver" Version="3.1.0" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.4" />
Expand Down
110 changes: 82 additions & 28 deletions SCMM.Web.Client/Shared/Components/SearchBar.razor
Original file line number Diff line number Diff line change
@@ -1,36 +1,76 @@
@using SCMM.Web.Client.Shared.Dialogs.Items
@inherits PersistentComponent
@using SCMM.Web.Client.Shared.Dialogs.Items
@using SCMM.Web.Data.Models.UI.Item
@using SCMM.Web.Data.Models.UI.Search
@inject ILogger<SearchBar> Logger
@inject IDialogService Dialogs
@inject NavigationManager NavigationManager
@inject ExternalNavigationManager ExternalNavigationManager
@inject PinnedActionManager PinnedActionManager
@inject AppState State
@inject HttpClient Http

<MudAutocomplete T="SearchResultDTO" Value="@Search" SearchFunc="@SearchAsync" ValueChanged="@OnResultSelected" ToStringFunc="@(x => x?.Description)" ResetValueOnEmptyText="true"
Placeholder="Search for an item, type, or collection..." DebounceInterval="500" FullWidth="false" Dense="true" Disabled="State.IsPrerendering"
Variant="MudBlazor.Variant.Text" AdornmentIcon="fas fa-fw fa-search ml-2" IconSize="MudBlazor.Size.Small" Class="mud-input-no-frills ma-0 px-4" Style="max-width: 350px">
<ItemTemplate Context="item">
<div class="d-flex algin-center">
<img src="@item.IconUrl" class="mr-2" style="width:2em; height:2em" />
<MudText Class="my-1">@item.Description</MudText>
<MudChip Variant="MudBlazor.Variant.Text" Color="MudBlazor.Color.Secondary" Size="MudBlazor.Size.Small" Class="ml-2">@item.Type</MudChip>
</div>
</ItemTemplate>
<ItemSelectedTemplate Context="item">
<div class="d-flex algin-center">
<img src="@item.IconUrl" class="mr-2" style="width:2em; height:2em" />
<MudText Class="my-1">@item.Description</MudText>
<MudChip Variant="MudBlazor.Variant.Text" Color="MudBlazor.Color.Secondary" Size="MudBlazor.Size.Small" Class="ml-2">@item.Type</MudChip>
</div>
</ItemSelectedTemplate>
</MudAutocomplete>
<div class="px-4" style="min-width: 350px; max-width: 250px">
<MudAutocomplete T="SearchResultDTO" Value="@Search" SearchFunc="@SearchAsync" ValueChanged="@((x) => OnActionSelected(x.Type, x.Url, x.Description))" ToStringFunc="@(x => x?.Description)" ResetValueOnEmptyText="true"
Placeholder="Search the website for..." DebounceInterval="500" FullWidth="false" Dense="true" Disabled="State.IsPrerendering"
Variant="MudBlazor.Variant.Text" AdornmentIcon="fas fa-fw fa-search ml-2" IconSize="MudBlazor.Size.Small" Class="mud-input-no-frills ma-0">
<ItemTemplate Context="item">
<div class="d-flex algin-center">
<img src="@item.IconUrl" class="mr-2" style="width:2em; height:2em" />
<MudText Class="no-wrap my-1">@item.Description</MudText>
<MudChip Variant="MudBlazor.Variant.Text" Color="MudBlazor.Color.Secondary" Size="MudBlazor.Size.Small" Class="no-wrap ml-2">@item.Type</MudChip>
</div>
</ItemTemplate>
<ItemSelectedTemplate Context="item">
<div class="d-flex algin-center">
<img src="@item.IconUrl" class="mr-2" style="width:2em; height:2em" />
<MudText Class="no-wrap my-1">@item.Description</MudText>
<MudChip Variant="MudBlazor.Variant.Text" Color="MudBlazor.Color.Secondary" Size="MudBlazor.Size.Small" Class="no-wrap ml-2">@item.Type</MudChip>
</div>
</ItemSelectedTemplate>
</MudAutocomplete>
</div>
@if (PinnedActions?.Any() == true)
{
<MudContainer Class="d-flex align-center justify-start scroll-x scrollbar-hidden pa-0">
@foreach (var pinnedActions in PinnedActions)
{
<div class="d-flex algin-center hover-darken pa-2" onclick="@(() => OnActionSelected(pinnedActions.Type, pinnedActions.Url, pinnedActions.Description))">
<img src="@pinnedActions.IconUrl" class="mr-2" style="width:2em; height:2em" />
<MudText Class="no-wrap my-1">@pinnedActions.Description</MudText>
</div>
}
</MudContainer>
}

@code {

private SearchResultDTO Search;

private IEnumerable<PinnedActionManager.Action> PinnedActions { get; set; }

protected override void OnInitialized()
{
base.OnInitialized();
PinnedActionManager.PinnedActionsChanged += (actions) => {
PinnedActions = actions;
StateHasChanged();
};
}

protected override async Task OnLoadStateAsync()
{
PinnedActions = await RestoreFromStateOrLoad(nameof(PinnedActions),
() => PinnedActionManager.ListPinnedActions()
);
}

protected override Task OnPersistStateAsync()
{
PersistToState(nameof(PinnedActions), PinnedActions);
return Task.CompletedTask;
}

private async Task<IEnumerable<SearchResultDTO>> SearchAsync(string value)
{
try
Expand All @@ -51,17 +91,27 @@
}
}

private void OnResultSelected(SearchResultDTO result)
private async Task OnActionSelected(string type, string url, string description)
{
Search = null;
switch (result.Type)
switch (type)
{
case "List":
Dialogs.Show<ViewItemListDialog>(null, parameters: new DialogParameters()
{
["ListName"] = description,
["ListUrl"] = url,
["SortBy"] = nameof(ItemDescriptionWithPriceDTO.TimeAccepted),
["SortDirection"] = MudBlazor.SortDirection.Descending
});
break;

case "Type":
Dialogs.Show<ViewItemListDialog>(null, parameters: new DialogParameters()
{
["ListName"] = $"All {result.Description.Pluralise()}",
["ListUrl"] = result.Url,
["DemandUrl"] = $"api/item/type/{result.Description}/demand",
["ListName"] = $"All {description.Pluralise()}",
["ListUrl"] = url,
["DemandUrl"] = $"api/item/type/{description}/demand",
["SortBy"] = nameof(ItemDescriptionWithPriceDTO.TimeAccepted),
["SortDirection"] = MudBlazor.SortDirection.Descending
});
Expand All @@ -70,18 +120,22 @@
case "Collection":
Dialogs.Show<ViewItemCollectionDialog>(null, parameters: new DialogParameters()
{
["CollectionName"] = $"{result.Description} Collection",
["CollectionUrl"] = result.Url
["CollectionName"] = $"{description} Collection",
["CollectionUrl"] = url
});
break;

case "Item":
Dialogs.Show<ViewItemDetailsDialog>(null, parameters: new DialogParameters()
{
["ItemName"] = result.Description,
["ItemUrl"] = result.Url
["ItemName"] = description,
["ItemUrl"] = url
});
break;

case "Store":
NavigationManager.NavigateTo(url);
break;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@inherits ResponsiveDialog
@inject ILogger<ViewItemCollectionDialog> Logger
@inject ISnackbar Snackbar
@inject PinnedActionManager PinnedActionManager
@inject HttpClient Http
@inject AppState State

Expand All @@ -14,7 +15,7 @@
}
else
{
<div class="d-flex mr-4">
<div class="d-flex align-start mr-4">
@if (!String.IsNullOrEmpty(Collection.CreatorAvatarUrl))
{
<MudTooltip Text="@Collection.CreatorName">
Expand All @@ -36,6 +37,14 @@
}
</div>
<MudSpacer />
@if (Collection != null && PinnedAction != null)
{
<MudToggleIconButton Toggled="IsActionPinned" ToggledChanged="TogglePinnedAction"
Title="Bookmark this item collection" ToggledTitle="Remove this bookmark"
Icon="far fa-bookmark" ToggledIcon="fas fa-bookmark"
Size="MudBlazor.Size.Small" ToggledSize="MudBlazor.Size.Small"
Class="ma-1 ml-4 mr-0" DisableRipple="true" Disabled="@State.IsPrerendering" />
}
<MudMenu AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft"
Dense="true" Class="hover-darken align-self-start mx-4 " title="Change sort order">
<ActivatorContent>
Expand Down Expand Up @@ -126,6 +135,10 @@

private ItemCollectionDTO Collection { get; set; }

private PinnedActionManager.Action PinnedAction { get; set; }

private bool IsActionPinned { get; set; }

protected override void OnConfigure(ResponsiveDialogOptions options)
{
options.FullscreenBreakpoint = MudBlazor.Breakpoint.Sm;
Expand All @@ -141,6 +154,16 @@
try
{
Collection = await Http.GetFromJsonWithDefaultsAsync<ItemCollectionDTO>(CollectionUrl);
PinnedAction = new PinnedActionManager.Action()
{
Type = "Collection",
Url = CollectionUrl,
IconUrl = Collection.AcceptedItems.FirstOrDefault()?.IconUrl ?? Collection.CreatorAvatarUrl,
Description = Collection.Name,
};

IsActionPinned = await PinnedActionManager.IsPinned(PinnedAction);

Dialog.StateHasChanged();
}
catch (Exception ex)
Expand All @@ -166,4 +189,17 @@

return items;
}

private async Task TogglePinnedAction(bool pinned)
{
IsActionPinned = !pinned;
if (pinned)
{
await PinnedActionManager.PinAction(PinnedAction);
}
else
{
await PinnedActionManager.UnpinAction(PinnedAction);
}
}
}
43 changes: 41 additions & 2 deletions SCMM.Web.Client/Shared/Dialogs/Items/ViewItemDetailsDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@
@inherits ResponsiveDialog
@inject ILogger<ViewItemDetailsDialog> Logger
@inject ISnackbar Snackbar
@inject PinnedActionManager PinnedActionManager
@inject HttpClient Http
@inject AppState State

<MudDialog>
<TitleContent>
<MudText Typo="MudBlazor.Typo.h6">@ItemName Details</MudText>
<div class="d-flex align-start mr-4">
<MudText Typo="MudBlazor.Typo.h6">@ItemName Details</MudText>
<MudSpacer />
@if (Item != null && PinnedAction != null)
{
<MudToggleIconButton Toggled="IsActionPinned" ToggledChanged="TogglePinnedAction"
Title="Bookmark this item" ToggledTitle="Remove this bookmark"
Icon="far fa-bookmark" ToggledIcon="fas fa-bookmark"
Size="MudBlazor.Size.Small" ToggledSize="MudBlazor.Size.Small"
Class="ma-1 mx-4" DisableRipple="true" Disabled="@State.IsPrerendering" />
}
</div>
</TitleContent>
<DialogContent>
@if (Item == null)
Expand All @@ -33,7 +45,11 @@
public string ItemUrl { get; set; }

private ItemDetailedDTO Item { get; set; }


private PinnedActionManager.Action PinnedAction { get; set; }

private bool IsActionPinned { get; set; }

protected override void OnConfigure(ResponsiveDialogOptions options)
{
options.FullscreenBreakpoint = MudBlazor.Breakpoint.Md;
Expand All @@ -49,6 +65,16 @@
try
{
Item = await Http.GetFromJsonWithDefaultsAsync<ItemDetailedDTO>(ItemUrl);
PinnedAction = new PinnedActionManager.Action()
{
Type = "Item",
Url = ItemUrl,
IconUrl = Item?.IconUrl,
Description = ItemName,
};

IsActionPinned = await PinnedActionManager.IsPinned(PinnedAction);

Dialog.StateHasChanged();
}
catch (Exception ex)
Expand All @@ -58,4 +84,17 @@
}
}

private async Task TogglePinnedAction(bool pinned)
{
IsActionPinned = !pinned;
if (pinned)
{
await PinnedActionManager.PinAction(PinnedAction);
}
else
{
await PinnedActionManager.UnpinAction(PinnedAction);
}
}

}
Loading

0 comments on commit fb29e97

Please sign in to comment.