frontend: improve calendar creation form and fix data binding bugs

This commit is contained in:
Lennart
2025-12-11 19:52:48 +01:00
parent 4b4210b4d7
commit ec28cb9d9a
21 changed files with 292 additions and 135 deletions

View File

@@ -36,27 +36,27 @@ export class CreateAddressbookForm extends LitElement {
<form @submit=${this.submit} ${ref(this.form)}>
<label>
principal (for group addressbooks)
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
<option value=${this.user}>${this.user}</option>
<select .value=${this.user} @change=${e => this.principal = e.target.value}>
<option .value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map(membership => html`
<option value=${membership}>${membership}</option>
<option .value=${membership}>${membership}</option>
`)}
</select>
</label>
<br>
<label>
id
<input type="text" name="id" value=${this.addr_id} @change=${e => this.addr_id = e.target.value} />
<input type="text" .value=${this.addr_id} @change=${e => this.addr_id = e.target.value} />
</label>
<br>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
<input type="text" .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} />
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
</label>
<br>
<button type="submit">Create</button>

View File

@@ -34,17 +34,17 @@ export class CreateCalendarForm extends LitElement {
<form @submit=${this.submit} ${ref(this.form)}>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
<input type="text" .value=${this.displayname} required @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${e => this.description = e.target.value} />
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${e => this.color = e.target.value} />
<input type="color" .value=${this.color} @change=${e => this.color = e.target.value} />
</label>
<br>
<button type="submit">Create</button>

View File

@@ -1,16 +1,30 @@
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 { escapeXml, SVG_ICON_CALENDAR, SVG_ICON_INTERNET } from ".";
import { getTimezones } from "./timezones.ts";
@customElement("create-calendar-form")
export class CreateCalendarForm extends LitElement {
constructor() {
super()
this.resetForm()
this.fetchTimezones()
}
resetForm() {
this.form.value?.reset()
this.principal = this.user
this.cal_id = self.crypto.randomUUID()
this.displayname = ''
this.description = ''
this.timezone_id = ''
this.color = ''
this.isSubscription = false
this.subscriptionUrl = null
this.components = new Set(["VEVENT", "VTODO"])
}
async fetchTimezones() {
this.timezones = await getTimezones()
}
@@ -22,23 +36,23 @@ export class CreateCalendarForm extends LitElement {
@property()
user: string = ''
@property()
principal: string = ''
principal: string
@property()
cal_id: string = self.crypto.randomUUID()
cal_id: string
@property()
displayname: string = ''
displayname: string
@property()
description: string = ''
description: string
@property()
timezone_id: string = ''
timezone_id: string
@property()
color: string = ''
color: string
@property()
isSubscription: boolean = false
isSubscription: boolean
@property()
subscriptionUrl: string = ''
subscriptionUrl: string
@property()
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
components: Set<"VEVENT" | "VTODO" | "VJOURNAL">
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
@@ -47,13 +61,13 @@ export class CreateCalendarForm extends LitElement {
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${ref(this.dialog)}>
<button @click=${e => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${ref(this.dialog)} @close=${e => this.resetForm()}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
principal (for group calendars)
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
<select required value=${this.user} @change=${e => this.principal = e.target.value}>
<option value=${this.user}>${this.user}</option>
${window.rusticalUser.memberships.map(membership => html`
<option value=${membership}>${membership}</option>
@@ -63,17 +77,17 @@ export class CreateCalendarForm extends LitElement {
<br>
<label>
id
<input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
<input type="text" required .value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
</label>
<br>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
<input type="text" required .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Timezone (optional)
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
<select .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
<option value="">No timezone</option>
${this.timezones.map(timezone => html`
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
@@ -83,45 +97,57 @@ export class CreateCalendarForm extends LitElement {
<br>
<label>
Description
<input type="text" name="description" @change=${e => this.description = e.target.value} />
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${e => this.color = e.target.value} />
<input type="color" .value=${this.color} @change=${e => this.color = e.target.value} />
</label>
<br>
<br>
<label>
Calendar is subscription to external calendar
<input type="checkbox" name="is_subscription" @change=${e => this.isSubscription = e.target.checked} />
</label>
<label>Type</label>
<div class="tab-radio">
<label>
<input type="radio" name="type" .checked=${!this.isSubscription} @change=${e => this.isSubscription = false}></input>
${SVG_ICON_CALENDAR}
Calendar
</label>
<label>
<input type="radio" name="type" .checked=${this.isSubscription} @change=${e => this.isSubscription = true}></input>
${SVG_ICON_INTERNET}
webCal Subscription
</label>
</div>
<br>
${this.isSubscription ? html`
<label>
Subscription URL
<input type="text" name="subscription_url" @change=${e => this.subscriptionUrl = e.target.value} />
<input type="text" pattern="https://.*" .required=${this.isSubscription} .value=${this.subscriptionUrl} @change=${e => this.subscriptionUrl = e.target.value} />
</label>
<br>
<br>
`: html``}
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
<label>
Support ${comp}
<input type="checkbox" value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
</label>
<br>
`)}
<label>Components</label>
<div>
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
<label>
Support ${comp}
<input type="checkbox" .value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} .checked=${this.components.has(comp)} />
</label>
<br>
`)}
</div>
<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>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close();}} class="cancel">Cancel</button>
</form>
</dialog>
`
}
async submit(e: SubmitEvent) {
console.log(this.displayname)
e.preventDefault()
if (!this.cal_id) {
alert("Empty id")
@@ -135,6 +161,10 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected")
return
}
if (this.isSubscription && !this.subscriptionUrl) {
alert("Invalid subscription url")
return
}
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
method: 'MKCOL',

View File

@@ -34,12 +34,12 @@ export class EditAddressbookForm extends LitElement {
<form @submit=${this.submit} ${ref(this.form)}>
<label>
Displayname
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
<input type="text" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
</label>
<br>
<button type="submit">Submit</button>

View File

@@ -53,12 +53,12 @@ export class EditCalendarForm extends LitElement {
<form @submit=${this.submit} ${ref(this.form)}>
<label>
Displayname
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
<input type="text" required .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Timezone (optional)
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
<select .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
<option value="">No timezone</option>
${this.timezones.map(timezone => html`
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
@@ -68,18 +68,18 @@ export class EditCalendarForm extends LitElement {
<br>
<label>
Description
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" .value=${this.color} @change=${e => this.color = e.target.value} />
<input type="color" .value=${this.color} @change=${e => this.color = e.target.value} />
</label>
<br>
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
<label>
Support ${comp}
<input type="checkbox" value=${comp} ?checked=${this.components.has(comp)} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
<input type="checkbox" .value=${comp} ?checked=${this.components.has(comp)} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
</label>
<br>
`)}

View File

@@ -1,3 +1,5 @@
import {svg} from 'lit'
export function escapeXml(unsafe: string): string {
return unsafe.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
@@ -5,3 +7,23 @@ export function escapeXml(unsafe: string): string {
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
export const SVG_ICON_CALENDAR = svg`<!-- Adapted from https://iconoir.com/ -->
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M15 4V2M15 4V6M15 4H10.5M3 10V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V10H3Z" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M3 10V6C3 4.89543 3.89543 4 5 4H7" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M7 2V6" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M21 10V6C21 4.89543 20.1046 4 19 4H18.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
`
export const SVG_ICON_INTERNET = svg`<!-- Adapted from https://iconoir.com/ -->
<svg class="icon" width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M13 2.04932C13 2.04932 16 5.99994 16 11.9999" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M11 21.9506C11 21.9506 8 17.9999 8 11.9999C8 5.99994 11 2.04932 11 2.04932" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M2.62964 15.5H12" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M2.62964 8.5H21.3704" stroke-linecap="round" stroke-linejoin="round"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.8789 17.9174C22.3727 18.2211 22.3423 18.9604 21.8337 19.0181L19.2671 19.309L18.1159 21.6213C17.8878 22.0795 17.1827 21.8552 17.0661 21.2873L15.8108 15.1713C15.7123 14.6913 16.1437 14.3892 16.561 14.646L21.8789 17.9174Z"></path>
</svg>
`