Skip to content

Commit

Permalink
Adds Product Manager (#20)
Browse files Browse the repository at this point in the history
* Auto generated API changes

* Initial commit

* Bump MudBlazor to 6.11.0

* Convert ProductDto to ProductResponse

* Add initial ProductManager page

* Get products from API

* Add and edit products

* Global snackbar config

* Styling and product user groups

* Auto-generated updated API

* Fix updating and creating products with user groups

* Implements user groups in both get and update products now with a single select dropdown

* Utilize new products/all endpoint for non-visible products

* Change API specs

* List of checkboxes instead of single select dropdown

* Remove User parameter in component

* Documentation of the ProductService

* Styling

* Set visibility on edit and add item

* Hide non-visible products switch

* lots of things, sorry   ○( ^皿^)っ Hehehe

* Remove obsolete code

* Fix horizontal scrolling for MudDataGrid

Removed sorting by columns to shorten column size.
Removed filtering by hidden/unhidden elements.
Removed trailing whitespace.
Made the table more dense.
Eye icon is no longer brown.

* change to LF

* Add vscode settings to gitignore

* Use color palette for product manager

* Reintroduce success color on palette

---------

Co-authored-by: Andreas Trøstrup <[email protected]>
Co-authored-by: Omid Marfavi <[email protected]>
  • Loading branch information
3 people authored Jan 18, 2024
1 parent 9043193 commit e4ab67e
Show file tree
Hide file tree
Showing 18 changed files with 2,203 additions and 683 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ bld/

# Visual Studio 2015 cache/options directory
.vs/
.vscode/

# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

Expand Down
5 changes: 0 additions & 5 deletions .vscode/settings.json

This file was deleted.

400 changes: 128 additions & 272 deletions Shifty.Api/Generated/AnalogCoreV1/AnalogCoreV1.cs

Large diffs are not rendered by default.

1,006 changes: 938 additions & 68 deletions Shifty.Api/Generated/AnalogCoreV2/AnalogCoreV2.cs

Large diffs are not rendered by default.

262 changes: 262 additions & 0 deletions Shifty.App/Components/ProductManager.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
@namespace Components
@using System.ComponentModel.DataAnnotations
@using Shifty.App.Services
@using Shifty.Api.Generated.AnalogCoreV1
@using Shifty.Api.Generated.AnalogCoreV2
@using Shared
@using LanguageExt.UnsafeValueAccess
@using Components
@inject ISnackbar Snackbar
@inject IProductService ProductService

<style>
.mud-table-cell {
padding: 0.5em 0.75em !important;
}
</style>
<MudPaper Elevation="15" Style="margin: 2em; border-radius: 5px;">
@if (_loading)
{
<MudContainer Style="width: 100%; display: flex;">
<LoadingIndicator Height="400px" />
</MudContainer>
}
<MudDataGrid
@ref="_dataGrid"
T="ProductResponse"
Items="@_products"
EditMode="DataGridEditMode.Form"
ReadOnly="false"
StartedEditingItem="@StartedEditingItem"
CanceledEditingItem="@CanceledEditingItem"
CommittedItemChanges="@CommittedItemChanges"
EditTrigger="DataGridEditTrigger.Manual"
QuickFilter="e => _showNonvisible || e.Visible"
RowStyleFunc="@_RowStyleFunc"
FixedHeader="true"
Height="calc(100vh - 250px)"
Dense="true"
SortMode="MudBlazor.SortMode.None">
<Columns>
<TemplateColumn Title="Edit">
<CellTemplate>
<MudIconButton
Size="@Size.Medium"
Icon="@Icons.Material.Outlined.Edit"
Color="Color.Primary"
OnClick="@context.Actions.StartEditingItemAsync" />
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Visible" Title="" InitialDirection="SortDirection.Descending">
<CellTemplate>
@{
if (context.Item.Visible)
{
<MudIcon Size="@Size.Small" Color="Color.Dark"
Icon="@Icons.Material.Filled.Visibility" />
}
else
{
<MudIcon Size="@Size.Small" Style="@($"color:{Colors.Grey.Default};")"
Icon="@Icons.Material.Filled.VisibilityOff" />
}
}
</CellTemplate>
<EditTemplate>
<MudSwitch Label="Visible" Color="Color.Primary" @bind-Checked="context.Item.Visible" />
</EditTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Id" Title="Id" IsEditable="false" />
<PropertyColumn Property="x => x.Name" Title="Name" IsEditable="true" />
<PropertyColumn Property="x => x.NumberOfTickets" Title="Tickets" IsEditable="true" />
<PropertyColumn Property="x => x.Price" Title="Price" IsEditable="true" />
<PropertyColumn Property='x => string.Join(", ", x.AllowedUserGroups.Select(e => e.ToString()))'
Title="User groups" IsEditable="true">
<EditTemplate>
<MudText Typo="Typo.subtitle1">Visible to:</MudText>
@foreach (var group in Enum.GetValues<UserGroup>())
{
<MudCheckBox Dense="true" Color="Color.Primary" @bind-Checked="UserGroupDict[group]" Value="@group">
@group</MudCheckBox>
}
</EditTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsPerk" Title="Perk?" IsEditable="false">
<CellTemplate>
@{
if (context.Item.IsPerk)
{
<MudIcon Style="color:gold;" Icon="@Icons.Material.Filled.Star" Title="IsPerk" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.StarOutline" Title="IsPerk" />
}
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Description" Title="Description" IsEditable="true" />
</Columns>
</MudDataGrid>

<MudToolBar>
<MudSpacer />
<MudButton
Color="Color.Primary"
Variant="Variant.Filled"
EndIcon="@Icons.Material.Outlined.Add"
OnClick="@AddItemToDataGrid">
Add Product
</MudButton>
</MudToolBar>
</MudPaper>
@code
{
private MudDataGrid<ProductResponse> _dataGrid;
private IEnumerable<ProductResponse> _products = new List<ProductResponse>();
private Dictionary<UserGroup, bool> UserGroupDict = new();

private void StartedEditingItem(ProductResponse item)
{
UserGroupDict = new();
foreach (var group in Enum.GetValues<UserGroup>())
{
UserGroupDict.Add(group, item.AllowedUserGroups.Contains(group));
}
}

private bool _loading = true;
private bool _showNonvisible = true;
protected override async Task OnInitializedAsync()
{
var result = await ProductService.GetProducts();
_loading = false;
result.Match(
Succ: products => {
_products = products;
},
Fail: error => {
Snackbar.Add(error.Message, Severity.Error);
}
);
}

void CanceledEditingItem(ProductResponse item)
{
Snackbar.Add("Cancelled product changes", Severity.Info);
}

async Task CommittedItemChanges(ProductResponse item)
{
List<UserGroup> AllowedUserGroups =
UserGroupDict.Where(e => e.Value)
.ToDictionary(kv => kv.Key, kv => kv.Value)
.Keys
.ToList();

if (item.Id == 0)
{
var result = await addProduct(item, AllowedUserGroups);

result.Match(
Succ: async product =>
{
// Succesfully added product
Snackbar.Add("Product added", Severity.Success);

// Retrieve all items again in order to update id
var retrieveItems = await ProductService.GetProducts();
retrieveItems.Match(
Succ: items => {
_products = items;
},
Fail: error => {
// Errors while re-retrieving items, non-fatal (just means _products are slightly outdated)
Snackbar.Add(error.Message, Severity.Warning);
}
);
},
Fail: error => {
Snackbar.Add(error.Message, Severity.Error);
}
);
}
else
{
var result = await updateProduct(item, AllowedUserGroups);

result.Match(
Succ: async result =>
{
Snackbar.Add("Product updated", Severity.Success);

var retrieveItems = await ProductService.GetProducts();
retrieveItems.Match(
Succ: items => {
_products = items;
},
Fail: error => {
// Errors while re-retrieving items, non-fatal (just means _products are slightly outdated)
Snackbar.Add(error.Message, Severity.Warning);
}
);
},
Fail: error =>
{
Snackbar.Add(error.Message, Severity.Warning);
}
);
}
}

async Task<LanguageExt.Try<ChangedProductResponse>> updateProduct(ProductResponse item, List<UserGroup>
AllowedUserGroups)
{
return await ProductService.UpdateProduct(new UpdateProductRequest{
Id = item.Id,
Name = item.Name,
Description = item.Description,
NumberOfTickets = item.NumberOfTickets,
Price = item.Price,
Visible = item.Visible,
AllowedUserGroups = AllowedUserGroups
});
}

async Task<LanguageExt.Try<ChangedProductResponse>> addProduct(ProductResponse item, List<UserGroup>
AllowedUserGroups)
{
return await ProductService.AddProduct(new AddProductRequest{
Name = item.Name,
Description = item.Description,
NumberOfTickets = item.NumberOfTickets,
Price = item.Price,
Visible = item.Visible,
AllowedUserGroups = AllowedUserGroups
});
}

void AddItemToDataGrid()
{
_products = _products.Append<ProductResponse>(new ProductResponse{
Id = 0,
Name = "",
Description = "",
IsPerk = false,
NumberOfTickets = 1,
Price = 0,
Visible = true,
AllowedUserGroups = new List<UserGroup>()
});
_dataGrid.SetEditingItemAsync(_products.Last());
}

private Func<ProductResponse, int, string> _RowStyleFunc => (x, i) =>
{
if (!x.Visible)
{
return "background-color:#ebebeb;font-style:italic;font-color:#8a8686;";
}
return "";
};
}
20 changes: 10 additions & 10 deletions Shifty.App/Components/Voucher.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@using Shifty.Api.Generated.AnalogCoreV2
@using Shared
@using LanguageExt.UnsafeValueAccess
@using Shifty.App.Repositories
@inject IProductService _productService
@inject IVoucherService _voucherService
@inject ISnackbar Snackbar
Expand All @@ -15,7 +16,7 @@
<MudCardContent>
<MudText Align="Align.Center" Class="mb-n4">Issue Voucher Form</MudText>
<MudForm @bind-IsValid="@_isFormValid" >
<MudAutocomplete T="ProductDto"
<MudAutocomplete T="ProductResponse"
Required="true"
RequiredError="Product is required"
Placeholder="Select product"
Expand All @@ -25,7 +26,7 @@
Label="Product"
SearchFunc="@Products"
ToStringFunc="@_converter"
@bind-Value=_voucherForm.Product/>
@bind-Value="_voucherForm.Product"/>

<MudNumericField @bind-Value="_voucherForm.Amount"
Placeholder="1"
Expand All @@ -51,7 +52,7 @@
<MudTextField T="string"
@bind-Value="_voucherForm.Prefix"
Label="Voucher prefix"
Validation="@(new Func<string,string>(prefixValidation))"
Validation="@(new Func<string,string>(_prefixValidation))"
Required="true"
Counter="3"
MaxLength="3"
Expand Down Expand Up @@ -99,7 +100,7 @@
private VoucherForm _voucherForm = new();
private bool _isFormValid = false;
private bool _showProgressBar = false;
private IEnumerable<ProductDto> _products = new List<ProductDto>();
private IEnumerable<ProductResponse> _products = new List<ProductResponse>();
private IEnumerable<IssueVoucherResponse> _vouchers;
private string _voucherCodes;
private MudTextField<string> _multilineReference;
Expand All @@ -108,15 +109,14 @@
{
[Required]
public string Description { get; set; }
public ProductDto Product { get; set; }
public ProductResponse Product { get; set; }
public int Amount { get; set; } = 1;
public string Requester { get; set; }
public string Prefix { get; set; }
}

protected override async Task OnInitializedAsync()
{
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight;
var result = await _productService.GetProducts();

result.Match(
Expand All @@ -130,7 +130,7 @@
);
}

private async Task<IEnumerable<ProductDto>> Products(string value)
private async Task<IEnumerable<ProductResponse>> Products(string value)

Check warning on line 133 in Shifty.App/Components/Voucher.razor

View workflow job for this annotation

GitHub Actions / dev-deploy / Build codebase / Build webapp / Build and test Webapp

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 133 in Shifty.App/Components/Voucher.razor

View workflow job for this annotation

GitHub Actions / dev-deploy / Build codebase / Build webapp / Build and test Webapp

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (string.IsNullOrEmpty(value))
return _products;
Expand Down Expand Up @@ -159,7 +159,7 @@
Succ: response => {
_showProgressBar = false;
_vouchers = response;
_voucherCodes = string.Join("\n", _vouchers.Select(response => response.VoucherCode));
_voucherCodes = string.Join("\n", _vouchers.Select(issueVoucherResponse => issueVoucherResponse.VoucherCode));
},
Fail: ex => {
_showProgressBar = false;
Expand All @@ -168,6 +168,6 @@
);
}

Func<ProductDto,string> _converter = p => p != null ? $"{p.Name} - {p.NumberOfTickets} ticket" + (p.NumberOfTickets == 1 ? "" : "s") : "";
Func<string, string> prefixValidation = str => str.Length == 3 ? null : "Prefix must be 3 letters";
Func<ProductResponse,string> _converter = p => p != null ? $"{p.Name} - {p.NumberOfTickets} ticket" + (p.NumberOfTickets == 1 ? "" : "s") : "";
Func<string, string> _prefixValidation = str => str.Length == 3 ? null : "Prefix must be 3 letters";
}
24 changes: 24 additions & 0 deletions Shifty.App/Pages/ProductManagement.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@page "/Products"
@using Components
@inject NavigationManager NavManager

@if (_user is not null && _user.IsInRole("Board"))
{
<ProductManager/>
}

@code {
[CascadingParameter] public Task<AuthenticationState> AuthTask { get; set; }
private System.Security.Claims.ClaimsPrincipal _user;

protected override async Task OnInitializedAsync()
{
var authState = await AuthTask;
_user = authState.User;

if (_user is null || !_user.IsInRole("Board"))
{
NavManager.NavigateTo("/");
}
}
}
Loading

0 comments on commit e4ab67e

Please sign in to comment.