|
|
|
@@ -0,0 +1,165 @@
|
|
|
|
|
@page "/facts"
|
|
|
|
|
@rendermode InteractiveServer
|
|
|
|
|
@using Microsoft.AspNetCore.Components.Authorization
|
|
|
|
|
@using Wiki.Application.Facts
|
|
|
|
|
@inject DepositFactUseCase Deposit
|
|
|
|
|
@inject NoteQueries Notes
|
|
|
|
|
@inject TimeProvider Clock
|
|
|
|
|
@inject ISnackbar Snackbar
|
|
|
|
|
|
|
|
|
|
<PageTitle>Facts</PageTitle>
|
|
|
|
|
|
|
|
|
|
<MudText Typo="Typo.h4" GutterBottom="true">Facts</MudText>
|
|
|
|
|
<MudText Typo="Typo.body2" Class="mb-4" Color="Color.Secondary">
|
|
|
|
|
A fact is one verified observation with its provenance. It lands verbatim in <code>raw/</code> under your name and the nightly distill turns it into wiki notes. Raw holds observations only, never hypotheses.
|
|
|
|
|
</MudText>
|
|
|
|
|
|
|
|
|
|
@if (_user is null)
|
|
|
|
|
{
|
|
|
|
|
<MudAlert Severity="Severity.Warning" Class="mb-4">Not signed in. Facts are attributed to the person depositing them, so this page needs the Authentik sign-in at wiki.renner.uno.</MudAlert>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<MudPaper Class="pa-4 mb-6">
|
|
|
|
|
<MudForm @ref="_form" @bind-IsValid="_valid">
|
|
|
|
|
<MudGrid>
|
|
|
|
|
<MudItem xs="12" md="8">
|
|
|
|
|
<MudTextField T="string" @bind-Value="_title" Label="Title" Variant="Variant.Outlined" Required="true" RequiredError="title is required" Immediate="true" Disabled="@_running" />
|
|
|
|
|
</MudItem>
|
|
|
|
|
<MudItem xs="12" md="4">
|
|
|
|
|
<MudDatePicker Label="Observed on" @bind-Date="_observedDate" Variant="Variant.Outlined" Disabled="@_running" />
|
|
|
|
|
</MudItem>
|
|
|
|
|
<MudItem xs="12" md="8">
|
|
|
|
|
<MudTextField T="string" @bind-Value="_source" Label="Source" HelperText="where the fact was observed or quoted from: a command, a URL, a document" Variant="Variant.Outlined" Required="true" RequiredError="source is required" Immediate="true" Disabled="@_running" />
|
|
|
|
|
</MudItem>
|
|
|
|
|
<MudItem xs="12" md="4">
|
|
|
|
|
<MudTextField T="string" @bind-Value="_tags" Label="Tags" HelperText="comma separated" Variant="Variant.Outlined" Immediate="true" Disabled="@_running" />
|
|
|
|
|
</MudItem>
|
|
|
|
|
<MudItem xs="12">
|
|
|
|
|
<MudTextField T="string" @bind-Value="_content" Label="Observation" Lines="8" Variant="Variant.Outlined" Required="true" Immediate="true" Disabled="@_running"
|
|
|
|
|
Validation="@((string s) => s is not null && s.Trim().Length >= RawSource.MinimumContentLength ? null : $"at least {RawSource.MinimumContentLength} characters of observation")"
|
|
|
|
|
Counter="0" HelperText="@($"{_content?.Trim().Length ?? 0} characters; markdown is fine")" />
|
|
|
|
|
</MudItem>
|
|
|
|
|
</MudGrid>
|
|
|
|
|
</MudForm>
|
|
|
|
|
<MudStack Row Justify="Justify.FlexEnd" AlignItems="AlignItems.Center" Spacing="2" Class="mt-2">
|
|
|
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">captured_by: @_user</MudText>
|
|
|
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="DepositAsync" Disabled="@(_running || !_valid)">Deposit</MudButton>
|
|
|
|
|
</MudStack>
|
|
|
|
|
</MudPaper>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (_running)
|
|
|
|
|
{
|
|
|
|
|
<MudProgressLinear Indeterminate="true" Class="mb-4" />
|
|
|
|
|
}
|
|
|
|
|
@if (_error is not null)
|
|
|
|
|
{
|
|
|
|
|
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
|
|
|
|
|
}
|
|
|
|
|
@if (_receipt is not null)
|
|
|
|
|
{
|
|
|
|
|
<MudAlert Severity="Severity.Success" Class="mb-4">
|
|
|
|
|
Stored as <MudLink Href="@($"raw/{Slug(_receipt.Path)}")">@_receipt.Path</MudLink> (commit @_receipt.Commit[..Math.Min(8, _receipt.Commit.Length)]).
|
|
|
|
|
</MudAlert>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<MudText Typo="Typo.subtitle1" Class="mb-2">Raw sources awaiting distillation</MudText>
|
|
|
|
|
@if (_raw is null)
|
|
|
|
|
{
|
|
|
|
|
<MudProgressLinear Indeterminate="true" />
|
|
|
|
|
}
|
|
|
|
|
else if (_raw.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Nothing queued.</MudText>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<MudSimpleTable Dense="true" Hover="true">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr><th>Captured</th><th>Title</th><th>Source</th></tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
@foreach (var r in _raw.OrderByDescending(r => r.Captured).ThenByDescending(r => r.Slug).Take(30))
|
|
|
|
|
{
|
|
|
|
|
<tr>
|
|
|
|
|
<td>@(r.Captured?.ToString("yyyy-MM-dd") ?? "")</td>
|
|
|
|
|
<td><MudLink Href="@($"raw/{r.Slug}")">@r.Title</MudLink></td>
|
|
|
|
|
<td><MudText Typo="Typo.caption" Color="Color.Secondary">@r.Source</MudText></td>
|
|
|
|
|
</tr>
|
|
|
|
|
}
|
|
|
|
|
</tbody>
|
|
|
|
|
</MudSimpleTable>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@code {
|
|
|
|
|
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
|
|
|
|
|
|
|
|
|
private MudForm? _form;
|
|
|
|
|
private bool _valid;
|
|
|
|
|
private bool _running;
|
|
|
|
|
private string? _user;
|
|
|
|
|
private string? _title;
|
|
|
|
|
private string? _source;
|
|
|
|
|
private string? _tags;
|
|
|
|
|
private string? _content;
|
|
|
|
|
private DateTime? _observedDate;
|
|
|
|
|
private string? _error;
|
|
|
|
|
private FactReceipt? _receipt;
|
|
|
|
|
private IReadOnlyList<RawQueueEntry>? _raw;
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
|
|
|
|
_observedDate = Clock.GetUtcNow().Date;
|
|
|
|
|
if (AuthState is not null)
|
|
|
|
|
{
|
|
|
|
|
var state = await AuthState;
|
|
|
|
|
_user = state.User.Identity?.IsAuthenticated == true ? state.User.Identity.Name : null;
|
|
|
|
|
}
|
|
|
|
|
await LoadRawAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadRawAsync() => _raw = await Notes.RawQueueAsync(CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
private async Task DepositAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_running || _user is null) return;
|
|
|
|
|
_running = true;
|
|
|
|
|
_error = null;
|
|
|
|
|
_receipt = null;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var observed = _observedDate is { } d ? new DateTimeOffset(DateTime.SpecifyKind(d, DateTimeKind.Utc)) : Clock.GetUtcNow();
|
|
|
|
|
var fact = new RawSource(
|
|
|
|
|
_title?.Trim() ?? string.Empty,
|
|
|
|
|
_source?.Trim() ?? string.Empty,
|
|
|
|
|
_content ?? string.Empty,
|
|
|
|
|
observed,
|
|
|
|
|
(_tags ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
|
|
|
|
|
var errors = fact.Validate();
|
|
|
|
|
if (errors.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
_error = string.Join("; ", errors);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_receipt = await Deposit.DepositAsync(fact, new Identity(_user, IdentityKind.Human), CancellationToken.None);
|
|
|
|
|
Snackbar.Add($"stored {_receipt.Path}", Severity.Success);
|
|
|
|
|
_title = _source = _tags = _content = null;
|
|
|
|
|
await LoadRawAsync();
|
|
|
|
|
}
|
|
|
|
|
catch (HttpRequestException e)
|
|
|
|
|
{
|
|
|
|
|
_error = $"The store did not accept the fact: {e.Message}";
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_running = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string Slug(string path)
|
|
|
|
|
{
|
|
|
|
|
var file = path[(path.LastIndexOf('/') + 1)..];
|
|
|
|
|
return file.EndsWith(".md", StringComparison.Ordinal) ? file[..^3] : file;
|
|
|
|
|
}
|
|
|
|
|
}
|