1157 lines
59 KiB
JavaScript
1157 lines
59 KiB
JavaScript
import server from "/calendar/@server/calendar.js"
|
||
import calendarUtil from "../calendarUtil.js"
|
||
import "../EventFileList.js"
|
||
import "../../components/Avatar.js"
|
||
|
||
css(`
|
||
eventform- {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-height: 0;
|
||
scrollbar-width: none;
|
||
-ms-overflow-style: none;
|
||
}
|
||
eventform- > form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1 1 auto;
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
eventform- ::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||
eventform- ::-webkit-scrollbar-thumb { background: transparent; }
|
||
eventform- ::-webkit-scrollbar-track { background: transparent; }
|
||
eventform- input::placeholder,
|
||
eventform- textarea::placeholder {
|
||
color: var(--headertext);
|
||
opacity: 0.35;
|
||
}
|
||
eventform- input[type="date"],
|
||
eventform- input[type="time"] {
|
||
min-width: 0;
|
||
}
|
||
eventform- input[type="checkbox"] {
|
||
flex-shrink: 0;
|
||
}
|
||
#eventform-toast-wrap {
|
||
transition: max-height 0.25s ease, opacity 0.22s ease, padding-top 0.25s ease;
|
||
}
|
||
`)
|
||
|
||
class EventForm extends Shadow {
|
||
cardInputStyles(el) {
|
||
return el
|
||
.border("none").outline("none").background("transparent")
|
||
.color("var(--text)").fontSize(0.9, em).fontFamily("Arial")
|
||
.padding(0).boxSizing("border-box")
|
||
}
|
||
|
||
selectedCalendars = [];
|
||
pendingSaveEventData = null; // stashed before the recurring scope prompt, consumed by performSave
|
||
uploadedFiles = [];
|
||
existingAttachments = [];
|
||
filesOpen = false;
|
||
attachmentsDeleted = false;
|
||
pendingDeletions = [];
|
||
initialAllDay = true;
|
||
startDate = calendarUtil.toDateInput(new Date());
|
||
endDate = calendarUtil.toDateInput(new Date());
|
||
startTime = "13:00";
|
||
endTime = "14:00";
|
||
recurrence = null;
|
||
|
||
constructor(calendars, updateEvents, event = null, onBack = null, onDelete = null, initialDate = null, onSaveError = null) {
|
||
super()
|
||
this.calendars = calendars;
|
||
this.editEvent = event;
|
||
this.updateEvents = updateEvents;
|
||
this.onBack = onBack;
|
||
this.onDelete = onDelete;
|
||
this.onSaveError = onSaveError;
|
||
|
||
if (event) {
|
||
const start = new Date(event.time_start);
|
||
const end = new Date(event.time_end);
|
||
this.startDate = calendarUtil.toDateInput(start);
|
||
this.endDate = calendarUtil.toDateInput(end);
|
||
this.startTime = event.all_day ? "13:00" : calendarUtil.toTimeInput(start);
|
||
this.endTime = event.all_day ? "14:00" : calendarUtil.toTimeInput(end);
|
||
this.selectedCalendars = calendars.filter(c => event.calendars?.includes(c.id));
|
||
this.existingAttachments = event.attachments ?? [];
|
||
this.recurrence = event.recurrence
|
||
? { frequency: event.recurrence.frequency, interval: event.recurrence.interval ?? 1, days_of_week: event.recurrence.days_of_week ? [...event.recurrence.days_of_week] : null }
|
||
: null;
|
||
|
||
// Snapshot original state in getFormData() shape for dirty checking
|
||
this.originalFormData = {
|
||
title: event.title ?? "",
|
||
location: event.location ?? "",
|
||
description: event.description ?? "",
|
||
all_day: event.all_day,
|
||
time_start: event.all_day ? this.startDate : `${this.startDate}T${this.startTime}`,
|
||
time_end: event.all_day ? this.endDate : `${this.endDate}T${this.endTime}`,
|
||
calendars: [...(event.calendars ?? [])].sort().join(","),
|
||
recurrence: this.recurrence ? { ...this.recurrence, days_of_week: this.recurrence.days_of_week ? [...this.recurrence.days_of_week] : null } : null
|
||
};
|
||
} else {
|
||
if (calendars.length > 0) {
|
||
this.selectedCalendars = [calendars[0]];
|
||
}
|
||
if (initialDate) {
|
||
this.startDate = calendarUtil.toDateInput(initialDate);
|
||
const h = initialDate.getHours(), m = initialDate.getMinutes();
|
||
if (h !== 0 || m !== 0) {
|
||
this.initialAllDay = false;
|
||
this.startTime = calendarUtil.toTimeInput(initialDate);
|
||
const endDate = new Date(initialDate.getTime() + 30 * 60 * 1000);
|
||
this.endTime = calendarUtil.toTimeInput(endDate);
|
||
this.endDate = calendarUtil.toDateInput(endDate);
|
||
} else {
|
||
this.endDate = calendarUtil.toDateInput(initialDate);
|
||
}
|
||
}
|
||
this.originalFormData = {
|
||
title: "",
|
||
location: "",
|
||
description: "",
|
||
all_day: this.initialAllDay,
|
||
time_start: this.initialAllDay ? this.startDate : `${this.startDate}T${this.startTime}`,
|
||
time_end: this.initialAllDay ? this.endDate : `${this.endDate}T${this.endTime}`,
|
||
calendars: (calendars.length > 0 ? [calendars[0].id] : []).sort().join(","),
|
||
recurrence: null
|
||
};
|
||
}
|
||
}
|
||
|
||
enforceEndAfterStart() {
|
||
const isAllDay = this.$('[name="all_day"]')?.checked ?? true;
|
||
const start = new Date(`${this.startDate}T${isAllDay ? "00:00" : this.startTime}`);
|
||
const end = new Date(`${this.endDate}T${isAllDay ? "00:00" : this.endTime}`);
|
||
|
||
if (end < start) {
|
||
const newEnd = new Date(start.getTime() + (isAllDay ? 0 : 60 * 60 * 1000));
|
||
this.endDate = calendarUtil.toDateInput(newEnd);
|
||
this.endTime = calendarUtil.toTimeInput(newEnd);
|
||
const endDateEl = this.$('[name="time_end_date"]');
|
||
const endTimeEl = this.$('[name="time_end_time"]');
|
||
if (endDateEl) endDateEl.value = this.endDate;
|
||
if (endTimeEl) endTimeEl.value = this.endTime;
|
||
}
|
||
}
|
||
|
||
recurrenceOptionKey() {
|
||
const r = this.recurrence;
|
||
if (!r) return "never";
|
||
if (r.frequency === 'daily') return "daily";
|
||
if (r.frequency === 'weekly' && r.interval === 2) return "biweekly";
|
||
if (r.frequency === 'weekly') return "weekly";
|
||
if (r.frequency === 'monthly') return "monthly";
|
||
if (r.frequency === 'yearly') return "yearly";
|
||
return "never";
|
||
}
|
||
|
||
recurrenceLabel() {
|
||
switch (this.recurrenceOptionKey()) {
|
||
case "daily": return "Daily";
|
||
case "weekly": return "Weekly";
|
||
case "biweekly": return "Every 2 weeks";
|
||
case "monthly": return "Monthly";
|
||
case "yearly": return "Yearly";
|
||
default: return "Never";
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const isEditMode = !!this.editEvent;
|
||
const showTime = isEditMode ? !this.editEvent.all_day : !this.initialAllDay;
|
||
const initialAllDay = isEditMode ? this.editEvent.all_day : this.initialAllDay;
|
||
|
||
form(() => {
|
||
VStack(() => {
|
||
// ── Header ────────────────────────────────────────────
|
||
HStack(() => {
|
||
button("Cancel")
|
||
.attr({ type: "button" })
|
||
.background("none").border("none").padding(0)
|
||
.color("var(--quillred)").fontSize(0.95, em)
|
||
.fontFamily("Arial").cursor("pointer").flexShrink(0)
|
||
.onTap(() => this.handleBack())
|
||
|
||
p(isEditMode ? "Edit Event" : "New Event")
|
||
.margin(0).fontSize(1, em).fontWeight("700")
|
||
.color("var(--headertext)").fontFamily("Arial")
|
||
.flex(1).textAlign("center")
|
||
|
||
button("Save")
|
||
.attr({ type: "submit" })
|
||
.background("none").border("none").padding(0)
|
||
.color("var(--quillred)").fontSize(0.95, em)
|
||
.fontWeight("600").fontFamily("Arial").cursor("pointer").flexShrink(0)
|
||
})
|
||
.paddingHorizontal(1.25, em).paddingVertical(0.9, em)
|
||
.alignItems("center").borderBottom("1px solid var(--divider)")
|
||
.flexShrink(0).background(util.darkMode() ? "transparent" : "var(--sidebottombars)")
|
||
|
||
// ── Error toast ───────────────────────────────────────
|
||
VStack(() => {
|
||
p("")
|
||
.attr({ id: "eventform-toast" })
|
||
.margin(0)
|
||
.padding("0.55em 1.1em")
|
||
.background("var(--quillred)")
|
||
.color("white")
|
||
.fontSize(0.85, em)
|
||
.fontWeight("500")
|
||
.fontFamily("Arial")
|
||
.borderRadius("0.5em")
|
||
.boxShadow("0 2px 10px rgba(0,0,0,0.15)")
|
||
.whiteSpace("nowrap")
|
||
})
|
||
.attr({ id: "eventform-toast-wrap" })
|
||
.alignItems("center")
|
||
.overflow("hidden")
|
||
.maxHeight(0)
|
||
.opacity(0)
|
||
.flexShrink(0)
|
||
|
||
// ── Scrollable body ───────────────────────────────────
|
||
VStack(() => {
|
||
// ── Title card ────────────────────────────────────
|
||
VStack(() => {
|
||
input("Title", "100%")
|
||
.attr({ name: "title", type: "text", value: this.editEvent?.title ?? "" })
|
||
.border("none").outline("none").background("transparent")
|
||
.color("var(--text)").fontSize(1.1, em).fontFamily("Arial")
|
||
.fontWeight("500").padding("0.9em 1em").boxSizing("border-box")
|
||
})
|
||
.background("var(--darkaccent)").border("1px solid var(--divider)")
|
||
.borderRadius(12, px).marginHorizontal(1, em).overflow("hidden")
|
||
.flexShrink(0)
|
||
|
||
// ── Date / time card ──────────────────────────────
|
||
VStack(() => {
|
||
// All day row
|
||
HStack(() => {
|
||
p("All day")
|
||
.margin(0).fontSize(0.92, em).color("var(--headertext)")
|
||
input("", "auto")
|
||
.attr({ name: "all_day", type: "checkbox", ...(initialAllDay ? { checked: true } : {}) })
|
||
.accentColor("var(--quillred)").transform("scale(1.25)")
|
||
.onChange(() => {
|
||
const isAllDay = this.$('[name="all_day"]').checked
|
||
const display = isAllDay ? "none" : ""
|
||
this.$("#time_start_time").style.display = display
|
||
this.$("#time_end_time").style.display = display
|
||
})
|
||
})
|
||
.paddingHorizontal(1, em).paddingVertical(0.78, em)
|
||
.justifyContent("space-between").alignItems("center")
|
||
.borderBottom("1px solid var(--divider)")
|
||
|
||
// Starts row
|
||
HStack(() => {
|
||
p("Starts")
|
||
.margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0)
|
||
HStack(() => {
|
||
input("", "auto", "1em")
|
||
.attr({ name: "time_start_date", type: "date", id: "time_start_date", value: this.startDate })
|
||
.styles(this.cardInputStyles).fontSize(0.88, em)
|
||
.onChange((e) => {
|
||
const oldDay = new Date(this.startDate + 'T00:00:00').getDay();
|
||
this.startDate = e.target.value;
|
||
this.enforceEndAfterStart();
|
||
if (this.recurrence?.frequency === 'weekly' && this.recurrence.days_of_week?.length > 0) {
|
||
const newDay = new Date(this.startDate + 'T00:00:00').getDay();
|
||
if (newDay !== oldDay && this.recurrence.days_of_week.includes(oldDay)) {
|
||
const updated = [...new Set(this.recurrence.days_of_week.map(d => d === oldDay ? newDay : d))].sort((a, b) => a - b);
|
||
this.recurrence.days_of_week = updated;
|
||
const oldEl = this.$(`#recur-day-${oldDay}`);
|
||
const newEl = this.$(`#recur-day-${newDay}`);
|
||
if (oldEl) { oldEl.style.background = 'transparent'; oldEl.style.color = 'var(--text)'; oldEl.style.opacity = '0.4'; }
|
||
if (newEl) { newEl.style.background = 'var(--quillred)'; newEl.style.color = 'white'; newEl.style.opacity = '1'; }
|
||
}
|
||
}
|
||
})
|
||
input("", "auto", "1em")
|
||
.attr({ name: "time_start_time", type: "time", id: "time_start_time", value: this.startTime, step: "300" })
|
||
.styles(this.cardInputStyles).fontSize(0.88, em)
|
||
.display(showTime ? "" : "none")
|
||
.onChange((e) => { this.startTime = e.target.value; this.enforceEndAfterStart() })
|
||
})
|
||
.gap(0.35, em).flex(1).justifyContent("flex-end")
|
||
})
|
||
.paddingHorizontal(1, em).paddingVertical(0.78, em)
|
||
.alignItems("center").borderBottom("1px solid var(--divider)")
|
||
|
||
// Ends row
|
||
HStack(() => {
|
||
p("Ends")
|
||
.margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0)
|
||
HStack(() => {
|
||
input("", "auto", "1em")
|
||
.attr({ name: "time_end_date", type: "date", id: "time_end_date", value: this.endDate })
|
||
.styles(this.cardInputStyles).fontSize(0.88, em)
|
||
.onChange((e) => { this.endDate = e.target.value; this.enforceEndAfterStart() })
|
||
input("", "auto", "1em")
|
||
.attr({ name: "time_end_time", type: "time", id: "time_end_time", value: this.endTime, step: "300" })
|
||
.styles(this.cardInputStyles).fontSize(0.88, em)
|
||
.display(showTime ? "" : "none")
|
||
.onChange((e) => { this.endTime = e.target.value; this.enforceEndAfterStart() })
|
||
})
|
||
.gap(0.35, em).flex(1).justifyContent("flex-end")
|
||
})
|
||
.paddingHorizontal(1, em).paddingVertical(0.78, em)
|
||
.alignItems("center").borderBottom("1px solid var(--divider)")
|
||
|
||
// Repeat row
|
||
HStack(() => {
|
||
p("Repeat")
|
||
.margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0)
|
||
p(this.recurrenceLabel())
|
||
.attr({ id: "recur-label" })
|
||
.margin(0).fontSize(0.88, em).fontFamily("Arial")
|
||
.color(this.recurrence ? "var(--text)" : "var(--headertext)")
|
||
.opacity(this.recurrence ? 1 : 0.55)
|
||
.flex(1).textAlign("right")
|
||
p("›")
|
||
.attr({ id: "recur-chevron" })
|
||
.margin(0).opacity(0.3).fontSize(1.1, em).flexShrink(0)
|
||
.transition("transform 0.25s ease")
|
||
})
|
||
.paddingHorizontal(1, em).paddingVertical(0.78, em)
|
||
.alignItems("center").gap(0.5, em)
|
||
.cursor("pointer")
|
||
.onTap(() => {
|
||
const picker = this.$("#recur-picker")
|
||
const chevron = this.$("#recur-chevron")
|
||
if (!picker) return
|
||
const isOpen = picker.getAttribute("data-open") === "1"
|
||
if (isOpen) {
|
||
picker.setAttribute("data-open", "0")
|
||
picker.style.maxHeight = "0"
|
||
if (chevron) chevron.style.transform = ""
|
||
} else {
|
||
picker.setAttribute("data-open", "1")
|
||
picker.style.maxHeight = picker.scrollHeight + "px"
|
||
if (chevron) chevron.style.transform = "rotate(90deg)"
|
||
}
|
||
})
|
||
|
||
// Inline recurrence picker
|
||
VStack(() => {
|
||
const currentKey = this.recurrenceOptionKey()
|
||
const isWeekly = currentKey === 'weekly' || currentKey === 'biweekly'
|
||
const selectedDays = this.recurrence?.days_of_week ?? []
|
||
|
||
const recurrenceOptions = [
|
||
{ key: "never", label: "Never" },
|
||
{ key: "daily", label: "Daily" },
|
||
{ key: "weekly", label: "Weekly" },
|
||
{ key: "biweekly", label: "Every 2 weeks" },
|
||
{ key: "monthly", label: "Monthly" },
|
||
{ key: "yearly", label: "Yearly" },
|
||
]
|
||
|
||
recurrenceOptions.forEach(opt => {
|
||
HStack(() => {
|
||
p(opt.label)
|
||
.margin(0).fontSize(0.9, em).color("var(--text)").fontFamily("Arial").flex(1)
|
||
p("✓")
|
||
.attr({ id: `recur-check-${opt.key}` })
|
||
.margin(0).fontSize(0.88, em)
|
||
.color("var(--quillred)").fontWeight("700")
|
||
.display(currentKey === opt.key ? "" : "none")
|
||
})
|
||
.paddingHorizontal(1.25, em).paddingVertical(0.72, em)
|
||
.alignItems("center")
|
||
.borderBottom("1px solid var(--divider)")
|
||
.cursor("pointer")
|
||
.onTap(() => {
|
||
let newRecurrence = null
|
||
if (opt.key !== 'never') {
|
||
if (opt.key === 'weekly' || opt.key === 'biweekly') {
|
||
const sd = new Date(this.startDate + 'T00:00:00').getDay()
|
||
const curDays = this.recurrence?.frequency === 'weekly'
|
||
? (this.recurrence.days_of_week ?? [sd])
|
||
: [sd]
|
||
newRecurrence = {
|
||
frequency: 'weekly',
|
||
interval: opt.key === 'biweekly' ? 2 : 1,
|
||
days_of_week: [...curDays]
|
||
}
|
||
} else {
|
||
const freqMap = { daily: 'daily', monthly: 'monthly', yearly: 'yearly' }
|
||
newRecurrence = { frequency: freqMap[opt.key], interval: 1 }
|
||
}
|
||
}
|
||
this.recurrence = newRecurrence
|
||
|
||
const allKeys = ['never', 'daily', 'weekly', 'biweekly', 'monthly', 'yearly']
|
||
allKeys.forEach(k => {
|
||
const el = this.$(`#recur-check-${k}`)
|
||
if (el) el.style.display = k === opt.key ? "" : "none"
|
||
})
|
||
|
||
const labelEl = this.$("#recur-label")
|
||
if (labelEl) {
|
||
labelEl.textContent = this.recurrenceLabel()
|
||
labelEl.style.color = newRecurrence ? "var(--text)" : "var(--headertext)"
|
||
labelEl.style.opacity = newRecurrence ? "1" : "0.55"
|
||
}
|
||
|
||
const isNowWeekly = opt.key === 'weekly' || opt.key === 'biweekly'
|
||
const daysRow = this.$("#recur-days")
|
||
if (daysRow) daysRow.style.display = isNowWeekly ? "" : "none"
|
||
|
||
if (isNowWeekly && newRecurrence?.days_of_week) {
|
||
const days = newRecurrence.days_of_week;
|
||
[0, 1, 2, 3, 4, 5, 6].forEach(d => {
|
||
const el = this.$(`#recur-day-${d}`)
|
||
if (!el) return
|
||
const sel = days.includes(d)
|
||
el.style.background = sel ? "var(--quillred)" : "transparent"
|
||
el.style.color = sel ? "white" : "var(--text)"
|
||
el.style.opacity = sel ? "1" : "0.4"
|
||
})
|
||
}
|
||
|
||
const picker = this.$("#recur-picker")
|
||
if (picker && picker.getAttribute("data-open") === "1") {
|
||
picker.style.maxHeight = picker.scrollHeight + "px"
|
||
}
|
||
})
|
||
})
|
||
|
||
// Day-of-week selector (visible only for weekly/biweekly)
|
||
const DAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S']
|
||
HStack(() => {
|
||
DAY_LABELS.forEach((label, i) => {
|
||
const sel = selectedDays.includes(i)
|
||
span(label)
|
||
.attr({ id: `recur-day-${i}` })
|
||
.width(2, em).height(2, em)
|
||
.lineHeight("2em")
|
||
.textAlign("center")
|
||
.borderRadius(50, pct)
|
||
.fontSize(0.78, em).fontWeight("600")
|
||
.cursor("pointer").flexShrink(0)
|
||
.background(sel ? "var(--quillred)" : "transparent")
|
||
.color(sel ? "white" : "var(--text)")
|
||
.opacity(sel ? "1" : "0.4")
|
||
.onTap(() => {
|
||
if (!this.recurrence?.days_of_week) return
|
||
const days = this.recurrence.days_of_week
|
||
const idx = days.indexOf(i)
|
||
if (idx >= 0) {
|
||
if (days.length > 1) days.splice(idx, 1)
|
||
} else {
|
||
days.push(i)
|
||
}
|
||
const nowSel = days.includes(i)
|
||
const el = this.$(`#recur-day-${i}`)
|
||
if (el) {
|
||
el.style.background = nowSel ? "var(--quillred)" : "transparent"
|
||
el.style.color = nowSel ? "white" : "var(--text)"
|
||
el.style.opacity = nowSel ? "1" : "0.4"
|
||
}
|
||
const picker = this.$("#recur-picker")
|
||
if (picker && picker.getAttribute("data-open") === "1") {
|
||
picker.style.maxHeight = picker.scrollHeight + "px"
|
||
}
|
||
})
|
||
})
|
||
})
|
||
.attr({ id: "recur-days" })
|
||
.justifyContent("space-around")
|
||
.paddingHorizontal(0.5, em).paddingVertical(0.65, em)
|
||
.display(isWeekly ? "" : "none")
|
||
})
|
||
.attr({ id: "recur-picker", "data-open": "0" })
|
||
.overflow("hidden").maxHeight(0)
|
||
.transition("max-height 0.3s ease")
|
||
})
|
||
.background("var(--darkaccent)").border("1px solid var(--divider)")
|
||
.borderRadius(12, px).marginHorizontal(1, em).overflow("hidden")
|
||
.flexShrink(0)
|
||
|
||
// ── Calendar / Location / Notes card ──────────────
|
||
VStack(() => {
|
||
// Calendar row — tapping expands inline picker
|
||
HStack(() => {
|
||
p("Calendar")
|
||
.margin(0).fontSize(0.92, em).color("var(--headertext)").flexShrink(0)
|
||
HStack(() => {
|
||
this.selectedCalendars.forEach(cal => {
|
||
HStack(() => {
|
||
p("").width(0.6, em).height(0.6, em)
|
||
.background(cal.color).borderRadius(50, pct).flexShrink(0)
|
||
p(cal.name)
|
||
.margin(0).fontSize(0.82, em).fontWeight("600")
|
||
.color("var(--text)").whiteSpace("nowrap")
|
||
})
|
||
.gap(0.3, em).alignItems("center")
|
||
})
|
||
})
|
||
.attr({ id: "calendar-display" })
|
||
.flex(1).justifyContent("flex-end").flexWrap("wrap").gap(0.5, em)
|
||
p("›")
|
||
.attr({ id: "cal-chevron" })
|
||
.margin(0).opacity(0.3).fontSize(1.1, em).flexShrink(0)
|
||
.transition("transform 0.25s ease")
|
||
})
|
||
.paddingHorizontal(1, em).paddingVertical(0.78, em)
|
||
.alignItems("center").gap(0.5, em)
|
||
.borderBottom("1px solid var(--divider)").cursor("pointer")
|
||
.onTap(() => {
|
||
const picker = this.$("#cal-picker")
|
||
const chevron = this.$("#cal-chevron")
|
||
if (!picker) return
|
||
const isOpen = picker.getAttribute("data-open") === "1"
|
||
if (isOpen) {
|
||
picker.setAttribute("data-open", "0")
|
||
picker.style.maxHeight = "0"
|
||
if (chevron) chevron.style.transform = ""
|
||
} else {
|
||
picker.setAttribute("data-open", "1")
|
||
picker.style.maxHeight = picker.scrollHeight + "px"
|
||
if (chevron) chevron.style.transform = "rotate(90deg)"
|
||
}
|
||
})
|
||
|
||
// Inline calendar picker (collapsed by default)
|
||
VStack(() => {
|
||
this.calendars.forEach(cal => {
|
||
const isSelected = this.selectedCalendars.some(c => c.id === cal.id)
|
||
HStack(() => {
|
||
HStack(() => {
|
||
p("").width(0.65, em).height(0.65, em)
|
||
.background(cal.color).borderRadius(50, pct).flexShrink(0)
|
||
p(cal.name)
|
||
.margin(0).fontSize(0.9, em).color("var(--text)").fontFamily("Arial")
|
||
})
|
||
.gap(0.45, em).alignItems("center").flex(1)
|
||
|
||
p("✓")
|
||
.attr({ id: `cal-check-${cal.id}` })
|
||
.margin(0).fontSize(0.88, em)
|
||
.color("var(--quillred)").fontWeight("700")
|
||
.display(isSelected ? "" : "none")
|
||
})
|
||
.paddingHorizontal(1.25, em).paddingVertical(0.72, em)
|
||
.alignItems("center")
|
||
.borderBottom("1px solid var(--divider)")
|
||
.cursor("pointer")
|
||
.onTap(() => {
|
||
const idx = this.selectedCalendars.findIndex(c => c.id === cal.id)
|
||
if (idx >= 0) {
|
||
if (this.selectedCalendars.length > 1) {
|
||
this.selectedCalendars.splice(idx, 1)
|
||
const check = this.$(`#cal-check-${cal.id}`)
|
||
if (check) check.style.display = "none"
|
||
}
|
||
} else {
|
||
this.selectedCalendars.push(cal)
|
||
const check = this.$(`#cal-check-${cal.id}`)
|
||
if (check) check.style.display = ""
|
||
}
|
||
this.updateCalendarDisplay()
|
||
})
|
||
})
|
||
})
|
||
.attr({ id: "cal-picker", "data-open": "0" })
|
||
.overflow("hidden").maxHeight(0)
|
||
.transition("max-height 0.3s ease")
|
||
|
||
// Location row
|
||
HStack(() => {
|
||
p("📍").margin(0).fontSize(0.85, em).flexShrink(0)
|
||
input("Location", "100%")
|
||
.attr({ name: "location", type: "text", value: this.editEvent?.location ?? "" })
|
||
.styles(this.cardInputStyles).flex(1)
|
||
})
|
||
.paddingHorizontal(1, em).paddingVertical(0.78, em)
|
||
.alignItems("center").gap(0.65, em)
|
||
.borderBottom("1px solid var(--divider)")
|
||
|
||
// Noes row
|
||
HStack(() => {
|
||
p("📝")
|
||
.margin(0).fontSize(0.85, em).flexShrink(0).alignSelf("flex-start").paddingTop(0.08, em)
|
||
textarea(this.editEvent?.description ?? "")
|
||
.attr({ name: "description" })
|
||
.styles(this.cardInputStyles).flex(1)
|
||
.minHeight(3, em).resize("none")
|
||
.fieldSizing("content").lineHeight("1.45")
|
||
.onAppear(function() {
|
||
this.value = this.placeholder
|
||
})
|
||
})
|
||
.paddingHorizontal(1, em).paddingVertical(0.78, em)
|
||
.alignItems("flex-start").gap(0.65, em)
|
||
})
|
||
.background("var(--darkaccent)").border("1px solid var(--divider)")
|
||
.borderRadius(12, px).marginHorizontal(1, em).overflow("hidden")
|
||
.flexShrink(0)
|
||
|
||
// ── Attachments card ──────────────────────────────
|
||
input("", "100%")
|
||
.attr({ name: "attachments", type: "file", multiple: true })
|
||
.display("none")
|
||
.onChange(async (e) => {
|
||
const files = Array.from(e.target.files)
|
||
e.target.value = ""
|
||
this.uploadedFiles.push(...files)
|
||
this.fileListComponent.update(files)
|
||
this.updateFilesChevron()
|
||
})
|
||
|
||
VStack(() => {
|
||
HStack(() => {
|
||
p("📎").margin(0).fontSize(0.85, em).flexShrink(0)
|
||
p("Attachments")
|
||
.margin(0).fontSize(0.92, em).color("var(--headertext)").flex(1)
|
||
HStack(() => {
|
||
span("Upload")
|
||
.color("var(--quillred)").cursor("pointer")
|
||
.fontFamily("Arial").fontSize(0.85, em)
|
||
.padding(0.5, em).margin(-0.5, em)
|
||
.onTap((e) => { e.stopPropagation(); this.$('[name="attachments"]').click() })
|
||
span("▼")
|
||
.attr({ id: "files-chevron" })
|
||
.margin(0).fontSize(0.7, em).color("var(--text)").opacity(0.5)
|
||
.display(this.hasAnyFiles() ? "inline-block" : "none")
|
||
.transition("transform 0.3s ease")
|
||
.transform(this.filesOpen ? "rotate(180deg)" : "rotate(0deg)")
|
||
.padding(0.5, em).margin(-0.5, em)
|
||
})
|
||
.gap(0.65, em).alignItems("center")
|
||
})
|
||
.paddingHorizontal(1, em).paddingVertical(0.78, em).alignItems("center").gap(0.65, em)
|
||
.cursor(this.hasAnyFiles() ? "pointer" : "")
|
||
.onTap(() => { if (this.hasAnyFiles()) this.toggleFiles() })
|
||
|
||
VStack(() => {
|
||
this.fileListComponent = EventFileList(
|
||
this.existingAttachments,
|
||
isEditMode ? (fileId) => this.handleDeleteAttachment(fileId) : null,
|
||
(index) => this.handleDeleteNewFile(index),
|
||
(file, url) => $("filepreview-")?.open(file, url)
|
||
)
|
||
this.fileListComponent
|
||
.padding("0 1em 0.75em").flexDirection("column")
|
||
.gap(0.5, em).border("none")
|
||
})
|
||
.attr({ id: "files-content" })
|
||
.overflow("hidden")
|
||
.maxHeight(this.hasAnyFiles() && this.filesOpen ? "600px" : "0")
|
||
.transition("max-height 0.5s ease")
|
||
})
|
||
.background("var(--darkaccent)").border("1px solid var(--divider)")
|
||
.borderRadius(12, px).marginHorizontal(1, em).overflow("hidden")
|
||
.flexShrink(0)
|
||
|
||
if (isEditMode) {
|
||
button("Delete Event")
|
||
.attr({ type: "button" })
|
||
.width("calc(100% - 2em)").marginHorizontal(1, em)
|
||
.padding(0.85, em).boxSizing("border-box")
|
||
.background("transparent").color("var(--quillred)")
|
||
.border("1.5px solid var(--quillred)")
|
||
.borderRadius(12, px).fontSize(0.95, em).fontFamily("Arial")
|
||
.fontWeight("600").cursor("pointer")
|
||
.flexShrink(0)
|
||
.onTap(() => this.handleDelete())
|
||
}
|
||
|
||
})
|
||
.overflowY("scroll").flex(1).minHeight(0).paddingTop(0.85, em).paddingBottom(1.5, em).gap(0.85, em)
|
||
|
||
// ── Footer: creator avatar + timestamps ───────────────
|
||
if (this.editEvent) {
|
||
const members = global.currentNetwork.data?.members || []
|
||
const creator = members.find(m => m.id === this.editEvent.creator_id)
|
||
if (creator) {
|
||
HStack(() => {
|
||
Avatar(creator, 2)
|
||
VStack(() => {
|
||
p(`Created ${calendarUtil.timeAgo(this.editEvent.created)} by ${creator.first_name}`)
|
||
.margin(0).fontSize(0.9, em).color("var(--headertext)").opacity(0.5)
|
||
if (this.editEvent.updated_at && this.editEvent.updated_at !== this.editEvent.created) {
|
||
p(`Last updated ${calendarUtil.timeAgo(this.editEvent.updated_at)}`)
|
||
.margin(0).fontSize(0.9, em).color("var(--headertext)").opacity(0.4)
|
||
}
|
||
})
|
||
.gap(0.15, em)
|
||
})
|
||
// .position("absolute")
|
||
// .bottom(0)
|
||
// .left(0)
|
||
// .right(0)
|
||
.paddingHorizontal(1, em)
|
||
.paddingVertical(0.65, em)
|
||
.alignItems("center")
|
||
.gap(0.5, em)
|
||
.flexShrink(0)
|
||
}
|
||
}
|
||
|
||
})
|
||
.height(100, pct)
|
||
.position("relative")
|
||
.onSubmit((e) => {
|
||
e.preventDefault()
|
||
this.handleSend(this.getFormData())
|
||
})
|
||
.onKeyDown((e) => {
|
||
if (e.key === "Enter" && e.target.tagName !== "TEXTAREA" && e.target.tagName !== "BUTTON") e.preventDefault()
|
||
})
|
||
})
|
||
.height(100, pct)
|
||
}
|
||
|
||
hasAnyFiles() {
|
||
return this.existingAttachments.length > 0 || this.uploadedFiles.length > 0;
|
||
}
|
||
|
||
toggleFiles() {
|
||
this.filesOpen = !this.filesOpen;
|
||
const content = this.$("#files-content");
|
||
const chevron = this.$("#files-chevron");
|
||
if (content) content.style.maxHeight = this.filesOpen ? "600px" : "0";
|
||
if (chevron) chevron.style.transform = this.filesOpen ? "rotate(180deg)" : "rotate(0deg)";
|
||
}
|
||
|
||
updateFilesChevron() {
|
||
const chevron = this.$("#files-chevron");
|
||
const content = this.$("#files-content");
|
||
const hasFiles = this.hasAnyFiles();
|
||
if (chevron) chevron.style.display = hasFiles ? "inline-block" : "none";
|
||
if (!hasFiles && this.filesOpen) {
|
||
this.filesOpen = false;
|
||
if (content) content.style.maxHeight = "0";
|
||
} else if (hasFiles && !this.filesOpen) {
|
||
this.filesOpen = true;
|
||
if (content) content.style.maxHeight = "600px";
|
||
if (chevron) chevron.style.transform = "rotate(180deg)";
|
||
}
|
||
}
|
||
|
||
updateCalendarDisplay() {
|
||
const el = this.$("#calendar-display");
|
||
if (!el) return;
|
||
el.innerHTML = this.selectedCalendars.map(cal => `
|
||
<div style="display:flex;align-items:center;gap:0.3em;">
|
||
<div style="width:0.6em;height:0.6em;background:${cal.color};border-radius:50%;flex-shrink:0;"></div>
|
||
<p style="font-size:0.82em;font-weight:600;margin:0;color:var(--text);white-space:nowrap;">${cal.name}</p>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
showError(msg) {
|
||
const wrap = this.$("#eventform-toast-wrap")
|
||
const toast = this.$("#eventform-toast")
|
||
if (!wrap || !toast) return
|
||
clearTimeout(this._errorTimer)
|
||
if (msg) {
|
||
toast.innerText = msg
|
||
wrap.style.maxHeight = "3em"
|
||
wrap.style.opacity = "1"
|
||
wrap.style.paddingTop = "0.85em"
|
||
this._errorTimer = setTimeout(() => this.hideError(), 3500)
|
||
} else {
|
||
this.hideError()
|
||
}
|
||
}
|
||
|
||
hideError() {
|
||
const wrap = this.$("#eventform-toast-wrap")
|
||
if (!wrap) return
|
||
clearTimeout(this._errorTimer)
|
||
wrap.style.maxHeight = "0"
|
||
wrap.style.opacity = "0"
|
||
wrap.style.paddingTop = "0"
|
||
}
|
||
|
||
getFormData() {
|
||
const isAllDay = this.$('[name="all_day"]')?.checked ?? true;
|
||
return {
|
||
title: this.$('[name="title"]').value,
|
||
location: this.$('[name="location"]').value,
|
||
time_start: isAllDay
|
||
? this.$('[name="time_start_date"]').value
|
||
: `${this.$('[name="time_start_date"]').value}T${this.$('[name="time_start_time"]').value}`,
|
||
time_end: isAllDay
|
||
? this.$('[name="time_end_date"]').value
|
||
: `${this.$('[name="time_end_date"]').value}T${this.$('[name="time_end_time"]').value}`,
|
||
all_day: isAllDay,
|
||
description: this.$('[name="description"]').value,
|
||
recurrence: this.recurrence
|
||
};
|
||
}
|
||
|
||
isNewEventDirty() {
|
||
const data = this.getFormData()
|
||
const o = this.originalFormData
|
||
const newCalIds = this.selectedCalendars.map(c => c.id).sort().join(",")
|
||
return (
|
||
data.title !== o.title ||
|
||
(data.location || "") !== o.location ||
|
||
(data.description || "") !== o.description ||
|
||
data.all_day !== o.all_day ||
|
||
data.time_start !== o.time_start ||
|
||
data.time_end !== o.time_end ||
|
||
newCalIds !== o.calendars ||
|
||
this.uploadedFiles.length > 0 ||
|
||
JSON.stringify(data.recurrence) !== JSON.stringify(o.recurrence)
|
||
)
|
||
}
|
||
|
||
handleBack() {
|
||
const isDirty = this.isNewEventDirty() || (!!this.editEvent && this.attachmentsDeleted);
|
||
if (isDirty) {
|
||
$('actionsheetpopup-').show(
|
||
"Discard Changes?",
|
||
[
|
||
{
|
||
label: "Discard",
|
||
onTap: () => {
|
||
if (this.onBack) {
|
||
this.onBack()
|
||
} else {
|
||
$("bottomsheet-")._closeOverride = null
|
||
$("bottomsheet-").setSheet(false)
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: "Keep Editing",
|
||
destructive: false,
|
||
onTap: () => {}
|
||
},
|
||
],
|
||
() => {}
|
||
)
|
||
return;
|
||
}
|
||
if (this.onBack) {
|
||
this.onBack()
|
||
} else {
|
||
$("bottomsheet-")._closeOverride = null
|
||
$("bottomsheet-").setSheet(false)
|
||
}
|
||
}
|
||
|
||
handleDelete() {
|
||
const event = this.editEvent;
|
||
const isRecurring = !!(event?._isOccurrence || event?.recurrence_parent_id || event?.recurrence_id);
|
||
if (isRecurring) {
|
||
$('actionsheetpopup-').show(
|
||
"Delete Recurring Event",
|
||
[
|
||
{ label: "Delete just this event", onTap: () => this.performDelete('single') },
|
||
{ label: "Delete this and future events", onTap: () => this.performDelete('future') },
|
||
{ label: "Delete all events in series", onTap: () => this.performDelete('all') },
|
||
],
|
||
() => {}
|
||
)
|
||
return;
|
||
}
|
||
this.performDelete(null);
|
||
}
|
||
|
||
async performDelete(scope) {
|
||
const event = this.editEvent;
|
||
const isOverride = !!event.recurrence_parent_id;
|
||
const templateId = isOverride ? event.recurrence_parent_id : event.id;
|
||
const occurrenceDate = isOverride
|
||
? (event.recurrence_exception_date instanceof Date
|
||
? event.recurrence_exception_date.toISOString()
|
||
: event.recurrence_exception_date) ?? null
|
||
: event._occurrenceDate?.toISOString() ?? null;
|
||
const serverEventId = (scope === 'single' && isOverride) ? event.id : templateId;
|
||
|
||
try {
|
||
const result = await server.deleteEvent(serverEventId, global.currentNetwork.id, scope, occurrenceDate);
|
||
if (result.status === 200) {
|
||
const deleteResult = { scope: scope ?? 'all', templateId, occurrenceDate, overrideId: isOverride ? event.id : null };
|
||
if (this.onDelete) this.onDelete(deleteResult);
|
||
} else {
|
||
this.showError(result.error ?? "Failed to delete event.");
|
||
}
|
||
} catch (err) {
|
||
console.error("Failed to delete event:", err);
|
||
this.showError("Failed to delete event.");
|
||
}
|
||
}
|
||
|
||
async performSave(scope) {
|
||
const eventData = this.pendingSaveEventData;
|
||
this.pendingSaveEventData = null;
|
||
|
||
const event = this.editEvent;
|
||
const isOverride = !!event.recurrence_parent_id;
|
||
const templateId = isOverride ? event.recurrence_parent_id : event.id;
|
||
const occurrenceDate = isOverride
|
||
? (event.recurrence_exception_date instanceof Date
|
||
? event.recurrence_exception_date.toISOString()
|
||
: event.recurrence_exception_date) ?? null
|
||
: event._occurrenceDate?.toISOString() ?? null;
|
||
|
||
const toISO = (str, isEnd = false) => calendarUtil.toISO(str, eventData.all_day, isEnd)
|
||
|
||
// scope='all': anchor dates to the template's start, not the tapped occurrence, to preserve earlier occurrences
|
||
let time_start = toISO(eventData.time_start);
|
||
let time_end = toISO(eventData.time_end, true);
|
||
if (scope === 'all' && event._templateStart) {
|
||
const tDateStr = calendarUtil.toDateInput(event._templateStart);
|
||
const tEndDateStr = calendarUtil.toDateInput(event._templateEnd ?? event._templateStart);
|
||
// Compute day offset between the tapped occurrence's date and the user's new date,
|
||
// then apply that same offset to the template's start/end dates.
|
||
// Zero offset (date unchanged) = previous behavior; non-zero = intentional date shift.
|
||
const shiftDate = (base, offset) =>
|
||
calendarUtil.toDateInput(calendarUtil.addDays(new Date(base + 'T00:00:00'), offset));
|
||
const daysBetween = (a, b) =>
|
||
Math.round((new Date(a + 'T00:00:00') - new Date(b + 'T00:00:00')) / 86400000);
|
||
const occStartDate = calendarUtil.toDateInput(event.time_start instanceof Date ? event.time_start : new Date(event.time_start));
|
||
|
||
if (eventData.all_day) {
|
||
const startShift = daysBetween(eventData.time_start, occStartDate);
|
||
const span = daysBetween(eventData.time_end, eventData.time_start);
|
||
const newTStart = shiftDate(tDateStr, startShift);
|
||
const newTEnd = shiftDate(newTStart, span);
|
||
time_start = toISO(newTStart);
|
||
time_end = toISO(newTEnd, true);
|
||
} else {
|
||
const occEndDate = calendarUtil.toDateInput(event.time_end instanceof Date ? event.time_end : new Date(event.time_end));
|
||
const formStart = eventData.time_start.includes('T') ? eventData.time_start.split('T')[0] : eventData.time_start;
|
||
const formEnd = eventData.time_end.includes('T') ? eventData.time_end.split('T')[0] : eventData.time_end;
|
||
const startT = eventData.time_start.includes('T') ? eventData.time_start.split('T')[1] : '00:00';
|
||
const endT = eventData.time_end.includes('T') ? eventData.time_end.split('T')[1] : '00:00';
|
||
const newTStart = shiftDate(tDateStr, daysBetween(formStart, occStartDate));
|
||
const newTEnd = shiftDate(tEndDateStr, daysBetween(formEnd, occEndDate));
|
||
time_start = toISO(`${newTStart}T${startT}`);
|
||
time_end = toISO(`${newTEnd}T${endT}`, true);
|
||
}
|
||
}
|
||
|
||
// For weekly recurrence, if the user shifted the start to a different day of week,
|
||
// replace the old anchor day in days_of_week so the first occurrence isn't skipped.
|
||
let recurrence = eventData.recurrence ? { ...eventData.recurrence } : null;
|
||
if (scope !== 'single' && recurrence?.frequency === 'weekly' && recurrence.days_of_week?.length > 0) {
|
||
const newDay = new Date(time_start).getDay();
|
||
const oldDayRef = scope === 'all' && event._templateStart
|
||
? new Date(event._templateStart)
|
||
: occurrenceDate ? new Date(occurrenceDate) : null;
|
||
const originalDay = oldDayRef?.getDay() ?? null;
|
||
if (newDay !== originalDay && !recurrence.days_of_week.includes(newDay)) {
|
||
const days = [...recurrence.days_of_week];
|
||
recurrence = {
|
||
...recurrence,
|
||
days_of_week: (originalDay !== null && days.includes(originalDay))
|
||
? days.map(d => d === originalDay ? newDay : d)
|
||
: [...days, newDay].sort((a, b) => a - b)
|
||
};
|
||
}
|
||
}
|
||
|
||
const payload = {
|
||
title: eventData.title || "New event",
|
||
location: eventData.location || null,
|
||
description: eventData.description || null,
|
||
time_start,
|
||
time_end,
|
||
all_day: eventData.all_day,
|
||
calendars: this.selectedCalendars.map(c => c.id),
|
||
scope,
|
||
exception_date: occurrenceDate,
|
||
recurrence
|
||
}
|
||
|
||
try {
|
||
// Override rows: pass event.id (direct UPDATE); virtual occurrences: pass templateId (INSERT override)
|
||
const serverEventId = (scope === 'single' && isOverride) ? event.id : templateId;
|
||
const result = await server.editEvent(serverEventId, payload, global.currentNetwork.id)
|
||
if (result.status === 200) {
|
||
// Run deletions against the final event ID so that for scope='single'/'future' (new event row)
|
||
// we remove from the new event (which inherited parent files) not from the original template
|
||
await this.performPendingDeletions(result.event.id)
|
||
const attachments = await this.uploadAndMergeFiles(result.event.id, [...this.existingAttachments])
|
||
const editResult = {
|
||
scope,
|
||
event: { ...result.event, calendars: this.selectedCalendars.map(c => c.id), attachments, recurrence: eventData.recurrence ?? null },
|
||
templateId,
|
||
occurrenceDate
|
||
};
|
||
if (this.updateEvents) {
|
||
this.updateEvents(editResult);
|
||
} else {
|
||
$("bottomsheet-").toggle();
|
||
}
|
||
} else {
|
||
this.rerender();
|
||
this.showError(result.error ?? "Failed to save event.")
|
||
this.onSaveError?.()
|
||
}
|
||
} catch (err) {
|
||
console.error("Failed to save event:", err);
|
||
this.rerender();
|
||
this.showError("Failed to save event.")
|
||
this.onSaveError?.()
|
||
}
|
||
}
|
||
|
||
handleDeleteNewFile(index) {
|
||
this.uploadedFiles.splice(index, 1);
|
||
this.fileListComponent.removeNew(index);
|
||
this.updateFilesChevron();
|
||
}
|
||
|
||
handleDeleteAttachment(fileId) {
|
||
this.pendingDeletions.push(fileId)
|
||
this.existingAttachments = this.existingAttachments.filter(f => f.id !== fileId)
|
||
this.fileListComponent.removeExisting(fileId)
|
||
this.updateFilesChevron()
|
||
this.attachmentsDeleted = true
|
||
}
|
||
|
||
async performPendingDeletions(eventId = null) {
|
||
const targetId = eventId ?? this.editEvent.id;
|
||
for (const fileId of this.pendingDeletions) {
|
||
try {
|
||
await mobileUtil.authFetch(`${config.SERVER}/events/${targetId}/attachments/${fileId}`, {
|
||
method: "DELETE",
|
||
credentials: "include"
|
||
})
|
||
} catch (err) {
|
||
console.error("Failed to delete attachment:", err)
|
||
this.showError("Failed to delete attachment.")
|
||
}
|
||
}
|
||
this.pendingDeletions = []
|
||
}
|
||
|
||
async uploadAndMergeFiles(eventId, existing = []) {
|
||
if (this.uploadedFiles.length === 0) return existing;
|
||
const uploadResult = await this.handleUpload(eventId);
|
||
if (!uploadResult.success) {
|
||
this.uploadedFiles = [];
|
||
this.showError("Failed to upload attachment(s).")
|
||
return existing;
|
||
}
|
||
const existingIds = new Set(existing.map(a => a.id));
|
||
const uniqueNew = uploadResult.insertedFiles.filter(a => !existingIds.has(a.id));
|
||
return [...existing, ...uniqueNew];
|
||
}
|
||
|
||
async handleSend(eventData) {
|
||
this.hideError()
|
||
|
||
const toISO = (str, isEnd = false) => calendarUtil.toISO(str, eventData.all_day, isEnd)
|
||
const startTimestamp = toISO(eventData.time_start);
|
||
const endTimestamp = toISO(eventData.time_end, true);
|
||
|
||
const eventPayload = {
|
||
title: eventData.title || "New event",
|
||
location: eventData.location || null,
|
||
time_start: startTimestamp,
|
||
time_end: endTimestamp,
|
||
all_day: eventData.all_day,
|
||
calendars: this.selectedCalendars.map(({ id }) => id),
|
||
description: eventData.description || null,
|
||
recurrence: eventData.recurrence ?? null
|
||
}
|
||
|
||
if (this.editEvent) {
|
||
const newCalIds = this.selectedCalendars.map(c => c.id).sort().join(",");
|
||
const unchanged =
|
||
eventData.title === this.originalFormData.title &&
|
||
(eventData.location || "") === this.originalFormData.location &&
|
||
(eventData.description || "") === this.originalFormData.description &&
|
||
eventData.all_day === this.originalFormData.all_day &&
|
||
eventData.time_start === this.originalFormData.time_start &&
|
||
eventData.time_end === this.originalFormData.time_end &&
|
||
newCalIds === this.originalFormData.calendars &&
|
||
this.uploadedFiles.length === 0 &&
|
||
!this.attachmentsDeleted &&
|
||
JSON.stringify(eventData.recurrence) === JSON.stringify(this.originalFormData.recurrence);
|
||
|
||
if (unchanged) {
|
||
this.onBack ? this.onBack() : $("bottomsheet-").toggle();
|
||
return;
|
||
}
|
||
|
||
const isRecurring = !!(this.editEvent._isOccurrence || this.editEvent.recurrence_parent_id || this.editEvent.recurrence_id);
|
||
if (isRecurring) {
|
||
this.pendingSaveEventData = eventData;
|
||
$('actionsheetpopup-').show(
|
||
"Edit Recurring Event",
|
||
[
|
||
{ label: "Edit just this event", onTap: () => this.performSave('single'), destructive: false },
|
||
{ label: "Edit this and future events", onTap: () => this.performSave('future'), destructive: false },
|
||
{ label: "Edit all events in series", onTap: () => this.performSave('all'), destructive: false },
|
||
],
|
||
() => { this.pendingSaveEventData = null; }
|
||
)
|
||
return;
|
||
}
|
||
|
||
await this.performPendingDeletions()
|
||
|
||
const result = await server.editEvent(this.editEvent.id, eventPayload, global.currentNetwork.id)
|
||
if (result.status === 200) {
|
||
const attachments = await this.uploadAndMergeFiles(result.event.id, [...this.existingAttachments]);
|
||
if (this.updateEvents) {
|
||
this.updateEvents({ ...result.event, calendars: this.selectedCalendars.map(c => c.id), attachments, recurrence: eventData.recurrence ?? null });
|
||
} else {
|
||
$("bottomsheet-").toggle();
|
||
}
|
||
} else {
|
||
this.showError(result.error ?? "Failed to save event.")
|
||
this.onSaveError?.()
|
||
}
|
||
} else {
|
||
const result = await server.addEvent(eventPayload, global.currentNetwork.id)
|
||
if (result.status === 200) {
|
||
const attachments = await this.uploadAndMergeFiles(result.event.id);
|
||
// Clear override so save button doesn't re-trigger trySave
|
||
$("bottomsheet-")._closeOverride = null
|
||
$("bottomsheet-").toggle();
|
||
setTimeout(() => {
|
||
this.updateEvents({ ...result.event, calendars: this.selectedCalendars.map(c => c.id), attachments });
|
||
}, 300);
|
||
} else {
|
||
this.showError(result.error ?? "Failed to save event.")
|
||
this.onSaveError?.()
|
||
}
|
||
}
|
||
}
|
||
|
||
async handleUpload(eventId) {
|
||
try {
|
||
const body = new FormData();
|
||
Array.from(this.uploadedFiles).forEach(file => {
|
||
body.append('attachments', file);
|
||
})
|
||
|
||
const res = await mobileUtil.authFetch(`${config.SERVER}/events/${eventId}/upload-attachments`, {
|
||
method: "POST",
|
||
credentials: "include",
|
||
headers: {
|
||
"Accept": "application/json"
|
||
},
|
||
body: body
|
||
});
|
||
|
||
const { insertedFiles } = await res.json();
|
||
return { success: res.ok, insertedFiles: res.ok ? insertedFiles : [] };
|
||
} catch (err) {
|
||
console.log("Failed to add attachment to event: ", eventId)
|
||
return { success: false, error: "Failed to add attachment(s)" };
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
register(EventForm)
|