Implement editing of DtStart, CalculatedEnd and Description

This commit is contained in:
Sijmen 2021-07-08 12:41:20 +02:00
parent 378b6a5e65
commit b7f6e83071
6 changed files with 167 additions and 39 deletions

View file

@ -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>

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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()

View file

@ -1,3 +1,3 @@
.event-summary > span:hover {
.property-editable:hover {
border: 1px solid rgba(0, 0, 0, 0.2);
}

View file

@ -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>();
}