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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
@ -13,6 +14,7 @@
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
|
||||||
|
<PackageReference Include="NodaTime" Version="3.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -12,6 +12,21 @@ namespace BlazorApp.Data
|
||||||
{
|
{
|
||||||
public class Calendar
|
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; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
[Required] public string Url { get; set; }
|
[Required] public string Url { get; set; }
|
||||||
|
@ -28,7 +43,7 @@ namespace BlazorApp.Data
|
||||||
|
|
||||||
private static DateTime _parseDateTime(string s)
|
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);
|
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.
|
// 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 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);
|
var response = await client.PutAsync(url, content);
|
||||||
|
|
||||||
// Parse the newly updated event.
|
// Parse the newly updated event.
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Could not update event: {body}");
|
||||||
|
return @event;
|
||||||
|
}
|
||||||
|
|
||||||
var events = ParseICal(body);
|
var events = ParseICal(body);
|
||||||
if (events.Count == 0)
|
if (events.Count == 0)
|
||||||
return null;
|
return @event;
|
||||||
var newEvent = events[0];
|
var newEvent = events[0];
|
||||||
|
|
||||||
// Extract the updated ETag
|
// Extract the updated ETag
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
namespace BlazorApp.Data
|
namespace BlazorApp.Data
|
||||||
{
|
{
|
||||||
|
@ -17,10 +22,23 @@ namespace BlazorApp.Data
|
||||||
public string ETag { get; set; }
|
public string ETag { get; set; }
|
||||||
public string Href { 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)
|
private static string _escape(string s)
|
||||||
{
|
{
|
||||||
|
if (s == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
return s
|
return s
|
||||||
.Replace(@"\", @"\\")
|
.Replace(@"\", @"\\")
|
||||||
.Replace(",", @"\,")
|
.Replace(",", @"\,")
|
||||||
|
@ -36,24 +54,70 @@ namespace BlazorApp.Data
|
||||||
sb.Append("PRODID:-//Sijmen//blazor-calendar//EN\r\n");
|
sb.Append("PRODID:-//Sijmen//blazor-calendar//EN\r\n");
|
||||||
sb.Append("BEGIN:VEVENT\r\n");
|
sb.Append("BEGIN:VEVENT\r\n");
|
||||||
|
|
||||||
|
var seenNames = new HashSet<string>();
|
||||||
|
|
||||||
foreach (var (name, parameters, oldValue) in RawProperties)
|
foreach (var (name, parameters, oldValue) in RawProperties)
|
||||||
{
|
{
|
||||||
sb.Append(name);
|
|
||||||
if (parameters != null)
|
|
||||||
sb.Append($";{parameters}");
|
|
||||||
|
|
||||||
var value = name switch
|
var value = name switch
|
||||||
{
|
{
|
||||||
"SUMMARY" => _escape(Summary),
|
"SUMMARY" => _escape(Summary),
|
||||||
|
"DESCRIPTION" => _escape(Description),
|
||||||
|
"DTSTART" => FormatDate(parameters, DtStart),
|
||||||
|
"DTEND" => FormatDate(parameters, DtEnd),
|
||||||
_ => oldValue
|
_ => 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:VEVENT\r\n");
|
||||||
sb.Append("END:VCALENDAR\r\n");
|
sb.Append("END:VCALENDAR\r\n");
|
||||||
return sb.ToString();
|
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}"
|
@page "/calendars/{calendarId:int}/events/{eventUid}"
|
||||||
|
@using System.Globalization
|
||||||
@inject Data.CalendarService _calendarService
|
@inject Data.CalendarService _calendarService
|
||||||
@inject NavigationManager _navigationManager
|
@inject NavigationManager _navigationManager
|
||||||
|
|
||||||
|
@ -14,41 +15,69 @@
|
||||||
}
|
}
|
||||||
else
|
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">
|
<p class="event-description" @onclick="() => _editing = Editing.Description">
|
||||||
<span @onclick="() => _editTitle = true">@_event.Summary</span>
|
@(_event.Description ?? "No description")
|
||||||
</h1>
|
</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
<input @bind="@_event.Summary">
|
|
||||||
<button @onclick="UpdateEvent">Save</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<strong>ETag:</strong> @_event.ETag
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (_event.DtStart != null)
|
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
<strong>Start:</strong> @_event.DtStart
|
<textarea rows="30" cols="100" @bind="@_event.Description"></textarea>
|
||||||
|
<button @onclick="UpdateEvent">Save</button>
|
||||||
</div>
|
</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)
|
@if (!_confirmDelete)
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-danger" @onclick="DeleteEvent">Delete</button>
|
<button class="btn btn-outline-danger" @onclick="DeleteEvent">Delete</button>
|
||||||
|
@ -67,13 +96,24 @@ else
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string EventUid { get; set; }
|
public string EventUid { get; set; }
|
||||||
|
|
||||||
|
const string DtFormat = "dd-MM-yyyy HH:mm:ss";
|
||||||
|
|
||||||
private Data.Calendar _calendar;
|
private Data.Calendar _calendar;
|
||||||
private Data.Event _event;
|
private Data.Event _event;
|
||||||
|
|
||||||
private bool _notFound;
|
private bool _notFound;
|
||||||
private bool _editTitle;
|
|
||||||
private bool _confirmDelete;
|
private bool _confirmDelete;
|
||||||
|
|
||||||
|
private enum Editing
|
||||||
|
{
|
||||||
|
Summary,
|
||||||
|
DtStart,
|
||||||
|
CalculatedEnd,
|
||||||
|
Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
private Editing? _editing;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_calendar = await Data.CalendarService.GetCalendarById(CalendarId);
|
_calendar = await Data.CalendarService.GetCalendarById(CalendarId);
|
||||||
|
@ -84,6 +124,7 @@ else
|
||||||
private async Task UpdateEvent()
|
private async Task UpdateEvent()
|
||||||
{
|
{
|
||||||
_event = await _calendar.UpdateEvent(_event);
|
_event = await _calendar.UpdateEvent(_event);
|
||||||
|
_editing = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteEvent()
|
private async Task DeleteEvent()
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
.event-summary > span:hover {
|
.property-editable:hover {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
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
|
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddRazorPages();
|
services.AddRazorPages().AddRazorRuntimeCompilation();
|
||||||
services.AddServerSideBlazor();
|
services.AddServerSideBlazor();
|
||||||
services.AddSingleton<CalendarService>();
|
services.AddSingleton<CalendarService>();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue