Skip to content

Commit

Permalink
tabs in the same panel are now reorderable + panels number limited to 2
Browse files Browse the repository at this point in the history
TODO
- Move tabs between panels
- Document SortableList and download its js file
- Refactors
- Unit tests
  • Loading branch information
joao4all committed Jul 22, 2024
1 parent 77023f0 commit e3d27a0
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 185 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public void SetUp()
this.renderer = this.context.RenderComponent<TabsPanelComponent>(parameters =>
{
parameters.Add(p => p.ViewModel, this.viewModel.Object);
parameters.Add(p => p.Handler, this.viewModel.Object);
parameters.Add(p => p.Panel, this.viewModel.Object);
parameters.Add(p => p.CssClass, "css-test-class");
parameters.Add(p => p.IsSidePanelAvailable, true);
parameters.Add(p => p.Tabs, this.viewModel.Object.OpenTabs.Items.ToList());
Expand Down
89 changes: 89 additions & 0 deletions COMETwebapp/Components/Shared/SortableList.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
@using System.Collections.Generic
@using System.Diagnostics.CodeAnalysis

@inject IJSRuntime JS

@typeparam T

<div id="@(this.Id)" class="@(this.CssClass)">
@foreach (var item in this.Items)
{
@if (this.SortableItemTemplate is not null)
{
@(this.SortableItemTemplate(item))
}
}
</div>

@code {

[Parameter]
public RenderFragment<T>? SortableItemTemplate { get; set; }

Check warning on line 21 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 21 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 21 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

[Parameter, AllowNull]
public List<T> Items { get; set; }

[Parameter]
public EventCallback<(int oldIndex, int newIndex)> OnUpdate { get; set; }

[Parameter]
public EventCallback<(int oldIndex, int newIndex)> OnRemove { get; set; }

[Parameter]
public string Id { get; set; } = Guid.NewGuid().ToString();

[Parameter]
public string Group { get; set; } = Guid.NewGuid().ToString();

[Parameter]
public string? Pull { get; set; }

Check warning on line 39 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 39 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 39 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

[Parameter]
public bool Put { get; set; } = true;

[Parameter]
public bool Sort { get; set; } = true;

[Parameter]
public string Handle { get; set; } = string.Empty;

[Parameter]
public string? Filter { get; set; }

Check warning on line 51 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 51 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 51 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

[Parameter]
public bool ForceFallback { get; set; } = true;

/// <summary>
/// Gets or sets the custom css class to be applied in the container component
/// </summary>
[Parameter]
public string CssClass { get; set; }

private DotNetObjectReference<SortableList<T>>? selfReference;

Check warning on line 62 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 62 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 62 in COMETwebapp/Components/Shared/SortableList.razor

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
this.selfReference = DotNetObjectReference.Create(this);
var module = await this.JS.InvokeAsync<IJSObjectReference>("import", "./Components/Shared/SortableList.razor.js");
await module.InvokeAsync<string>("init", this.Id, this.Group, this.Pull, this.Put, this.Sort, this.Handle, this.Filter, this.selfReference, this.ForceFallback);
}
}

[JSInvokable]
public void OnUpdateJS(int oldIndex, int newIndex)
{
// invoke the OnUpdate event passing in the oldIndex and the newIndex
this.OnUpdate.InvokeAsync((oldIndex, newIndex));
}

[JSInvokable]
public void OnRemoveJS(int oldIndex, int newIndex)
{
// remove the item from the list
this.OnRemove.InvokeAsync((oldIndex, newIndex));
}

public void Dispose() => this.selfReference?.Dispose();
}
13 changes: 13 additions & 0 deletions COMETwebapp/Components/Shared/SortableList.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
you need the ::deep identifier if you are using scoped styles like this
because scoped styles are only applied to markup in the component, not
to the markup inside the render fragment.
*/

::deep .sortable-ghost {
visibility: hidden;
}

::deep .sortable-fallback {
opacity: 1 !important
}
34 changes: 34 additions & 0 deletions COMETwebapp/Components/Shared/SortableList.razor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export function init(id, group, pull, put, sort, handle, filter, component, forceFallback) {
var sortable = new Sortable(document.getElementById(id), {
animation: 200,
group: {
name: group,
pull: pull || true,
put: put
},
filter: filter || undefined,
sort: sort,
forceFallback: forceFallback,
handle: handle || undefined,
onUpdate: (event) => {
// Revert the DOM to match the .NET state
event.item.remove();
event.to.insertBefore(event.item, event.to.childNodes[event.oldIndex]);

// Notify .NET to update its model and re-render
component.invokeMethodAsync('OnUpdateJS', event.oldDraggableIndex, event.newDraggableIndex);
},
onRemove: (event) => {
if (event.pullMode === 'clone') {
// Remove the clone
event.clone.remove();
}

event.item.remove();
event.from.insertBefore(event.item, event.from.childNodes[event.oldIndex]);

// Notify .NET to update its model and re-render
component.invokeMethodAsync('OnRemoveJS', event.oldDraggableIndex, event.newDraggableIndex);
}
});
}
41 changes: 22 additions & 19 deletions COMETwebapp/Components/Tabs/TabsPanelComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,30 @@
------------------------------------------------------------------------------->
@using COMETwebapp.Model
@using CDP4Common.CommonData

@inherits DisposableComponent

<div class="panel-view @(this.CssClass)">
@if (this.Handler.CurrentTab is not null)
@if (this.Panel.CurrentTab is not null)
{
<div class="d-flex tabs-row justify-content-between">
<div class="d-flex tabs-row justify-content-between user-select-none">
<div class="d-flex gap-2">
@foreach (var tab in this.Tabs)
{
<TabComponent Text="@(GetTabText(tab))"
Caption="@(this.GetCaptionText(tab.ObjectOfInterest))"
Icon="typeof(FeatherX)"
CustomOptionIcon="@(typeof(FeatherCopy))"
CustomOptionIconVisible="@(tab.ObjectOfInterest != null)"
OnClick="@(() => this.OnTabClick.InvokeAsync((tab, this.Handler)))"
OnIconClick="@(() => this.OnRemoveTabClick.InvokeAsync(tab))"
OnCustomOptionIconClick="@(() => this.OnCreateTabForModel.InvokeAsync(tab))"
IsCurrent="@(tab == this.Handler.CurrentTab)"
ApplicationIcon="@(Applications.ExistingApplications.OfType<TabbedApplication>().First(x => x.ComponentType == tab.ComponentType).IconType)"
@key="tab"/>
}

<SortableList Group="sharedList" Items="@(this.Panel.OpenTabs.Items.ToList())" OnUpdate="@(indexes => this.SortTabs(indexes.oldIndex, indexes.newIndex))" Context="tab" CssClass="d-flex gap-2">
<SortableItemTemplate>
<TabComponent Text="@(GetTabText(tab))"
Caption="@(this.GetCaptionText(tab.ObjectOfInterest))"
Icon="typeof(FeatherX)"
CustomOptionIcon="@(typeof(FeatherCopy))"
CustomOptionIconVisible="@(tab.ObjectOfInterest != null)"
OnClick="@(() => this.OnTabClick.InvokeAsync((tab, this.Panel)))"
OnIconClick="@(() => this.OnRemoveTabClick.InvokeAsync(tab))"
OnCustomOptionIconClick="@(() => this.OnCreateTabForModel.InvokeAsync(tab))"
IsCurrent="@(tab == this.Panel.CurrentTab)"
ApplicationIcon="@(Applications.ExistingApplications.OfType<TabbedApplication>().First(x => x.ComponentType == tab.ComponentType).IconType)"
@key="tab"/>
</SortableItemTemplate>
</SortableList>

<TabComponent Text="Select Model"
Icon="typeof(FeatherPlus)"
Expand All @@ -61,9 +64,9 @@

</div>
<div class="template-container" id="tabs-page-content">
<DynamicApplicationBase ViewModel="this.Handler.CurrentTab.ApplicationBaseViewModel"
ApplicationBaseType="this.Handler.CurrentTab.ComponentType"
CurrentThing="this.Handler.CurrentTab.ObjectOfInterest as Thing"/>
<DynamicApplicationBase ViewModel="this.Panel.CurrentTab.ApplicationBaseViewModel"
ApplicationBaseType="this.Panel.CurrentTab.ComponentType"
CurrentThing="this.Panel.CurrentTab.ObjectOfInterest as Thing"/>
</div>
}
</div>
35 changes: 19 additions & 16 deletions COMETwebapp/Components/Tabs/TabsPanelComponent.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,14 @@ public partial class TabsPanelComponent : DisposableComponent
/// Gets or sets the tab handler to be used
/// </summary>
[Parameter]
public ITabHandler Handler { get; set; }
public TabPanelInformation Panel { get; set; }

/// <summary>
/// Gets or sets the <see cref="ITabsViewModel" />
/// </summary>
[Parameter]
public ITabsViewModel ViewModel { get; set; }

/// <summary>
/// Gets or sets the tabs to be displayed
/// </summary>
[Parameter]
public List<TabbedApplicationInformation> Tabs { get; set; } = [];

/// <summary>
/// Gets or sets the method to be executed when the open tab button is clicked
/// </summary>
Expand All @@ -89,7 +83,7 @@ public partial class TabsPanelComponent : DisposableComponent
/// Gets or sets the method to be executed when the tab is clicked
/// </summary>
[Parameter]
public EventCallback<(TabbedApplicationInformation, ITabHandler)> OnTabClick { get; set; }
public EventCallback<(TabbedApplicationInformation, TabPanelInformation)> OnTabClick { get; set; }

/// <summary>
/// Gets or sets the condition to check if the side panel should be available
Expand All @@ -103,6 +97,17 @@ public partial class TabsPanelComponent : DisposableComponent
[Inject]
public ISessionService SessionService { get; set; }

/// <summary>
/// Sorts the tabs by the means of drag and drop
/// </summary>
/// <param name="oldIndex">The dragged tab old index</param>
/// <param name="newIndex">The dragged tab new index</param>
/// <returns>A <see cref="Task" /></returns>
private void SortTabs(int oldIndex, int newIndex)
{
this.Panel.OpenTabs.Move(oldIndex, newIndex);
}

/// <summary>
/// Gets the tab text for the given object of interest
/// </summary>
Expand Down Expand Up @@ -158,16 +163,14 @@ private string GetCaptionText(object objectOfInterest)
/// </summary>
private void AddSidePanel()
{
var currentTab = this.ViewModel.CurrentTab;
var currentTab = this.ViewModel.MainPanel.CurrentTab;
this.ViewModel.SidePanel.OpenTabs.Add(currentTab);
this.ViewModel.SidePanel.CurrentTab = currentTab;

var newPanel = new TabPanelInformation
{
CurrentTab = currentTab
};
this.ViewModel.MainPanel.OpenTabs.Remove(currentTab);
this.ViewModel.MainPanel.CurrentTab = this.ViewModel.MainPanel.OpenTabs.Items.FirstOrDefault();

currentTab.Panel = newPanel;
this.ViewModel.SidePanels.Add(newPanel);
this.ViewModel.CurrentTab = this.ViewModel.OpenTabs.Items.FirstOrDefault(x => x.Panel == null);
// todo: make open tabs reactive so when there are no open tabs, the current tab is set to null

Check warning on line 173 in COMETwebapp/Components/Tabs/TabsPanelComponent.razor.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
}
}
}
37 changes: 0 additions & 37 deletions COMETwebapp/Model/ITabHandler.cs

This file was deleted.

22 changes: 20 additions & 2 deletions COMETwebapp/Model/TabPanelInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,20 @@

namespace COMETwebapp.Model
{
using DynamicData;

using ReactiveUI;

/// <summary>
/// The <see cref="TabPanelInformation" /> provides required information related to a panel
/// </summary>
public class TabPanelInformation : ITabHandler
public class TabPanelInformation : ReactiveObject
{
/// <summary>
/// Backing field for the property <see cref="CurrentTab" />
/// </summary>
private TabbedApplicationInformation currentTab;

/// <summary>
/// Initializes a new instance of the <see cref="TabbedApplicationInformation" /> class.
/// </summary>
Expand All @@ -39,6 +48,15 @@ public TabPanelInformation()
/// <summary>
/// Gets or sets the current tab
/// </summary>
public TabbedApplicationInformation CurrentTab { get; set; }
public TabbedApplicationInformation CurrentTab
{
get => this.currentTab;
set => this.RaiseAndSetIfChanged(ref this.currentTab, value);
}

/// <summary>
/// Gets the collection of all <see cref="TabbedApplicationInformation" /> contained by the panel
/// </summary>
public SourceList<TabbedApplicationInformation> OpenTabs { get; set; } = new();
}
}
5 changes: 0 additions & 5 deletions COMETwebapp/Model/TabbedApplicationInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,5 @@ public TabbedApplicationInformation(IApplicationBaseViewModel applicationBaseVie
/// Gets the object of interest
/// </summary>
public object ObjectOfInterest { get; }

/// <summary>
/// Gets or sets the <see cref="TabPanelInformation"/>
/// </summary>
public TabPanelInformation Panel { get; set; }
}
}
Loading

0 comments on commit e3d27a0

Please sign in to comment.