Implement editing of DtStart, CalculatedEnd and Description
This commit is contained in:
parent
378b6a5e65
commit
b7f6e83071
6 changed files with 167 additions and 39 deletions
|
@ -5,6 +5,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -13,6 +14,7 @@
|
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
|
||||
<PackageReference Include="NodaTime" Version="3.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -12,6 +12,21 @@ namespace BlazorApp.Data
|
|||
{
|
||||
public class Calendar
|
||||
{
|
||||
/// <summary>
|
||||
/// Format string used for parsing iCal date-time strings.
|
||||
/// </summary>
|
||||
private const string DateTimeParseFormat = "yyyyMMddTHHmmssK";
|
||||
|
||||
/// <summary>
|
||||
/// Format string used for formatting (but not parsing) iCal date-time strings.
|
||||
/// </summary>
|
||||
public const string DateTimeFormat = "yyyyMMddTHHmmss'Z'";
|
||||
|
||||
/// <summary>
|
||||
/// Format string used for parsing and formatting iCal date strings.
|
||||
/// </summary>
|
||||
private const string DateFormat = "yyyyMMdd";
|
||||
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required] public string Url { get; set; }
|
||||
|
@ -28,7 +43,7 @@ namespace BlazorApp.Data
|
|||
|
||||
private static DateTime _parseDateTime(string s)
|
||||
{
|
||||
var format = s.Contains('T') ? "yyyyMMddTHHmmssK" : "yyyyMMdd";
|
||||
var format = s.Contains('T') ? DateTimeParseFormat : DateFormat;
|
||||
return DateTime.ParseExact(s, format, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
|
@ -256,15 +271,21 @@ namespace BlazorApp.Data
|
|||
// Build a new URL based on the calendar's URL and the event's collection href.
|
||||
var url = new Uri(new Uri(Url), @event.Href);
|
||||
|
||||
var content = new StringContent(@event.ToICal(), Encoding.UTF8, "text/calendar");
|
||||
var iCal = @event.ToICal();
|
||||
var content = new StringContent(iCal, Encoding.UTF8, "text/calendar");
|
||||
var response = await client.PutAsync(url, content);
|
||||
|
||||
// Parse the newly updated event.
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Could not update event: {body}");
|
||||
return @event;
|
||||
}
|
||||
|
||||
var events = ParseICal(body);
|
||||
if (events.Count == 0)
|
||||
return null;
|
||||
return @event;
|
||||
var newEvent = events[0];
|
||||
|
||||
// Extract the updated ETag
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using NodaTime;
|
||||
|
||||
namespace BlazorApp.Data
|
||||
{
|
||||
|
@ -17,10 +22,23 @@ namespace BlazorApp.Data
|
|||
public string ETag { get; set; }
|
||||
public string Href { get; set; }
|
||||
|
||||
public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration;
|
||||
public DateTime? CalculatedEnd
|
||||
{
|
||||
get => DtEnd ?? DtStart + Duration;
|
||||
set
|
||||
{
|
||||
if (Duration != null)
|
||||
Duration = DtStart - value;
|
||||
else
|
||||
DtEnd = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static string _escape(string s)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
|
||||
return s
|
||||
.Replace(@"\", @"\\")
|
||||
.Replace(",", @"\,")
|
||||
|
@ -36,24 +54,70 @@ namespace BlazorApp.Data
|
|||
sb.Append("PRODID:-//Sijmen//blazor-calendar//EN\r\n");
|
||||
sb.Append("BEGIN:VEVENT\r\n");
|
||||
|
||||
var seenNames = new HashSet<string>();
|
||||
|
||||
foreach (var (name, parameters, oldValue) in RawProperties)
|
||||
{
|
||||
sb.Append(name);
|
||||
if (parameters != null)
|
||||
sb.Append($";{parameters}");
|
||||
|
||||
var value = name switch
|
||||
{
|
||||
"SUMMARY" => _escape(Summary),
|
||||
"DESCRIPTION" => _escape(Description),
|
||||
"DTSTART" => FormatDate(parameters, DtStart),
|
||||
"DTEND" => FormatDate(parameters, DtEnd),
|
||||
_ => oldValue
|
||||
};
|
||||
|
||||
sb.Append($":{value}\r\n");
|
||||
if (value != null)
|
||||
{
|
||||
sb.Append(name);
|
||||
if (parameters != null)
|
||||
sb.Append($";{parameters}");
|
||||
sb.Append($":{value}\r\n");
|
||||
}
|
||||
|
||||
seenNames.Add(name);
|
||||
}
|
||||
|
||||
// If one of the event's properties did not exist yet, add it to the end.
|
||||
if (!seenNames.Contains("SUMMARY") && !string.IsNullOrWhiteSpace(Summary))
|
||||
sb.Append($"SUMMARY:{_escape(Summary)}\r\n");
|
||||
if (!seenNames.Contains("DESCRIPTION") && !string.IsNullOrWhiteSpace(Description))
|
||||
sb.Append($"DESCRIPTION:{_escape(Description)}\r\n");
|
||||
if (!seenNames.Contains("DTSTART") && DtStart != null)
|
||||
sb.Append($"DTSTART:{DtStart?.ToUniversalTime().ToString(Calendar.DateTimeFormat)}\r\n");
|
||||
if (!seenNames.Contains("DTEND") && !seenNames.Contains("DURATION") && CalculatedEnd != null)
|
||||
sb.Append($"DTEND:{DtStart?.ToUniversalTime().ToString(Calendar.DateTimeFormat)}\r\n");
|
||||
|
||||
sb.Append("END:VEVENT\r\n");
|
||||
sb.Append("END:VCALENDAR\r\n");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string FormatDate(string parameters, DateTime? dateTime)
|
||||
{
|
||||
if (dateTime == null)
|
||||
return null;
|
||||
|
||||
DateTimeZone timezone;
|
||||
if (parameters != null)
|
||||
{
|
||||
// If there's parameters, see if we can find a timezone, or default to UTC.
|
||||
var parameterList = parameters.Split('=');
|
||||
var timezoneIana =
|
||||
Array.Find(parameterList, s => s.StartsWith("TZID="))?[5..] ?? "UTC";
|
||||
timezone = DateTimeZoneProviders.Tzdb[timezoneIana];
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there's no parameters, default to UTC.
|
||||
timezone = DateTimeZone.Utc;
|
||||
}
|
||||
|
||||
// Convert the datetime to the used timezone, to avoid changing it.
|
||||
return Instant
|
||||
.FromDateTimeUtc(((DateTime) dateTime).ToUniversalTime())
|
||||
.InZone(timezone)
|
||||
.ToString(Calendar.DateTimeFormat, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
@page "/calendars/{calendarId:int}/events/{eventUid}"
|
||||
@using System.Globalization
|
||||
@inject Data.CalendarService _calendarService
|
||||
@inject NavigationManager _navigationManager
|
||||
|
||||
|
@ -14,41 +15,69 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
@if (!_editTitle)
|
||||
<h1>Event</h1>
|
||||
|
||||
<div>
|
||||
<strong>Summary: </strong>
|
||||
@if (_editing != Editing.Summary)
|
||||
{
|
||||
<span class="property-editable" @onclick="() => _editing = Editing.Summary">
|
||||
@_event.Summary
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>
|
||||
<input @bind="@_event.Summary"/>
|
||||
<button @onclick="UpdateEvent">Save</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Start: </strong>
|
||||
@if (_editing != Editing.DtStart)
|
||||
{
|
||||
<span class="property-editable" @onclick="() => _editing = Editing.DtStart">
|
||||
@(_event.DtStart?.ToString(DtFormat) ?? "Unknown")
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input @bind="@_event.DtStart" @bind:format="@DtFormat"/>
|
||||
<button @onclick="UpdateEvent">Save</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>End: </strong>
|
||||
@if (_editing != Editing.CalculatedEnd)
|
||||
{
|
||||
<span class="property-editable" @onclick="() => _editing = Editing.CalculatedEnd">
|
||||
@(_event.CalculatedEnd?.ToString(DtFormat) ?? "Unknown")
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input @bind="@_event.CalculatedEnd" @bind:format="@DtFormat"/>
|
||||
<button @onclick="UpdateEvent">Save</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_editing != Editing.Description)
|
||||
{
|
||||
<h1 class="event-summary">
|
||||
<span @onclick="() => _editTitle = true">@_event.Summary</span>
|
||||
</h1>
|
||||
<p class="event-description" @onclick="() => _editing = Editing.Description">
|
||||
@(_event.Description ?? "No description")
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input @bind="@_event.Summary">
|
||||
<button @onclick="UpdateEvent">Save</button>
|
||||
}
|
||||
|
||||
<div>
|
||||
<strong>ETag:</strong> @_event.ETag
|
||||
</div>
|
||||
|
||||
@if (_event.DtStart != null)
|
||||
{
|
||||
<div>
|
||||
<strong>Start:</strong> @_event.DtStart
|
||||
<textarea rows="30" cols="100" @bind="@_event.Description"></textarea>
|
||||
<button @onclick="UpdateEvent">Save</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_event.CalculatedEnd != null)
|
||||
{
|
||||
<div>
|
||||
<strong>End:</strong> @_event.CalculatedEnd
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_event.Description != null)
|
||||
{
|
||||
<p class="event-description">@_event.Description</p>
|
||||
}
|
||||
|
||||
@if (!_confirmDelete)
|
||||
{
|
||||
<button class="btn btn-outline-danger" @onclick="DeleteEvent">Delete</button>
|
||||
|
@ -67,13 +96,24 @@ else
|
|||
[Parameter]
|
||||
public string EventUid { get; set; }
|
||||
|
||||
const string DtFormat = "dd-MM-yyyy HH:mm:ss";
|
||||
|
||||
private Data.Calendar _calendar;
|
||||
private Data.Event _event;
|
||||
|
||||
private bool _notFound;
|
||||
private bool _editTitle;
|
||||
private bool _confirmDelete;
|
||||
|
||||
private enum Editing
|
||||
{
|
||||
Summary,
|
||||
DtStart,
|
||||
CalculatedEnd,
|
||||
Description,
|
||||
}
|
||||
|
||||
private Editing? _editing;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_calendar = await Data.CalendarService.GetCalendarById(CalendarId);
|
||||
|
@ -84,6 +124,7 @@ else
|
|||
private async Task UpdateEvent()
|
||||
{
|
||||
_event = await _calendar.UpdateEvent(_event);
|
||||
_editing = null;
|
||||
}
|
||||
|
||||
private async Task DeleteEvent()
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.event-summary > span:hover {
|
||||
.property-editable:hover {
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
|
@ -20,7 +20,7 @@ namespace BlazorApp
|
|||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddRazorPages();
|
||||
services.AddRazorPages().AddRazorRuntimeCompilation();
|
||||
services.AddServerSideBlazor();
|
||||
services.AddSingleton<CalendarService>();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue