Merge branch 'main' of https://git.azuze.fr/kawa/SharepointToolbox-Web
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
@* Recursive editor row for one folder in the visual builder. *@
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
|
||||
<div class="folder-node" style="margin-left:@(Depth > 1 ? "18px" : "0")">
|
||||
<div class="flex-row" style="gap:6px">
|
||||
<span class="text-muted" style="font-family:monospace">📁</span>
|
||||
<input class="form-input" style="width:auto;flex:1;min-width:160px"
|
||||
placeholder="Folder name" value="@Node.Name"
|
||||
@oninput="OnNameInput" />
|
||||
@if (Depth < FolderNode.MaxDepth)
|
||||
{
|
||||
<button class="btn btn-secondary btn-sm" type="button" @onclick="AddChild" title="Add subfolder">+ Sub</button>
|
||||
}
|
||||
<button class="btn btn-danger btn-sm" type="button" @onclick="() => OnRemove.InvokeAsync(Node)" title="Remove">✕</button>
|
||||
</div>
|
||||
|
||||
@foreach (var child in Node.Children)
|
||||
{
|
||||
<FolderTreeNode Node="child" Depth="Depth + 1" OnRemove="RemoveChild" OnChanged="OnChanged" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public FolderNode Node { get; set; } = default!;
|
||||
[Parameter] public int Depth { get; set; } = 1;
|
||||
[Parameter] public EventCallback<FolderNode> OnRemove { get; set; }
|
||||
[Parameter] public EventCallback OnChanged { get; set; }
|
||||
|
||||
private async Task OnNameInput(ChangeEventArgs e)
|
||||
{
|
||||
Node.Name = e.Value?.ToString() ?? string.Empty;
|
||||
await OnChanged.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task AddChild()
|
||||
{
|
||||
Node.Children.Add(new FolderNode());
|
||||
await OnChanged.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task RemoveChild(FolderNode child)
|
||||
{
|
||||
Node.Children.Remove(child);
|
||||
await OnChanged.InvokeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
@inject ILibraryDiscoveryService LibraryDiscovery
|
||||
|
||||
@* Library name field with a picker: type a title, or click Browse to load and
|
||||
choose from the libraries on the selected site. *@
|
||||
<div class="form-group library-picker">
|
||||
<label class="form-label">@Label</label>
|
||||
<div class="flex-row" style="gap:8px;align-items:stretch">
|
||||
<input class="form-input" style="flex:1"
|
||||
placeholder="@Placeholder"
|
||||
value="@Library"
|
||||
@oninput="OnTextInput"
|
||||
disabled="@Disabled" />
|
||||
<button type="button" class="btn btn-secondary"
|
||||
@onclick="Browse"
|
||||
disabled="@(Disabled || _loading)">
|
||||
@(_loading ? "Loading…" : "Browse")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_error))
|
||||
{
|
||||
<div class="alert alert-error mt-8">@_error</div>
|
||||
}
|
||||
|
||||
@if (_open)
|
||||
{
|
||||
<div class="library-picker-list" style="max-height:200px;overflow:auto;border:1px solid var(--border);border-radius:4px;padding:4px;margin-top:6px">
|
||||
@foreach (var lib in _libraries)
|
||||
{
|
||||
<button type="button"
|
||||
class="library-picker-item @(string.Equals(lib, Library, StringComparison.OrdinalIgnoreCase) ? "active" : "")"
|
||||
style="display:block;width:100%;text-align:left;padding:4px 8px;border:none;background:none;cursor:pointer;border-radius:3px"
|
||||
@onclick="() => Pick(lib)">
|
||||
@lib
|
||||
</button>
|
||||
}
|
||||
@if (_libraries.Count == 0)
|
||||
{
|
||||
<div class="text-muted" style="padding:6px">No document libraries found on this site.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public TenantProfile Profile { get; set; } = default!;
|
||||
[Parameter] public string? SiteUrl { get; set; }
|
||||
[Parameter] public string Library { get; set; } = string.Empty;
|
||||
[Parameter] public EventCallback<string> LibraryChanged { get; set; }
|
||||
[Parameter] public string Label { get; set; } = "Library";
|
||||
[Parameter] public string Placeholder { get; set; } = "Shared Documents";
|
||||
[Parameter] public bool Disabled { get; set; }
|
||||
|
||||
private List<string> _libraries = new();
|
||||
private bool _loading, _open;
|
||||
private string _error = string.Empty;
|
||||
private string? _loadedForSite;
|
||||
|
||||
private async Task OnTextInput(ChangeEventArgs e)
|
||||
{
|
||||
Library = e.Value?.ToString() ?? string.Empty;
|
||||
await LibraryChanged.InvokeAsync(Library);
|
||||
}
|
||||
|
||||
private async Task Browse()
|
||||
{
|
||||
_error = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(SiteUrl)) { _error = "Select a site first."; return; }
|
||||
|
||||
// Toggle closed if already showing the list for this site.
|
||||
if (_open && _loadedForSite == SiteUrl) { _open = false; return; }
|
||||
|
||||
// Reload when the site changed since the last load.
|
||||
if (_libraries.Count == 0 || _loadedForSite != SiteUrl)
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
_libraries = (await LibraryDiscovery.ListLibrariesAsync(Profile, SiteUrl!)).ToList();
|
||||
_loadedForSite = SiteUrl;
|
||||
}
|
||||
catch (Exception ex) { _error = ex.Message; return; }
|
||||
finally { _loading = false; }
|
||||
}
|
||||
_open = true;
|
||||
}
|
||||
|
||||
private async Task Pick(string lib)
|
||||
{
|
||||
Library = lib;
|
||||
_open = false;
|
||||
await LibraryChanged.InvokeAsync(Library);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
@inject IUserSessionService Session
|
||||
@inject SharepointToolbox.Web.Infrastructure.Persistence.ProfileRepository ProfileRepo
|
||||
@implements IDisposable
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
@using SharepointToolbox.Web.Services.Session
|
||||
|
||||
<div class="profile-selector">
|
||||
<button class="profile-selector-trigger @(Session.HasProfile ? "" : "unset")" @onclick="ToggleAsync">
|
||||
<span class="profile-selector-icon">🏢</span>
|
||||
<span class="profile-selector-name">@(Session.CurrentProfile?.Name ?? "Select a profile")</span>
|
||||
<span class="profile-selector-caret @(_open ? "open" : "")">▾</span>
|
||||
</button>
|
||||
|
||||
@if (_open)
|
||||
{
|
||||
<div class="profile-selector-backdrop" @onclick="Close"></div>
|
||||
<div class="profile-selector-menu">
|
||||
@if (_profiles.Count == 0)
|
||||
{
|
||||
<div class="profile-selector-empty">No profiles configured.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var p in _profiles)
|
||||
{
|
||||
var active = Session.CurrentProfile?.Id == p.Id;
|
||||
<button class="profile-selector-item @(active ? "active" : "")" @onclick="() => Select(p)">
|
||||
<span class="profile-selector-item-text">
|
||||
<span class="profile-selector-item-name">@p.Name</span>
|
||||
<span class="profile-selector-item-url">@p.TenantUrl</span>
|
||||
</span>
|
||||
@if (active)
|
||||
{
|
||||
<span class="profile-selector-check">✓</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<a class="profile-selector-manage" href="/profiles" @onclick="Close">⚙️ Manage profiles</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool _open;
|
||||
private List<TenantProfile> _profiles = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Session.ProfileChanged += OnProfileChanged;
|
||||
await LoadProfilesAsync();
|
||||
}
|
||||
|
||||
private void OnProfileChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
private async Task LoadProfilesAsync()
|
||||
=> _profiles = (await ProfileRepo.LoadAsync()).ToList();
|
||||
|
||||
private async Task ToggleAsync()
|
||||
{
|
||||
_open = !_open;
|
||||
// Refresh the list when opening so newly created/edited profiles show up without a reload.
|
||||
if (_open) await LoadProfilesAsync();
|
||||
}
|
||||
|
||||
private void Close() => _open = false;
|
||||
|
||||
private void Select(TenantProfile p)
|
||||
{
|
||||
_open = false;
|
||||
Session.SetProfile(p);
|
||||
}
|
||||
|
||||
public void Dispose() => Session.ProfileChanged -= OnProfileChanged;
|
||||
}
|
||||
Reference in New Issue
Block a user