Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/DataModel/Properties/ModelResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -4046,7 +4046,7 @@
<value></value>
</data>
<data name="MonsterDefinition_TypeCaption" xml:space="preserve">
<value>Monsters</value>
<value>Monster</value>
</data>
<data name="MonsterDefinition_TypeCaptionPlural" xml:space="preserve">
<value>Monsters</value>
Expand Down
1 change: 1 addition & 0 deletions src/Web/AdminPanel/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<base href="/" />
<ResourcePreloader />
<link href="@Assets["MUnique.OpenMU.Startup.styles.css"]" rel="stylesheet" />
<link href="@Assets["_content/MUnique.OpenMU.Web.AdminPanel/MUnique.OpenMU.Web.AdminPanel.styles.css"]" rel="stylesheet" />
@foreach (var stylesheetSrc in Web.AdminPanel.Exports.Stylesheets)
{
<link href="@Assets[stylesheetSrc]" rel="stylesheet" />
Expand Down
72 changes: 72 additions & 0 deletions src/Web/AdminPanel/Components/Layout/CreationPanel.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@* <copyright file="CreationPanel.razor" company="MUnique">
Licensed under the MIT License. See LICENSE file in the project root for full license information.
</copyright> *@

@using MUnique.OpenMU.Web.AdminPanel.Properties
@using MUnique.OpenMU.Web.Shared.Components.Form
@using MUnique.OpenMU.Web.Shared.Services

@implements IDisposable
@inject CreationPanelService Panel
@inject Blazored.Toast.Services.IToastService ToastService

@if (this.Panel.Current is { } session)
{
<div class="creation-panel @(this.Panel.IsCollapsed ? "collapsed" : "expanded")">
<button type="button"
class="collapse-toggle"
title="@(this.Panel.IsCollapsed ? Resources.ShowEntryForm : Resources.HideEntryForm)"
@onclick="@this.Panel.ToggleCollapse">
<span class="oi @(this.Panel.IsCollapsed ? "oi-chevron-left" : "oi-chevron-right")" aria-hidden="true"></span>
</button>
@if (!this.Panel.IsCollapsed)
{
<div class="creation-panel-body">
<h3>@session.Title</h3>
<ItemCreationForm Item="@session.Item"
PersistenceContext="@session.Context"
Owner="@session.Owner"
OnValidSubmit="@this.OnSubmitAsync"
OnCancel="@this.OnCancelAsync" />
</div>
}
</div>
}

@code {

/// <inheritdoc />
protected override void OnInitialized()
{
base.OnInitialized();
this.Panel.StateChanged += this.OnStateChanged;
}

/// <inheritdoc />
public void Dispose()
{
this.Panel.StateChanged -= this.OnStateChanged;
}

private void OnStateChanged()
{
_ = this.InvokeAsync(this.StateHasChanged);
}

private async Task OnSubmitAsync()
{
try
{
await this.Panel.CompleteAsync();
}
catch (Exception ex)
{
this.ToastService.ShowError($"Could not save the new entry: {ex.Message}");
}
}

private Task OnCancelAsync()
{
return this.Panel.CancelAsync();
}
}
61 changes: 61 additions & 0 deletions src/Web/AdminPanel/Components/Layout/CreationPanel.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.creation-panel {
position: sticky;
top: 0;
align-self: flex-start;
height: 100vh;
flex-shrink: 0;
display: flex;
flex-direction: row;
background-color: #fff;
border-left: 1px solid #d6d5d5;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.08);
overflow: hidden;
z-index: 2;
}

.creation-panel.expanded {
width: clamp(32rem, 42vw, 60rem);
flex-basis: clamp(32rem, 42vw, 60rem);
}

.creation-panel.collapsed {
width: 1.75rem;
}

.collapse-toggle {
flex-shrink: 0;
width: 1.75rem;
padding: 0;
border: none;
border-right: 1px solid #d6d5d5;
background-color: #f7f7f7;
cursor: pointer;
color: #333;
}

.collapse-toggle:hover {
background-color: #e9e9e9;
}

.creation-panel-body {
flex: 1;
min-width: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 1rem 1.25rem;
}

.creation-panel-body h3 {
margin-top: 0;
margin-bottom: 1rem;
word-break: break-word;
}

/* Let the form controls fill the wider panel so the space is actually used. */
.creation-panel-body ::deep input:not([type="checkbox"]):not([type="radio"]),
.creation-panel-body ::deep select,
.creation-panel-body ::deep textarea,
.creation-panel-body ::deep .blazored-typeahead {
width: 100%;
box-sizing: border-box;
}
1 change: 1 addition & 0 deletions src/Web/AdminPanel/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
</article>

</main>
<CreationPanel />
</div>

<div id="blazor-error-ui" data-nosnippet>
Expand Down
1 change: 1 addition & 0 deletions src/Web/AdminPanel/Components/Layout/MainLayout.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

main {
flex: 1;
min-width: 0;
}

.sidebar {
Expand Down
147 changes: 98 additions & 49 deletions src/Web/AdminPanel/Pages/EditConfigGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
using System.ComponentModel;
using System.Reflection;
using System.Threading;
using Blazored.Modal;
using Blazored.Modal.Services;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.QuickGrid;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared;
using MUnique.OpenMU.Web.Shared.Components.Form.Modal;
using MUnique.OpenMU.Web.Shared.Services;

/// <summary>
/// Razor page which shows objects of the specified type in a grid.
Expand Down Expand Up @@ -62,6 +62,12 @@ public partial class EditConfigGrid : ComponentBase, IAsyncDisposable
[Inject]
public IModalService ModalService { get; set; } = null!;

/// <summary>
/// Gets or sets the creation panel service which hosts the "create new" form in a persistent side panel.
/// </summary>
[Inject]
public CreationPanelService CreationPanelService { get; set; } = null!;

/// <summary>
/// Gets or sets the toast service.
/// </summary>
Expand Down Expand Up @@ -96,9 +102,18 @@ private IQueryable<ViewModel>? ViewModels

private string? NameFilter { get; set; }

/// <inheritdoc />
protected override void OnInitialized()
{
base.OnInitialized();
this.CreationPanelService.ItemCreated += this.OnItemCreatedAsync;
}

/// <inheritdoc />
public async ValueTask DisposeAsync()
{
this.CreationPanelService.ItemCreated -= this.OnItemCreatedAsync;

await (this._disposeCts?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false);
this._disposeCts?.Dispose();
this._disposeCts = null;
Expand Down Expand Up @@ -205,29 +220,53 @@ private async Task OnCreateButtonClickAsync()
{
var cancellationToken = this._disposeCts?.Token ?? default;
var gameConfiguration = await this.DataSource.GetOwnerAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
using var creationContext = this.PersistenceContextProvider.CreateNewTypedContext(this.Type!, true, gameConfiguration);
var newObject = creationContext.CreateNew(this.Type!);
var parameters = new ModalParameters();
var modalType = typeof(ModalCreateNew<>).MakeGenericType(this.Type!);

parameters.Add(nameof(ModalCreateNew<object>.Item), newObject);
parameters.Add(nameof(ModalCreateNew<object>.PersistenceContext), creationContext);
var options = new ModalOptions
var creationContext = this.PersistenceContextProvider.CreateNewTypedContext(this.Type!, true, gameConfiguration);
try
{
var newObject = creationContext.CreateNew(this.Type!);

var session = new CreationSession
{
Title = $"Create {this.Type!.GetTypeCaption()}",
Item = newObject,
ItemType = this.Type!,
Context = creationContext,
OwnsContext = true,
SaveAsync = async () =>
{
await creationContext.SaveChangesAsync().ConfigureAwait(false);
await this.DataSource.ForceDiscardChangesAsync().ConfigureAwait(false);
this.ToastService.ShowSuccess(Resources.CreatedSuccessfully);
},
};

var started = await this.CreationPanelService.BeginAsync(session).ConfigureAwait(false);
if (!started)
{
creationContext.Dispose();
}
}
catch
{
DisableBackgroundCancel = true,
};
creationContext.Dispose();
throw;
}
Comment thread
sven-n marked this conversation as resolved.
}

var modal = this.ModalService.Show(modalType, $"Create", parameters, options);
var result = await modal.Result.ConfigureAwait(false);
if (!result.Cancelled)
private Task OnItemCreatedAsync(Type createdType)
{
if (createdType != this.Type)
{
await creationContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await this.DataSource.ForceDiscardChangesAsync().ConfigureAwait(false);
return Task.CompletedTask;
}

this.ToastService.ShowSuccess("New object successfully created.");
return this.InvokeAsync(() =>
{
var cancellationToken = this._disposeCts?.Token ?? default;
this._viewModels = null;
this.StateHasChanged();
this._loadTask = Task.Run(() => this.LoadDataAsync(cancellationToken), cancellationToken);
}
});
Comment thread
sven-n marked this conversation as resolved.
}

private async Task OnDuplicateButtonClickAsync(ViewModel viewModel)
Expand All @@ -236,40 +275,50 @@ private async Task OnDuplicateButtonClickAsync(ViewModel viewModel)
{
var cancellationToken = this._disposeCts?.Token ?? default;
var gameConfiguration = await this.DataSource.GetOwnerAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
using var context = this.PersistenceContextProvider.CreateNewTypedContext(this.Type!, true, gameConfiguration);
var original = await context.GetByIdAsync(viewModel.Id, this.Type!, cancellationToken).ConfigureAwait(false);
if (original is null)
{
this.ToastService.ShowError(string.Format(Resources.CouldNotFindToDuplicate, viewModel.Name));
return;
}

var newObject = await this.DuplicateObjectAsync(original, gameConfiguration, context, viewModel, cancellationToken).ConfigureAwait(false);
if (newObject is null)
var context = this.PersistenceContextProvider.CreateNewTypedContext(this.Type!, true, gameConfiguration);
try
{
return;
}

var parameters = new ModalParameters();
var modalType = typeof(ModalCreateNew<>).MakeGenericType(this.Type!);
var original = await context.GetByIdAsync(viewModel.Id, this.Type!, cancellationToken).ConfigureAwait(false);
if (original is null)
{
this.ToastService.ShowError(string.Format(Resources.CouldNotFindToDuplicate, viewModel.Name));
context.Dispose();
return;
}

parameters.Add(nameof(ModalCreateNew<object>.Item), newObject);
parameters.Add(nameof(ModalCreateNew<object>.PersistenceContext), context);
var options = new ModalOptions
{
DisableBackgroundCancel = true,
};
var newObject = await this.DuplicateObjectAsync(original, gameConfiguration, context, viewModel, cancellationToken).ConfigureAwait(false);
if (newObject is null)
{
context.Dispose();
return;
}

var modal = this.ModalService.Show(modalType, $"Duplicate '{viewModel.Name}'", parameters, options);
var result = await modal.Result.ConfigureAwait(false);
if (!result.Cancelled)
var duplicatedName = viewModel.Name;
var session = new CreationSession
{
Title = $"Duplicate '{duplicatedName}'",
Item = newObject,
ItemType = this.Type!,
Context = context,
OwnsContext = true,
SaveAsync = async () =>
{
await context.SaveChangesAsync().ConfigureAwait(false);
await this.DataSource.ForceDiscardChangesAsync().ConfigureAwait(false);
this.ToastService.ShowSuccess(string.Format(Resources.DuplicatedSuccessfully, duplicatedName));
},
};

var started = await this.CreationPanelService.BeginAsync(session).ConfigureAwait(false);
if (!started)
{
context.Dispose();
}
}
catch
{
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await this.DataSource.ForceDiscardChangesAsync().ConfigureAwait(false);

this.ToastService.ShowSuccess(string.Format(Resources.DuplicatedSuccessfully, viewModel.Name));
this._viewModels = null;
this._loadTask = Task.Run(() => this.LoadDataAsync(cancellationToken), cancellationToken);
context.Dispose();
throw;
}
}
catch (Exception ex)
Expand Down
Loading
Loading