Merge branch 'main' of https://git.azuze.fr/kawa/SharepointToolbox-Web
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
@* Recursive editor row for one folder in the visual builder. *@
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
@inject TranslationSource T
|
||||
|
||||
<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"
|
||||
placeholder="@T["foldertree.ph.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-secondary btn-sm" type="button" @onclick="AddChild" title="@T["foldertree.btn.addsub.tooltip"]">@T["foldertree.btn.addsub"]</button>
|
||||
}
|
||||
<button class="btn btn-danger btn-sm" type="button" @onclick="() => OnRemove.InvokeAsync(Node)" title="Remove">✕</button>
|
||||
<button class="btn btn-danger btn-sm" type="button" @onclick="() => OnRemove.InvokeAsync(Node)" title="@T["foldertree.btn.remove.tooltip"]">✕</button>
|
||||
</div>
|
||||
|
||||
@foreach (var child in Node.Children)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@inject ILibraryDiscoveryService LibraryDiscovery
|
||||
@inject TranslationSource T
|
||||
|
||||
@* Library name field with a picker: type a title, or click Browse to load and
|
||||
choose from the libraries on the selected site. *@
|
||||
@@ -13,7 +14,7 @@
|
||||
<button type="button" class="btn btn-secondary"
|
||||
@onclick="Browse"
|
||||
disabled="@(Disabled || _loading)">
|
||||
@(_loading ? "Loading…" : "Browse")
|
||||
@(_loading ? T["librarypicker.loadingShort"] : T["librarypicker.browse"])
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +37,7 @@
|
||||
}
|
||||
@if (_libraries.Count == 0)
|
||||
{
|
||||
<div class="text-muted" style="padding:6px">No document libraries found on this site.</div>
|
||||
<div class="text-muted" style="padding:6px">@T["librarypicker.noLibraries"]</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -65,7 +66,7 @@
|
||||
private async Task Browse()
|
||||
{
|
||||
_error = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(SiteUrl)) { _error = "Select a site first."; return; }
|
||||
if (string.IsNullOrWhiteSpace(SiteUrl)) { _error = T["librarypicker.selectSiteFirst"]; return; }
|
||||
|
||||
// Toggle closed if already showing the list for this site.
|
||||
if (_open && _loadedForSite == SiteUrl) { _open = false; return; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
@inject TranslationSource T
|
||||
|
||||
@* Reusable logo picker. Reads an image into a base64 LogoData (no disk/blob storage). *@
|
||||
<div class="logo-upload">
|
||||
@@ -8,13 +9,13 @@
|
||||
<div class="flex-row" style="gap:12px;align-items:center">
|
||||
<img src="data:@Value.MimeType;base64,@Value.Base64" alt=""
|
||||
style="max-height:60px;max-width:200px;object-fit:contain;border:1px solid var(--border);border-radius:4px;padding:4px;background:#fff" />
|
||||
<button type="button" class="btn btn-secondary btn-sm" @onclick="Remove">Remove</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @onclick="Remove">@T["logoupload.remove"]</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<InputFile OnChange="OnChange" accept="image/png,image/jpeg,image/svg+xml,image/gif" />
|
||||
<small class="text-muted d-block">PNG, JPEG, SVG or GIF — max @(MaxBytes / 1024) KB.</small>
|
||||
<small class="text-muted d-block">@string.Format(T["logoupload.hint"], MaxBytes / 1024)</small>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(_error)) { <div class="alert alert-error mt-8">@_error</div> }
|
||||
</div>
|
||||
@@ -36,7 +37,7 @@
|
||||
|
||||
if (file.Size > MaxBytes)
|
||||
{
|
||||
_error = $"File too large ({file.Size / 1024} KB). Max {MaxBytes / 1024} KB.";
|
||||
_error = string.Format(T["logoupload.err.toolarge"], file.Size / 1024, MaxBytes / 1024);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_error = $"Could not read image: {ex.Message}";
|
||||
_error = string.Format(T["logoupload.err.read"], ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
@* Dropdown for choosing how multi-site reports are bundled on export. *@
|
||||
@inject TranslationSource T
|
||||
|
||||
@if (Visible)
|
||||
{
|
||||
<select class="form-select" style="width:auto;font-size:13px" value="@Value" @onchange="OnChange"
|
||||
title="How to bundle reports when multiple sites are scanned">
|
||||
<option value="@ReportMergeMode.SingleMerged">One document, no tabs</option>
|
||||
<option value="@ReportMergeMode.SingleTabbed">One document, tabs (HTML)</option>
|
||||
<option value="@ReportMergeMode.MultipleFiles">Multiple documents (ZIP)</option>
|
||||
title="@T["mergemode.tooltip"]">
|
||||
<option value="@ReportMergeMode.SingleMerged">@T["mergemode.opt.singleMerged"]</option>
|
||||
<option value="@ReportMergeMode.SingleTabbed">@T["mergemode.opt.singleTabbed"]</option>
|
||||
<option value="@ReportMergeMode.MultipleFiles">@T["mergemode.opt.multipleFiles"]</option>
|
||||
</select>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@inject TranslationSource T
|
||||
|
||||
<div class="no-profile">
|
||||
<h2>No profile selected</h2>
|
||||
<p>Select or create a tenant profile to get started.</p>
|
||||
<a href="/profiles" class="btn btn-primary">Go to Profiles</a>
|
||||
<h2>@T["noprofile.heading"]</h2>
|
||||
<p>@T["noprofile.body"]</p>
|
||||
<a href="/profiles" class="btn btn-primary">@T["noprofile.goto"]</a>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@inject IUserSessionService Session
|
||||
@inject SharepointToolbox.Web.Infrastructure.Persistence.ProfileRepository ProfileRepo
|
||||
@inject TranslationSource T
|
||||
@implements IDisposable
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
@using SharepointToolbox.Web.Services.Session
|
||||
@@ -7,7 +8,7 @@
|
||||
<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-name">@(Session.CurrentProfile?.Name ?? T["profile.selector.placeholder"])</span>
|
||||
<span class="profile-selector-caret @(_open ? "open" : "")">▾</span>
|
||||
</button>
|
||||
|
||||
@@ -17,7 +18,7 @@
|
||||
<div class="profile-selector-menu">
|
||||
@if (_profiles.Count == 0)
|
||||
{
|
||||
<div class="profile-selector-empty">No profiles configured.</div>
|
||||
<div class="profile-selector-empty">@T["profile.selector.empty"]</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -36,7 +37,7 @@
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<a class="profile-selector-manage" href="/profiles" @onclick="Close">⚙️ Manage profiles</a>
|
||||
<a class="profile-selector-manage" href="/profiles" @onclick="Close">⚙️ @T["profile.selector.manage"]</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@inject IUserSessionService Session
|
||||
@inject ISessionManager SessionManager
|
||||
@inject NavigationManager Nav
|
||||
@inject TranslationSource T
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
@using SharepointToolbox.Web.Services.Session
|
||||
|
||||
@@ -10,10 +11,10 @@
|
||||
<div class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="connect-modal-title">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h3 id="connect-modal-title">Connect to Microsoft</h3>
|
||||
<h3 id="connect-modal-title">@T["connect.title"]</h3>
|
||||
<p class="text-muted">
|
||||
Authenticate to access <strong>@Session.CurrentProfile?.Name</strong>.
|
||||
Your session token is stored in your browser only — never saved to disk.
|
||||
@T["connect.subtitle.prefix"] <strong>@Session.CurrentProfile?.Name</strong>.
|
||||
@T["connect.token.note"]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -23,14 +24,14 @@
|
||||
}
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @onclick="Cancel" disabled="@_connecting">Cancel</button>
|
||||
<button class="btn btn-secondary" @onclick="Cancel" disabled="@_connecting">@T["btn.cancel"]</button>
|
||||
<button class="btn btn-primary" @onclick="ConnectAsync" disabled="@_connecting">
|
||||
@(_connecting ? "Redirecting…" : "Connect via Microsoft")
|
||||
@(_connecting ? T["connect.redirecting"] : T["connect.button"])
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-muted" style="font-size:11px;margin-top:8px;text-align:right">
|
||||
You will be redirected to Microsoft login. MFA is supported.
|
||||
@T["connect.redirect.note"]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +55,7 @@
|
||||
private async Task ConnectAsync()
|
||||
{
|
||||
var profile = Session.CurrentProfile;
|
||||
if (profile is null) { _error = "No client profile selected."; return; }
|
||||
if (profile is null) { _error = T["connect.err.noprofile"]; return; }
|
||||
|
||||
_connecting = true;
|
||||
_error = string.Empty;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
@inject ISiteDiscoveryService SiteDiscovery
|
||||
@inject TranslationSource T
|
||||
|
||||
<div class="site-picker">
|
||||
<div class="flex-row" style="gap:8px;align-items:flex-end">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label class="form-label">@(Single ? "Site" : "Sites")</label>
|
||||
<input class="form-input" @bind="_filter" @bind:event="oninput" placeholder="Filter loaded sites by name or URL…" />
|
||||
<label class="form-label">@(Single ? T["sitepicker.label.site"] : T["sitepicker.label.sites"])</label>
|
||||
<input class="form-input" @bind="_filter" @bind:event="oninput" placeholder="@T["sitepicker.ph.filter"]" />
|
||||
</div>
|
||||
<button class="btn btn-secondary" @onclick="LoadSites" disabled="@_loading">
|
||||
@(_loading ? "Loading…" : (_all.Count > 0 ? "Reload sites" : "Load sites"))
|
||||
@(_loading ? T["sitepicker.status.loadingShort"] : (_all.Count > 0 ? T["sitepicker.btn.reload"] : T["sitepicker.btn.load"]))
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -21,11 +22,11 @@
|
||||
<div class="flex-row mt-8" style="gap:12px;align-items:center">
|
||||
@if (!Single)
|
||||
{
|
||||
<button class="btn btn-link btn-sm" @onclick="SelectAllFiltered">Select all (@Filtered.Count())</button>
|
||||
<button class="btn btn-link btn-sm" @onclick="SelectAllFiltered">@string.Format(T["sitepicker.btn.selectAllCount"], Filtered.Count())</button>
|
||||
}
|
||||
<button class="btn btn-link btn-sm" @onclick="ClearSelection">Clear</button>
|
||||
<button class="btn btn-link btn-sm" @onclick="ClearSelection">@T["sitepicker.btn.clear"]</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="count-badge">@SelectedSites.Count selected</span>
|
||||
<span class="count-badge">@string.Format(T["sitepicker.status.selectedCount"], SelectedSites.Count)</span>
|
||||
</div>
|
||||
<div class="site-picker-list" style="max-height:240px;overflow:auto;border:1px solid var(--border);border-radius:4px;padding:4px;margin-top:6px">
|
||||
@foreach (var s in Filtered)
|
||||
@@ -45,13 +46,13 @@
|
||||
}
|
||||
@if (!Filtered.Any())
|
||||
{
|
||||
<div class="text-muted" style="padding:6px">No sites match the filter.</div>
|
||||
<div class="text-muted" style="padding:6px">@T["sitepicker.empty.noMatch"]</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (!_loading)
|
||||
{
|
||||
<div class="text-muted mt-8" style="font-size:12px">Click “Load sites” to list the tenant’s SharePoint sites, then @(Single ? "pick one." : "tick the ones to scan.")</div>
|
||||
<div class="text-muted mt-8" style="font-size:12px">@(Single ? T["sitepicker.hint.loadSingle"] : T["sitepicker.hint.loadMulti"])</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -84,7 +85,7 @@
|
||||
try
|
||||
{
|
||||
_all = (await SiteDiscovery.SearchSitesAsync(Profile)).ToList();
|
||||
if (_all.Count == 0) _error = "No sites returned. The account may lack Sites.Read.All.";
|
||||
if (_all.Count == 0) _error = T["sitepicker.err.noSites"];
|
||||
}
|
||||
catch (Exception ex) { _error = ex.Message; }
|
||||
finally { _loading = false; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@inject IUserContextAccessor UserContext
|
||||
@inject TranslationSource T
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
@using SharepointToolbox.Web.Services.Session
|
||||
|
||||
@@ -17,7 +18,7 @@ else
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
You have <strong>read-only</strong> access (Tech-N0). Contact an Admin to request write access.
|
||||
@T["writeguard.readonly.before"] <strong>@T["writeguard.readonly.emphasis"]</strong> @T["writeguard.readonly.after"]
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user