@page "/admin/{TableDisplayName}"
@layout HopFrameLayout
@rendermode InteractiveServer
@implements IDisposable
@using HopFrame.Core.Config
@using HopFrame.Core.Callbacks
@using HopFrame.Core.Services
@using HopFrame.Web.Models
@using HopFrame.Web.Plugins
@using HopFrame.Web.Plugins.Events
@using HopFrame.Web.Services
@using Microsoft.JSInterop
@if (!DisplaySelection) {
@_config?.DisplayName
}
@_config?.DisplayName
@if (!DisplaySelection && _buttonToggles.ShowRefreshButton) {
Refresh
}
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopLeft)) {
@button.Title
}
{ SearchFocus(); UpdateSearchSuggestions(); }"
@onfocusout="SearchUnfocus"
Style="width: 500px"/>
@if (_isSearchActive && _searchSuggestions.Count > 0) {
}
@if (_hasCreatePolicy && DisplayActions && _buttonToggles.ShowAddEntityButton) {
Add Entity
}
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.TopRight)) {
@button.Title
}
@if (DisplaySelection) {
}
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
}
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy) && (_buttonToggles.ShowEditButton || _buttonToggles.ShowDeleteButton && _pluginButtons.Any(pb => pb.IsForTable(_config)))) {
@foreach (var button in _pluginButtons.Where(pb => pb.IsForTable(_config)).Where(pb => pb.Position == PluginButtonPosition.OnEntry)) {
}
@if (_hasUpdatePolicy && _buttonToggles.ShowEditButton) {
}
@if (_hasDeletePolicy && _buttonToggles.ShowDeleteButton) {
}
}
@if (_totalPages > 1) {
Page
of @_totalPages
}
@inject IContextExplorer Explorer
@inject NavigationManager Navigator
@inject IJSRuntime Js
@inject IDialogService Dialogs
@inject IHopFrameAuthHandler Handler
@inject ICallbackEmitter Emitter
@inject IPluginOrchestrator PluginOrchestrator
@inject ISearchSuggestionProvider SearchSuggestions
@code {
[Parameter]
public required string TableDisplayName { get; set; }
[Parameter]
public bool DisplaySelection { get; set; }
[Parameter]
public bool DisplayActions { get; set; } = true;
[Parameter]
public RelationPickerDialogData? DialogData { get; set; }
[Parameter]
public DataGridSelectMode SelectionMode { get; set; } = DataGridSelectMode.Single;
[Parameter]
public int PerPage { get; set; } = 20;
private TableConfig? _config;
private ITableManager? _manager;
public object[] CurrentlyDisplayedModels = [];
private int _currentPage;
private int _totalPages;
private string? _searchTerm;
private bool _loading;
private bool _isSearchActive;
private IList _searchSuggestions = [];
private FluentSearch? _searchBox;
private bool _hasUpdatePolicy;
private bool _hasDeletePolicy;
private bool _hasCreatePolicy;
private bool _allSelected;
private readonly CancellationTokenSource _tokenSource = new();
private List _pluginButtons = new();
private DefaultButtonToggles _buttonToggles = new();
private string? _currentUser;
protected override void OnInitialized() {
_config ??= Explorer.GetTable(TableDisplayName);
if (_config is null || (_config.Ignored && DialogData is null)) {
Navigator.NavigateTo("/admin", true);
}
}
protected override async Task OnInitializedAsync() {
if (!await Handler.IsAuthenticatedAsync(_config?.ViewPolicy)) {
Navigator.NavigateTo("/admin", true);
return;
}
_currentUser = await Handler.GetCurrentUserDisplayNameAsync();
var eventResult = await PluginOrchestrator.DispatchEvent(new TableInitializedEvent(this, _currentUser!) {
Table = _config!
});
if (eventResult.IsCanceled) return;
_pluginButtons = eventResult.PluginButtons;
_buttonToggles = eventResult.DefaultButtons;
_hasUpdatePolicy = await Handler.IsAuthenticatedAsync(_config?.UpdatePolicy);
_hasDeletePolicy = await Handler.IsAuthenticatedAsync(_config?.DeletePolicy);
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
CurrentlyDisplayedModels = (await _manager!.LoadPage(_currentPage, PerPage)).ToArray();
_totalPages = await _manager.TotalPages(PerPage);
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
try {
await Js.InvokeVoidAsync("removeBg");
}
catch (Exception) {
// ignored
}
}
public void Dispose() {
_searchCancel.Dispose();
_tokenSource.Dispose();
}
private CancellationTokenSource _searchCancel = new();
private async Task OnSearch(ChangeEventArgs eventArgs) {
await _searchCancel.CancelAsync();
_searchTerm = eventArgs.Value?.ToString();
if (_searchTerm is null) return;
_searchCancel = new();
UpdateSearchSuggestions();
await Task.Delay(500, _searchCancel.Token);
var eventResult = await PluginOrchestrator.DispatchEvent(new SearchEvent(this, _currentUser!) {
SearchTerm = _searchTerm,
Table = _config!,
CurrentPage = _currentPage
}, _tokenSource.Token);
if (eventResult.IsCanceled) {
if (eventResult.SearchResult is null) return;
CurrentlyDisplayedModels = eventResult.SearchResult.ToArray();
_totalPages = eventResult.TotalPages;
return;
}
_searchTerm = eventResult.SearchTerm;
await Reload();
}
private async Task SearchSuggestionSelected(string? suggestion) {
if (string.IsNullOrWhiteSpace(suggestion)) return;
_searchTerm = SearchSuggestions.CompleteSearchSuggestion(_config!, _searchTerm ?? string.Empty, suggestion);
_searchBox!.Value = _searchTerm;
_searchBox.FocusAsync();
UpdateSearchSuggestions();
if (!suggestion.EndsWith('='))
await OnSearch(new() {
Value = _searchTerm
});
}
private void UpdateSearchSuggestions() {
if (_config is null || !_config.ShowSearchSuggestions) return;
_searchSuggestions = SearchSuggestions.GenerateSearchSuggestions(_config, _searchTerm ?? string.Empty).ToList();
}
private CancellationTokenSource _searchFocusCancel = new();
private async Task SearchFocus() {
_isSearchActive = true;
await _searchFocusCancel.CancelAsync();
_searchFocusCancel = new();
}
private async Task SearchUnfocus() {
await Task.Delay(10, _searchFocusCancel.Token);
_isSearchActive = false;
}
public async Task Reload() {
_loading = true;
var eventResult = await PluginOrchestrator.DispatchEvent(new ReloadEvent(this, _currentUser!) {
Table = _config!
}, _tokenSource.Token);
if (eventResult.IsCanceled) {
_loading = false;
return;
}
if (!string.IsNullOrEmpty(_searchTerm)) {
(var query, _totalPages) = await _manager!.Search(_searchTerm, _currentPage, PerPage);
CurrentlyDisplayedModels = query.ToArray();
}
else {
await OnInitializedAsync();
}
_loading = false;
}
public async Task ChangePage(int page) {
var eventResult = await PluginOrchestrator.DispatchEvent(new PageChangeEvent(this, _currentUser!) {
CurrentPage = _currentPage,
NewPage = page,
TotalPages = _totalPages,
Table = _config!
}, _tokenSource.Token);
if (eventResult.IsCanceled) return;
page = eventResult.NewPage;
if (page < 0 || page > _totalPages - 1) return;
_currentPage = page;
await Reload();
}
private async Task DeleteEntry(object element) {
if (!await Handler.IsAuthenticatedAsync(_config?.DeletePolicy)) {
Navigator.NavigateTo("/admin", true);
return;
}
var dialog = await Dialogs.ShowConfirmationAsync("Do you really want to delete this entry?");
var result = await dialog.Result;
if (result.Cancelled) return;
var eventResult = await PluginOrchestrator.DispatchEvent(new DeleteEntryEvent(this, _currentUser!) {
Entity = element,
Table = _config!
}, _tokenSource.Token);
if (eventResult.IsCanceled) return;
await _manager!.DeleteItem(element);
await Emitter.DispatchCallback(CallbackTypes.DeleteEntry(_config!), element);
await Reload();
}
private async Task CreateOrEdit(object? element) {
if (!await Handler.IsAuthenticatedAsync(element is null ? _config?.CreatePolicy : _config?.UpdatePolicy)) {
Navigator.NavigateTo("/admin", true);
return;
}
var panel = await Dialogs.ShowPanelAsync(new EditorDialogData(_config!, element), new DialogParameters {
TrapFocus = false
});
var result = await panel.Result;
var data = result.Data as EditorDialogData;
if (result.Cancelled) return;
HopFrameTablePageEventArgs eventArgs;
if (element is null) {
eventArgs = new CreateEntryEvent(this, _currentUser!) {
Table = _config!,
Entity = data!.CurrentObject!
};
}
else {
eventArgs = new UpdateEntryEvent(this, _currentUser!) {
Table = _config!,
Entity = data!.CurrentObject!
};
}
var eventResult = await PluginOrchestrator.DispatchEvent(eventArgs, _tokenSource.Token);
if (eventResult.IsCanceled) return;
if (element is null) {
await _manager!.AddItem(data!.CurrentObject!);
await Emitter.DispatchCallback(CallbackTypes.CreateEntry(_config!), data.CurrentObject!);
}
else {
await _manager!.EditItem(data!.CurrentObject!);
await Emitter.DispatchCallback(CallbackTypes.UpdateEntry(_config!), data.CurrentObject!);
}
await Reload();
}
private void SelectItem(object item, bool selected) {
var eventResult = PluginOrchestrator.DispatchEvent(new SelectEntryEvent(this, _currentUser!) {
Entity = item,
Selected = selected,
Table = _config!
}, _tokenSource.Token).Result;
if (eventResult.IsCanceled) return;
selected = eventResult.Selected;
if (!selected)
DialogData!.SelectedObjects.Remove(item);
else DialogData!.SelectedObjects.Add(item);
}
private void SelectAll() {
var selected = CurrentlyDisplayedModels.All(DialogData!.SelectedObjects.Contains);
foreach (var displayedModel in CurrentlyDisplayedModels) {
SelectItem(displayedModel, !selected);
}
_allSelected = selected;
}
private async Task DisplayProperty(PropertyConfig config, object entry) {
var display = await _manager!.DisplayProperty(entry, config);
if (display.Length > config.DisplayLength)
display = display[..config.DisplayLength] + "...";
return display;
}
public InputFile? FileInputElement;
public Func, Task>? OnFileUpload;
private async Task OnInputFiles(InputFileChangeEventArgs e) {
if (OnFileUpload is null) return;
if (e.FileCount == 1) {
await OnFileUpload.Invoke([e.File]);
}
else {
await OnFileUpload.Invoke(e.GetMultipleFiles());
}
}
public void RequestRender() {
StateHasChanged();
}
}