Add birthday calendar creation to frontend

This commit is contained in:
Lennart
2025-12-05 14:50:02 +01:00
parent af239e34bf
commit 57275a10b4
7 changed files with 270 additions and 10 deletions

View File

@@ -0,0 +1,102 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { escapeXml } from ".";
import { getTimezones } from "./timezones.ts";
@customElement("create-birthday-calendar-form")
export class CreateCalendarForm extends LitElement {
protected override createRenderRoot() {
return this
}
@property()
principal: string = ''
@property()
addr_id: string = ''
@property()
displayname: string = ''
@property()
description: string = ''
@property()
color: string = ''
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
@property()
timezones: Array<String> = []
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
<dialog ${ref(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${e => this.description = e.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${e => this.color = e.target.value} />
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
}
async submit(e: SubmitEvent) {
e.preventDefault()
if (!this.addr_id) {
alert("Empty id")
return
}
if (!this.displayname) {
alert("Empty displayname")
return
}
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
method: 'MKCOL',
headers: {
'Content-Type': 'application/xml'
},
body: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT" />
</CAL:supported-calendar-component-set>
</prop>
</set>
</mkcol>
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}
}
declare global {
interface HTMLElementTagNameMap {
'create-calendar-form': CreateCalendarForm
}
}

View File

@@ -14,6 +14,7 @@ export default defineConfig({
rollupOptions: {
input: [
"lib/create-birthday-calendar-form.ts",
"lib/create-calendar-form.ts",
"lib/edit-calendar-form.ts",
"lib/import-calendar-form.ts",

View File

@@ -0,0 +1,122 @@
import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let CreateCalendarForm = class extends i {
constructor() {
super(...arguments);
this.principal = "";
this.addr_id = "";
this.displayname = "";
this.description = "";
this.color = "";
this.dialog = e();
this.form = e();
this.timezones = [];
}
createRenderRoot() {
return this;
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
<dialog ${n(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${n(this.form)}>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(e2) {
e2.preventDefault();
if (!this.addr_id) {
alert("Empty id");
return;
}
if (!this.displayname) {
alert("Empty displayname");
return;
}
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
method: "MKCOL",
headers: {
"Content-Type": "application/xml"
},
body: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT" />
</CAL:supported-calendar-component-set>
</prop>
</set>
</mkcol>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
};
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "principal", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "addr_id", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "displayname", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "color", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "timezones", 2);
CreateCalendarForm = __decorateClass([
t("create-birthday-calendar-form")
], CreateCalendarForm);
export {
CreateCalendarForm
};

View File

@@ -1,6 +1,6 @@
<h2>{{user.id }}'s Addressbooks</h2>
<ul class="collection-list">
{% for (meta, addressbook) in addressbooks %}
{% for (meta, birthday_cal, addressbook) in addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner">
@@ -24,9 +24,17 @@
></edit-addressbook-form>
<delete-button trash
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
{% if !birthday_cal.is_some() %}
<create-birthday-calendar-form
principal="{{ addressbook.principal }}"
addr_id="{{ addressbook.id }}"
displayname="{{ addressbook.displayname.as_deref().unwrap_or_default() }} birthdays"
></create-birthday-calendar-form>
{% endif %}
</div>
<div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
{{ birthday_cal.is_some() }}
</div>
</div>
</li>
@@ -37,7 +45,7 @@
{%if !deleted_addressbooks.is_empty() %}
<h3>Deleted Addressbooks</h3>
<ul class="collection-list">
{% for (meta, addressbook) in deleted_addressbooks %}
{% for (meta, birthday_cal, addressbook) in deleted_addressbooks %}
<li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner">

View File

@@ -5,6 +5,7 @@
<script>
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
</script>
<script type="module" src="/frontend/assets/js/create-birthday-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>

View File

@@ -12,7 +12,7 @@ use http::{Method, StatusCode};
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
use rustical_store::{
AddressbookStore, CalendarStore,
AddressbookStore, CalendarStore, PrefixedCalendarStore,
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
};
use std::sync::Arc;
@@ -39,7 +39,11 @@ use crate::routes::{
#[cfg(not(feature = "dev"))]
use assets::{Assets, EmbedService};
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
pub fn frontend_router<
AP: AuthenticationProvider,
CS: CalendarStore,
AS: AddressbookStore + PrefixedCalendarStore,
>(
prefix: &'static str,
auth_provider: Arc<AP>,
cal_store: Arc<CS>,

View File

@@ -1,10 +1,12 @@
use std::sync::Arc;
use askama::Template;
use askama_web::WebTemplate;
use axum::{Extension, extract::Path, response::IntoResponse};
use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
use rustical_store::{
Addressbook, AddressbookStore, Calendar, CollectionMetadata, PrefixedCalendarStore,
auth::Principal,
};
use std::sync::Arc;
use crate::pages::user::{Section, UserPage};
@@ -18,11 +20,11 @@ impl Section for AddressbooksSection {
#[template(path = "components/sections/addressbooks_section.html")]
pub struct AddressbooksSection {
pub user: Principal,
pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
pub addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
pub deleted_addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
}
pub async fn route_addressbooks<AS: AddressbookStore>(
pub async fn route_addressbooks<AS: AddressbookStore + PrefixedCalendarStore>(
Path(user_id): Path<String>,
Extension(addr_store): Extension<Arc<AS>>,
user: Principal,
@@ -43,22 +45,42 @@ pub async fn route_addressbooks<AS: AddressbookStore>(
let mut addressbook_infos = vec![];
for addressbook in addressbooks {
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
let birthday_cal = match addr_store
.get_calendar(&addressbook.principal, &birthday_id, true)
.await
{
Ok(cal) => Some(cal),
Err(rustical_store::Error::NotFound) => None,
err => Some(err.unwrap()),
};
addressbook_infos.push((
addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id)
.await
.unwrap(),
birthday_cal,
addressbook,
));
}
let mut deleted_addressbook_infos = vec![];
for addressbook in deleted_addressbooks {
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
let birthday_cal = match addr_store
.get_calendar(&addressbook.principal, &birthday_id, true)
.await
{
Ok(cal) => Some(cal),
Err(rustical_store::Error::NotFound) => None,
err => Some(err.unwrap()),
};
deleted_addressbook_infos.push((
addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id)
.await
.unwrap(),
birthday_cal,
addressbook,
));
}