mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
frontend: improve calendar creation form and fix data binding bugs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
`)}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {svg} from 'lit'
|
||||
|
||||
export function escapeXml(unsafe: string): string {
|
||||
return unsafe.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
@@ -5,3 +7,23 @@ export function escapeXml(unsafe: string): string {
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
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>
|
||||
`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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";
|
||||
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||
import { e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
@@ -34,27 +34,27 @@ let CreateAddressbookForm = class extends i {
|
||||
<form @submit=${this.submit} ${n(this.form)}>
|
||||
<label>
|
||||
principal (for group addressbooks)
|
||||
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||
<option value=${this.user}>${this.user}</option>
|
||||
<select .value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||
<option .value=${this.user}>${this.user}</option>
|
||||
${window.rusticalUser.memberships.map((membership) => x`
|
||||
<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=${(e2) => this.addr_id = e2.target.value} />
|
||||
<input type="text" .value=${this.addr_id} @change=${(e2) => this.addr_id = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
<input type="text" .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} />
|
||||
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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";
|
||||
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||
import { e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
@@ -35,17 +35,17 @@ let CreateCalendarForm = class extends i {
|
||||
<form @submit=${this.submit} ${n(this.form)}>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
<input type="text" .value=${this.displayname} required @change=${(e2) => this.displayname = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Description
|
||||
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
|
||||
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Color
|
||||
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
|
||||
<input type="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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";
|
||||
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||
import { S as SVG_ICON_CALENDAR, a as SVG_ICON_INTERNET, e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
@@ -17,19 +17,23 @@ let CreateCalendarForm = class extends i {
|
||||
constructor() {
|
||||
super();
|
||||
this.user = "";
|
||||
this.principal = "";
|
||||
this.dialog = e();
|
||||
this.form = e();
|
||||
this.timezones = [];
|
||||
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 = "";
|
||||
this.components = /* @__PURE__ */ new Set();
|
||||
this.dialog = e();
|
||||
this.form = e();
|
||||
this.timezones = [];
|
||||
this.fetchTimezones();
|
||||
this.subscriptionUrl = null;
|
||||
this.components = /* @__PURE__ */ new Set(["VEVENT", "VTODO"]);
|
||||
}
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones();
|
||||
@@ -39,13 +43,13 @@ let CreateCalendarForm = class extends i {
|
||||
}
|
||||
render() {
|
||||
return x`
|
||||
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
||||
<dialog ${n(this.dialog)}>
|
||||
<button @click=${(e2) => this.dialog.value.showModal()}>Create calendar</button>
|
||||
<dialog ${n(this.dialog)} @close=${(e2) => this.resetForm()}>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit} ${n(this.form)}>
|
||||
<label>
|
||||
principal (for group calendars)
|
||||
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||
<select required value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||
<option value=${this.user}>${this.user}</option>
|
||||
${window.rusticalUser.memberships.map((membership) => x`
|
||||
<option value=${membership}>${membership}</option>
|
||||
@@ -55,17 +59,17 @@ let CreateCalendarForm = class extends i {
|
||||
<br>
|
||||
<label>
|
||||
id
|
||||
<input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
|
||||
<input type="text" required .value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
<input type="text" required .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||
<select .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map((timezone) => x`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
@@ -75,48 +79,59 @@ let CreateCalendarForm = class extends i {
|
||||
<br>
|
||||
<label>
|
||||
Description
|
||||
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
|
||||
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Color
|
||||
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
|
||||
<input type="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<br>
|
||||
<label>
|
||||
Calendar is subscription to external calendar
|
||||
<input type="checkbox" name="is_subscription" @change=${(e2) => this.isSubscription = e2.target.checked} />
|
||||
</label>
|
||||
<label>Type</label>
|
||||
<div class="tab-radio">
|
||||
<label>
|
||||
<input type="radio" name="type" .checked=${!this.isSubscription} @change=${(e2) => this.isSubscription = false}></input>
|
||||
${SVG_ICON_CALENDAR}
|
||||
Calendar
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="type" .checked=${this.isSubscription} @change=${(e2) => this.isSubscription = true}></input>
|
||||
${SVG_ICON_INTERNET}
|
||||
webCal Subscription
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
${this.isSubscription ? x`
|
||||
<label>
|
||||
Subscription URL
|
||||
<input type="text" name="subscription_url" @change=${(e2) => this.subscriptionUrl = e2.target.value} />
|
||||
<input type="text" pattern="https://.*" .required=${this.isSubscription} .value=${this.subscriptionUrl} @change=${(e2) => this.subscriptionUrl = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<br>
|
||||
` : x``}
|
||||
<br>
|
||||
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
||||
<label>
|
||||
Support ${comp}
|
||||
<input type="checkbox" value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
||||
</label>
|
||||
<br>
|
||||
`)}
|
||||
|
||||
<label>Components</label>
|
||||
<div>
|
||||
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
||||
<label>
|
||||
Support ${comp}
|
||||
<input type="checkbox" .value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.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>
|
||||
</form>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
async submit(e2) {
|
||||
console.log(this.displayname);
|
||||
e2.preventDefault();
|
||||
if (!this.cal_id) {
|
||||
alert("Empty id");
|
||||
@@ -130,6 +145,10 @@ let CreateCalendarForm = class extends i {
|
||||
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",
|
||||
headers: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
||||
import { n, t } from "./property-B8WoKf1Y.mjs";
|
||||
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||
import { n, t } from "./property-C8WJQOrH.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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";
|
||||
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||
import { e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
@@ -33,12 +33,12 @@ let EditAddressbookForm = class extends i {
|
||||
<form @submit=${this.submit} ${n(this.form)}>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
<input type="text" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Description
|
||||
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<button type="submit">Submit</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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";
|
||||
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||
import { e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
@@ -40,12 +40,12 @@ let EditCalendarForm = class extends i {
|
||||
<form @submit=${this.submit} ${n(this.form)}>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
<input type="text" required .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||
<select .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map((timezone) => x`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
@@ -55,18 +55,18 @@ let EditCalendarForm = class extends i {
|
||||
<br>
|
||||
<label>
|
||||
Description
|
||||
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Color
|
||||
<input type="color" name="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
||||
<input type="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
||||
<label>
|
||||
Support ${comp}
|
||||
<input type="checkbox" value=${comp} ?checked=${this.components.has(comp)} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
||||
<input type="checkbox" .value=${comp} ?checked=${this.components.has(comp)} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
||||
</label>
|
||||
<br>
|
||||
`)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { i, x } from "./lit-DKg0et_P.mjs";
|
||||
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { i, x } from "./lit-DKg0et_P.mjs";
|
||||
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
function escapeXml(unsafe) {
|
||||
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
export {
|
||||
escapeXml as e
|
||||
};
|
||||
27
crates/frontend/public/assets/js/index-fgowJCc1.mjs
Normal file
27
crates/frontend/public/assets/js/index-fgowJCc1.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { b } from "./lit-DKg0et_P.mjs";
|
||||
function escapeXml(unsafe) {
|
||||
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
const SVG_ICON_CALENDAR = b`<!-- 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>
|
||||
`;
|
||||
const SVG_ICON_INTERNET = b`<!-- 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>
|
||||
`;
|
||||
export {
|
||||
SVG_ICON_CALENDAR as S,
|
||||
SVG_ICON_INTERNET as a,
|
||||
escapeXml as e
|
||||
};
|
||||
@@ -66,7 +66,7 @@ const { is: i$2, defineProperty: e$1, getOwnPropertyDescriptor: h$1, getOwnPrope
|
||||
}
|
||||
}
|
||||
return i2;
|
||||
} }, f$1 = (t2, s2) => !i$2(t2, s2), b = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
|
||||
} }, f$1 = (t2, s2) => !i$2(t2, s2), b$1 = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
|
||||
Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();
|
||||
let y$1 = class y extends HTMLElement {
|
||||
static addInitializer(t2) {
|
||||
@@ -75,7 +75,7 @@ let y$1 = class y extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
|
||||
}
|
||||
static createProperty(t2, s2 = b) {
|
||||
static createProperty(t2, s2 = b$1) {
|
||||
if (s2.state && (s2.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t2) && ((s2 = Object.create(s2)).wrapped = true), this.elementProperties.set(t2, s2), !s2.noAccessor) {
|
||||
const i2 = Symbol(), h2 = this.getPropertyDescriptor(t2, i2, s2);
|
||||
void 0 !== h2 && e$1(this.prototype, t2, h2);
|
||||
@@ -93,7 +93,7 @@ let y$1 = class y extends HTMLElement {
|
||||
}, configurable: true, enumerable: true };
|
||||
}
|
||||
static getPropertyOptions(t2) {
|
||||
return this.elementProperties.get(t2) ?? b;
|
||||
return this.elementProperties.get(t2) ?? b$1;
|
||||
}
|
||||
static _$Ei() {
|
||||
if (this.hasOwnProperty(d$1("elementProperties"))) return;
|
||||
@@ -256,7 +256,7 @@ y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("eleme
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const t = globalThis, i$1 = t.trustedTypes, s$1 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e = "$lit$", h = `lit$${Math.random().toFixed(9).slice(2)}$`, o$1 = "?" + h, n2 = `<${o$1}>`, r = document, l = () => r.createComment(""), c = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof t2?.[Symbol.iterator], d = "[ \n\f\r]", f = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
|
||||
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129);
|
||||
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), b = y2(2), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129);
|
||||
function P(t2, i2) {
|
||||
if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
||||
return void 0 !== s$1 ? s$1.createHTML(i2) : i2;
|
||||
@@ -513,6 +513,7 @@ o?.({ LitElement: i });
|
||||
(s.litElementVersions ??= []).push("4.2.1");
|
||||
export {
|
||||
E,
|
||||
b,
|
||||
f$1 as f,
|
||||
i,
|
||||
u$1 as u,
|
||||
@@ -1,4 +1,4 @@
|
||||
import { f, u } from "./lit-DkXrt_Iv.mjs";
|
||||
import { f, u } from "./lit-DKg0et_P.mjs";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
@@ -1,4 +1,4 @@
|
||||
import { E } from "./lit-DkXrt_Iv.mjs";
|
||||
import { E } from "./lit-DKg0et_P.mjs";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google LLC
|
||||
@@ -338,6 +338,7 @@ select {
|
||||
form {
|
||||
|
||||
input[type="text"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
input[type="color"],
|
||||
textarea,
|
||||
@@ -364,3 +365,57 @@ svg.icon {
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-radio {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr;
|
||||
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--text-on-background-color);
|
||||
margin: 2px;
|
||||
|
||||
label {
|
||||
padding: 10px;
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
input {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
label:hover {
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 15%);
|
||||
}
|
||||
|
||||
label:has(input:focus-visible) {
|
||||
outline: medium auto currentColor;
|
||||
outline: medium auto invert;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
label:has(input:checked) {
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 20%);
|
||||
}
|
||||
}
|
||||
|
||||
9
crates/frontend/public/templates/icons/internet.svg
Normal file
9
crates/frontend/public/templates/icons/internet.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- 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>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
Reference in New Issue
Block a user