Allow for editing event titles:
- Add frontend for editing - Save the original iCal properties when loading - Use these to closely reproduce the original iCal for PUT'ing
This commit is contained in:
parent
024c8282b7
commit
3c2c33736b
6 changed files with 173 additions and 39 deletions
|
@ -15,4 +15,10 @@
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Pages\Event.razor.scss">
|
||||||
|
<DependentUpon>Event.razor</DependentUpon>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -3,24 +3,13 @@ using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
|
||||||
namespace BlazorApp.Data
|
namespace BlazorApp.Data
|
||||||
{
|
{
|
||||||
public class Event
|
|
||||||
{
|
|
||||||
public string Uid { get; set; }
|
|
||||||
public string Summary { get; set; }
|
|
||||||
public DateTime? DtStart { get; set; }
|
|
||||||
public DateTime? DtEnd { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public TimeSpan? Duration { get; set; }
|
|
||||||
|
|
||||||
public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Calendar
|
public class Calendar
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
@ -127,17 +116,22 @@ namespace BlazorApp.Data
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name == "END" && value == "VEVENT")
|
||||||
|
{
|
||||||
|
// End of the current event, add it to the list
|
||||||
|
events.Add(@event);
|
||||||
|
@event = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters = left.Length == 2 ? left[1] : null;
|
||||||
|
|
||||||
|
// Add property to the RawProperties list.
|
||||||
|
// Used for rebuilding the full iCal for updating the event on the server.
|
||||||
|
@event.RawProperties.Add((name, parameters, value));
|
||||||
|
|
||||||
switch (name)
|
switch (name)
|
||||||
{
|
{
|
||||||
case "END":
|
|
||||||
if (value == "VEVENT")
|
|
||||||
{
|
|
||||||
// End of the current event, add it to the list
|
|
||||||
events.Add(@event);
|
|
||||||
@event = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "UID":
|
case "UID":
|
||||||
@event.Uid = value;
|
@event.Uid = value;
|
||||||
break;
|
break;
|
||||||
|
@ -162,18 +156,8 @@ namespace BlazorApp.Data
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static List<Event> _filterSortEvents(List<Event> events)
|
||||||
* <summary>Retrieves and parses all events from the CalDav server.</summary>
|
|
||||||
*/
|
|
||||||
public async Task<List<Event>> GetEvents()
|
|
||||||
{
|
{
|
||||||
var client = new HttpClient();
|
|
||||||
if (AuthorizationHeader != null)
|
|
||||||
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
|
|
||||||
|
|
||||||
var body = await client.GetStringAsync(Url);
|
|
||||||
var events = ParseICal(body);
|
|
||||||
|
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
events = events.FindAll(@event => @event.DtStart > now || @event.DtEnd > now);
|
events = events.FindAll(@event => @event.DtStart > now || @event.DtEnd > now);
|
||||||
|
|
||||||
|
@ -190,6 +174,26 @@ namespace BlazorApp.Data
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <summary>Retrieves and parses all events from the CalDav server.</summary>
|
||||||
|
*/
|
||||||
|
public async Task<List<Event>> GetEvents()
|
||||||
|
{
|
||||||
|
var client = new HttpClient();
|
||||||
|
if (AuthorizationHeader != null)
|
||||||
|
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
|
||||||
|
|
||||||
|
var response = await client.GetAsync(Url);
|
||||||
|
var eTag = response.Headers.ETag?.Tag;
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var events = ParseICal(body);
|
||||||
|
foreach (var @event in events)
|
||||||
|
@event.ETag = eTag;
|
||||||
|
|
||||||
|
return _filterSortEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <summary>Retrieves and parses an event with UID <code>uid</code> from the CalDav server.</summary>
|
* <summary>Retrieves and parses an event with UID <code>uid</code> from the CalDav server.</summary>
|
||||||
*/
|
*/
|
||||||
|
@ -199,6 +203,7 @@ namespace BlazorApp.Data
|
||||||
if (AuthorizationHeader != null)
|
if (AuthorizationHeader != null)
|
||||||
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
|
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
|
||||||
|
|
||||||
|
// Request the event with UID `uid`.
|
||||||
var request = new HttpRequestMessage(new HttpMethod("REPORT"), Url);
|
var request = new HttpRequestMessage(new HttpMethod("REPORT"), Url);
|
||||||
request.Headers.Add("Depth", "1");
|
request.Headers.Add("Depth", "1");
|
||||||
|
|
||||||
|
@ -225,13 +230,53 @@ namespace BlazorApp.Data
|
||||||
var result = await client.SendAsync(request);
|
var result = await client.SendAsync(request);
|
||||||
var body = await result.Content.ReadAsStreamAsync();
|
var body = await result.Content.ReadAsStreamAsync();
|
||||||
|
|
||||||
|
// Parse the received XML
|
||||||
var reader = new XmlTextReader(body);
|
var reader = new XmlTextReader(body);
|
||||||
var document = new XmlDocument();
|
var document = new XmlDocument();
|
||||||
document.Load(reader);
|
document.Load(reader);
|
||||||
|
|
||||||
var node = document.GetElementsByTagName("C:calendar-data")[0]?.InnerText;
|
// Extract and parse the ICal data
|
||||||
var events = ParseICal(node);
|
var dataNode = document.GetElementsByTagName("C:calendar-data")[0]?.InnerText;
|
||||||
return events.Count > 0 ? events[0] : null;
|
var events = ParseICal(dataNode);
|
||||||
|
if (events.Count == 0)
|
||||||
|
return null;
|
||||||
|
var @event = events[0];
|
||||||
|
|
||||||
|
// Extract the ETag and event collection href.
|
||||||
|
@event.ETag = document.GetElementsByTagName("getetag")[0]?.InnerText;
|
||||||
|
@event.Href = document.GetElementsByTagName("href")[0]?.InnerText;
|
||||||
|
|
||||||
|
return @event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Event> UpdateEvent(Event @event)
|
||||||
|
{
|
||||||
|
var client = new HttpClient();
|
||||||
|
if (AuthorizationHeader != null)
|
||||||
|
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
|
||||||
|
|
||||||
|
// Setting If-Match header to the ETag is required when updating an event.
|
||||||
|
if (@event.ETag != null)
|
||||||
|
client.DefaultRequestHeaders.IfMatch.Add(new EntityTagHeaderValue(@event.ETag));
|
||||||
|
|
||||||
|
// 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 response = await client.PutAsync(url, content);
|
||||||
|
|
||||||
|
// Parse the newly updated event.
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var events = ParseICal(body);
|
||||||
|
if (events.Count == 0)
|
||||||
|
return null;
|
||||||
|
var newEvent = events[0];
|
||||||
|
|
||||||
|
// Extract the updated ETag
|
||||||
|
newEvent.ETag = response.Headers.ETag?.Tag;
|
||||||
|
|
||||||
|
return newEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
59
BlazorApp/Data/Event.cs
Normal file
59
BlazorApp/Data/Event.cs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BlazorApp.Data
|
||||||
|
{
|
||||||
|
public class Event
|
||||||
|
{
|
||||||
|
public string Uid { get; set; }
|
||||||
|
public string Summary { get; set; }
|
||||||
|
public DateTime? DtStart { get; set; }
|
||||||
|
public DateTime? DtEnd { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public TimeSpan? Duration { get; set; }
|
||||||
|
|
||||||
|
public List<(string, string, string)> RawProperties { get; } = new();
|
||||||
|
public string ETag { get; set; }
|
||||||
|
public string Href { get; set; }
|
||||||
|
|
||||||
|
public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration;
|
||||||
|
|
||||||
|
private static string _escape(string s)
|
||||||
|
{
|
||||||
|
return s
|
||||||
|
.Replace(@"\", @"\\")
|
||||||
|
.Replace(",", @"\,")
|
||||||
|
.Replace(";", @"\;")
|
||||||
|
.Replace("\n", @"\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToICal()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append("BEGIN:VCALENDAR\r\n");
|
||||||
|
sb.Append("VERSION:2.0\r\n");
|
||||||
|
sb.Append("PRODID:-//Sijmen//blazor-calendar//EN\r\n");
|
||||||
|
sb.Append("BEGIN:VEVENT\r\n");
|
||||||
|
|
||||||
|
foreach (var (name, parameters, oldValue) in RawProperties)
|
||||||
|
{
|
||||||
|
sb.Append(name);
|
||||||
|
if (parameters != null)
|
||||||
|
sb.Append($";{parameters}");
|
||||||
|
|
||||||
|
var value = name switch
|
||||||
|
{
|
||||||
|
"SUMMARY" => _escape(Summary),
|
||||||
|
_ => oldValue
|
||||||
|
};
|
||||||
|
|
||||||
|
sb.Append($":{value}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append("END:VEVENT\r\n");
|
||||||
|
sb.Append("END:VCALENDAR\r\n");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,21 @@
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<h1>@_event.Summary</h1>
|
@if (!_editTitle)
|
||||||
|
{
|
||||||
|
<h1 class="event-summary">
|
||||||
|
<span @onclick="() => _editTitle = true">@_event.Summary</span>
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input @bind="@_event.Summary">
|
||||||
|
<button @onclick="UpdateEvent">Save</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>ETag:</strong> @_event.ETag
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (_event.DtStart != null)
|
@if (_event.DtStart != null)
|
||||||
{
|
{
|
||||||
|
@ -40,11 +54,18 @@ else
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string EventUid { get; set; }
|
public string EventUid { get; set; }
|
||||||
|
|
||||||
|
private Data.Calendar _calendar;
|
||||||
private Data.Event _event;
|
private Data.Event _event;
|
||||||
|
private bool _editTitle;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
var calendar = await Data.CalendarService.GetCalendarById(CalendarId);
|
_calendar = await Data.CalendarService.GetCalendarById(CalendarId);
|
||||||
_event = await calendar.GetEventByUid(EventUid);
|
_event = await _calendar.GetEventByUid(EventUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void UpdateEvent()
|
||||||
|
{
|
||||||
|
_event = await _calendar.UpdateEvent(_event);
|
||||||
}
|
}
|
||||||
}
|
}
|
3
BlazorApp/Pages/Event.razor.css
Normal file
3
BlazorApp/Pages/Event.razor.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.event-summary > span:hover {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
"BlazorApp": {
|
"BlazorApp": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": "true",
|
"dotnetRunMessages": "true",
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://localhost:5000",
|
"applicationUrl": "http://localhost:5000",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|
Loading…
Reference in a new issue