Adds calendar dropdown to new event modal
This commit is contained in:
parent
909fae5eb4
commit
1a08e0e855
@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Calendar;
|
use App\Models\Calendar;
|
||||||
|
use App\Models\CalendarInstance;
|
||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
use App\Models\Location;
|
use App\Models\Location;
|
||||||
use App\Services\Event\EventAttendeeSynchronizer;
|
use App\Services\Event\EventAttendeeSynchronizer;
|
||||||
@ -60,6 +61,7 @@ class EventController extends Controller
|
|||||||
);
|
);
|
||||||
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start, $tz, $rrule));
|
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start, $tz, $rrule));
|
||||||
$data = array_merge($data, $this->buildAttendeeFormData($request));
|
$data = array_merge($data, $this->buildAttendeeFormData($request));
|
||||||
|
$data = array_merge($data, $this->buildCalendarPickerData($request, $calendar, null));
|
||||||
|
|
||||||
if ($request->header('HX-Request') === 'true') {
|
if ($request->header('HX-Request') === 'true') {
|
||||||
return view('event.partials.form-modal', $data);
|
return view('event.partials.form-modal', $data);
|
||||||
@ -100,6 +102,7 @@ class EventController extends Controller
|
|||||||
$data = compact('calendar', 'instance', 'event', 'start', 'end', 'tz', 'rrule');
|
$data = compact('calendar', 'instance', 'event', 'start', 'end', 'tz', 'rrule');
|
||||||
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start ?? '', $tz, $rrule));
|
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start ?? '', $tz, $rrule));
|
||||||
$data = array_merge($data, $this->buildAttendeeFormData($request, $event));
|
$data = array_merge($data, $this->buildAttendeeFormData($request, $event));
|
||||||
|
$data = array_merge($data, $this->buildCalendarPickerData($request, $calendar, $event));
|
||||||
|
|
||||||
if ($request->header('HX-Request') === 'true') {
|
if ($request->header('HX-Request') === 'true') {
|
||||||
return view('event.partials.form-modal', $data);
|
return view('event.partials.form-modal', $data);
|
||||||
@ -202,6 +205,7 @@ class EventController extends Controller
|
|||||||
'location' => ['nullable', 'string'],
|
'location' => ['nullable', 'string'],
|
||||||
'all_day' => ['sometimes', 'boolean'],
|
'all_day' => ['sometimes', 'boolean'],
|
||||||
'category' => ['nullable', 'string', 'max:50'],
|
'category' => ['nullable', 'string', 'max:50'],
|
||||||
|
'calendar_uri' => ['nullable', 'string', 'max:255'],
|
||||||
'repeat_frequency' => [
|
'repeat_frequency' => [
|
||||||
function (string $attribute, mixed $value, $fail) {
|
function (string $attribute, mixed $value, $fail) {
|
||||||
if ($value === null || $value === '') {
|
if ($value === null || $value === '') {
|
||||||
@ -254,6 +258,8 @@ class EventController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$data = $request->validate($rules);
|
$data = $request->validate($rules);
|
||||||
|
$targetCalendar = $this->resolveTargetCalendar($request, $calendar);
|
||||||
|
$this->authorize('update', $targetCalendar);
|
||||||
|
|
||||||
$tz = $this->displayTimezone($calendar, $request);
|
$tz = $this->displayTimezone($calendar, $request);
|
||||||
$isAllDay = (bool) ($data['all_day'] ?? false);
|
$isAllDay = (bool) ($data['all_day'] ?? false);
|
||||||
@ -294,7 +300,7 @@ class EventController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$event = Event::create([
|
$event = Event::create([
|
||||||
'calendarid' => $calendar->id,
|
'calendarid' => $targetCalendar->id,
|
||||||
'uri' => Str::uuid() . '.ics',
|
'uri' => Str::uuid() . '.ics',
|
||||||
'lastmodified' => time(),
|
'lastmodified' => time(),
|
||||||
'etag' => md5($ical),
|
'etag' => md5($ical),
|
||||||
@ -323,7 +329,7 @@ class EventController extends Controller
|
|||||||
|
|
||||||
$attendeeSync->syncRows($event, $attendees);
|
$attendeeSync->syncRows($event, $attendees);
|
||||||
|
|
||||||
return redirect()->route('calendar.show', $calendar);
|
return redirect()->route('calendar.show', $targetCalendar);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -351,6 +357,7 @@ class EventController extends Controller
|
|||||||
'location' => ['nullable', 'string'],
|
'location' => ['nullable', 'string'],
|
||||||
'all_day' => ['sometimes', 'boolean'],
|
'all_day' => ['sometimes', 'boolean'],
|
||||||
'category' => ['nullable', 'string', 'max:50'],
|
'category' => ['nullable', 'string', 'max:50'],
|
||||||
|
'calendar_uri' => ['nullable', 'string', 'max:255'],
|
||||||
'repeat_frequency' => [
|
'repeat_frequency' => [
|
||||||
function (string $attribute, mixed $value, $fail) {
|
function (string $attribute, mixed $value, $fail) {
|
||||||
if ($value === null || $value === '') {
|
if ($value === null || $value === '') {
|
||||||
@ -392,6 +399,8 @@ class EventController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$data = $request->validate($rules);
|
$data = $request->validate($rules);
|
||||||
|
$targetCalendar = $this->resolveTargetCalendar($request, $calendar);
|
||||||
|
$this->authorize('update', $targetCalendar);
|
||||||
|
|
||||||
$tz = $this->displayTimezone($calendar, $request);
|
$tz = $this->displayTimezone($calendar, $request);
|
||||||
$isAllDay = (bool) ($data['all_day'] ?? false);
|
$isAllDay = (bool) ($data['all_day'] ?? false);
|
||||||
@ -435,6 +444,7 @@ class EventController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$event->update([
|
$event->update([
|
||||||
|
'calendarid' => $targetCalendar->id,
|
||||||
'calendardata' => $ical,
|
'calendardata' => $ical,
|
||||||
'etag' => md5($ical),
|
'etag' => md5($ical),
|
||||||
'lastmodified' => time(),
|
'lastmodified' => time(),
|
||||||
@ -456,7 +466,7 @@ class EventController extends Controller
|
|||||||
|
|
||||||
$attendeeSync->syncRows($event, $attendees);
|
$attendeeSync->syncRows($event, $attendees);
|
||||||
|
|
||||||
return redirect()->route('calendar.show', $calendar);
|
return redirect()->route('calendar.show', $targetCalendar);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -835,6 +845,69 @@ class EventController extends Controller
|
|||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildCalendarPickerData(Request $request, Calendar $calendar, ?Event $event = null): array
|
||||||
|
{
|
||||||
|
$instances = CalendarInstance::query()
|
||||||
|
->forUser($request->user())
|
||||||
|
->withUiMeta()
|
||||||
|
->ordered()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$selectedUri = old('calendar_uri');
|
||||||
|
|
||||||
|
if (!$selectedUri) {
|
||||||
|
if ($event) {
|
||||||
|
$selectedUri = $instances->firstWhere('calendarid', $event->calendarid)?->uri;
|
||||||
|
} else {
|
||||||
|
$selectedUri = $instances->first()?->uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected = $instances->firstWhere('uri', $selectedUri)
|
||||||
|
?? $instances->firstWhere('calendarid', $calendar->id)
|
||||||
|
?? $instances->first();
|
||||||
|
|
||||||
|
$calendarPickerOptions = $instances->map(function (CalendarInstance $instance) {
|
||||||
|
$color = $instance->resolvedColor($instance->calendarcolor);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uri' => $instance->uri,
|
||||||
|
'name' => $instance->displayname ?: __('common.calendar'),
|
||||||
|
'color' => $color,
|
||||||
|
];
|
||||||
|
})->values()->all();
|
||||||
|
|
||||||
|
$selectedCalendarUri = $selected?->uri;
|
||||||
|
$selectedCalendarName = $selected?->displayname ?: __('common.calendar');
|
||||||
|
$selectedCalendarColor = $selected?->resolvedColor($selected?->calendarcolor) ?? '#64748b';
|
||||||
|
|
||||||
|
return compact(
|
||||||
|
'calendarPickerOptions',
|
||||||
|
'selectedCalendarUri',
|
||||||
|
'selectedCalendarName',
|
||||||
|
'selectedCalendarColor',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTargetCalendar(Request $request, Calendar $fallback): Calendar
|
||||||
|
{
|
||||||
|
$uri = trim((string) $request->input('calendar_uri', ''));
|
||||||
|
if ($uri === '') {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = CalendarInstance::query()
|
||||||
|
->forUser($request->user())
|
||||||
|
->where('uri', $uri)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$instance) {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Calendar::query()->find($instance->calendarid) ?? $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* resolve location_id from hints or geocoding
|
* resolve location_id from hints or geocoding
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -9,6 +9,13 @@ const SELECTORS = {
|
|||||||
colorPickerColor: '[data-colorpicker-color]',
|
colorPickerColor: '[data-colorpicker-color]',
|
||||||
colorPickerHex: '[data-colorpicker-hex]',
|
colorPickerHex: '[data-colorpicker-hex]',
|
||||||
colorPickerRandom: '[data-colorpicker-random]',
|
colorPickerRandom: '[data-colorpicker-random]',
|
||||||
|
calendarPicker: '[data-calendar-picker]',
|
||||||
|
calendarPickerInput: '[data-calendar-picker-input]',
|
||||||
|
calendarPickerToggle: '[data-calendar-picker-toggle]',
|
||||||
|
calendarPickerMenu: '[data-calendar-picker-menu]',
|
||||||
|
calendarPickerLabel: '[data-calendar-picker-label]',
|
||||||
|
calendarPickerColorDot: '[data-calendar-picker-color]',
|
||||||
|
calendarPickerOption: '[data-calendar-picker-option]',
|
||||||
eventAllDayToggle: '[data-all-day-toggle]',
|
eventAllDayToggle: '[data-all-day-toggle]',
|
||||||
eventStartInput: '[data-event-start]',
|
eventStartInput: '[data-event-start]',
|
||||||
eventEndInput: '[data-event-end]',
|
eventEndInput: '[data-event-end]',
|
||||||
@ -1510,6 +1517,69 @@ function initColorPickers(root = document) {
|
|||||||
root.querySelectorAll(SELECTORS.colorPicker).forEach(wire);
|
root.querySelectorAll(SELECTORS.colorPicker).forEach(wire);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* calendar picker (custom dropdown with color chip)
|
||||||
|
*/
|
||||||
|
function initCalendarPickers(root = document) {
|
||||||
|
root.querySelectorAll(SELECTORS.calendarPicker).forEach((picker) => {
|
||||||
|
if (picker.__calendarPickerWired) return;
|
||||||
|
picker.__calendarPickerWired = true;
|
||||||
|
|
||||||
|
const input = picker.querySelector(SELECTORS.calendarPickerInput);
|
||||||
|
const toggle = picker.querySelector(SELECTORS.calendarPickerToggle);
|
||||||
|
const menu = picker.querySelector(SELECTORS.calendarPickerMenu);
|
||||||
|
const label = picker.querySelector(SELECTORS.calendarPickerLabel);
|
||||||
|
const colorDot = picker.querySelector(SELECTORS.calendarPickerColorDot);
|
||||||
|
|
||||||
|
if (!input || !toggle || !menu || !label || !colorDot) return;
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
menu.classList.add('hidden');
|
||||||
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
menu.classList.remove('hidden');
|
||||||
|
toggle.setAttribute('aria-expanded', 'true');
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (menu.classList.contains('hidden')) open();
|
||||||
|
else close();
|
||||||
|
});
|
||||||
|
|
||||||
|
picker.addEventListener('click', (event) => {
|
||||||
|
const option = event.target.closest(SELECTORS.calendarPickerOption);
|
||||||
|
if (!option || !picker.contains(option)) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const uri = option.dataset.calendarPickerUri || '';
|
||||||
|
const name = option.dataset.calendarPickerName || '';
|
||||||
|
const color = option.dataset.calendarPickerColor || '#64748b';
|
||||||
|
|
||||||
|
input.value = uri;
|
||||||
|
label.textContent = name;
|
||||||
|
colorDot.style.backgroundColor = color;
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
if (!picker.contains(event.target)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* month view overflow handling (progressive enhancement)
|
* month view overflow handling (progressive enhancement)
|
||||||
@ -1652,6 +1722,7 @@ window.addEventListener('resize', () => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function initUI() {
|
function initUI() {
|
||||||
|
initCalendarPickers();
|
||||||
initColorPickers();
|
initColorPickers();
|
||||||
initNaturalEventParser();
|
initNaturalEventParser();
|
||||||
initEventAllDayToggles();
|
initEventAllDayToggles();
|
||||||
@ -1675,6 +1746,7 @@ document.addEventListener('htmx:afterSwap', (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initColorPickers(e.target);
|
initColorPickers(e.target);
|
||||||
|
initCalendarPickers(e.target);
|
||||||
initNaturalEventParser(e.target);
|
initNaturalEventParser(e.target);
|
||||||
initEventAllDayToggles(e.target);
|
initEventAllDayToggles(e.target);
|
||||||
initRecurrenceControls(e.target);
|
initRecurrenceControls(e.target);
|
||||||
|
|||||||
@ -25,15 +25,64 @@
|
|||||||
<span>Repeat</span>
|
<span>Repeat</span>
|
||||||
</x-button>
|
</x-button>
|
||||||
</li>
|
</li>
|
||||||
<li id="tab-invitees" role="tab" aria-controls="tab-invitees" aria-selected="false">
|
<li id="tab-attendees" role="tab" aria-controls="tab-attendees" aria-selected="false">
|
||||||
<x-button type="button">
|
<x-button type="button">
|
||||||
<x-icon-user-circle width="20" />
|
<x-icon-user-circle width="20" />
|
||||||
<span>Invitees</span>
|
<span>Guests</span>
|
||||||
|
</x-button>
|
||||||
|
</li>
|
||||||
|
<li id="tab-description" role="tab" aria-controls="tab-description" aria-selected="false">
|
||||||
|
<x-button type="button">
|
||||||
|
<x-icon-user-circle width="20" />
|
||||||
|
<span>Description</span>
|
||||||
</x-button>
|
</x-button>
|
||||||
</li>
|
</li>
|
||||||
</menu>
|
</menu>
|
||||||
<div class="panels">
|
<div class="panels">
|
||||||
<div id="tab-details" role="tabpanel" aria-labelledby="tab-btn-details">
|
<div id="tab-details" role="tabpanel" aria-labelledby="tab-btn-details">
|
||||||
|
{{-- Calendar --}}
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="calendar_uri" :value="__('common.calendar')" />
|
||||||
|
|
||||||
|
<div class="relative" data-calendar-picker>
|
||||||
|
<input type="hidden" id="calendar_uri" name="calendar_uri" value="{{ $selectedCalendarUri ?? '' }}" data-calendar-picker-input>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button--secondary w-full justify-between"
|
||||||
|
data-calendar-picker-toggle
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="inline-flex items-center gap-2">
|
||||||
|
<span class="inline-block h-3 w-3 rounded-full" data-calendar-picker-color style="background-color: {{ $selectedCalendarColor ?? '#64748b' }}"></span>
|
||||||
|
<span data-calendar-picker-label>{{ $selectedCalendarName ?? __('common.calendar') }}</span>
|
||||||
|
</span>
|
||||||
|
<x-icon-chevron-down width="18" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="absolute z-20 mt-2 w-full rounded-md border-md border-secondary bg-white shadow-sm hidden" data-calendar-picker-menu>
|
||||||
|
<ul class="list-none p-1 m-0 flex flex-col gap-1">
|
||||||
|
@foreach (($calendarPickerOptions ?? []) as $option)
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button button--tertiary w-full justify-start"
|
||||||
|
data-calendar-picker-option
|
||||||
|
data-calendar-picker-uri="{{ $option['uri'] }}"
|
||||||
|
data-calendar-picker-name="{{ $option['name'] }}"
|
||||||
|
data-calendar-picker-color="{{ $option['color'] }}"
|
||||||
|
>
|
||||||
|
<span class="inline-block h-3 w-3 rounded-full" style="background-color: {{ $option['color'] }}"></span>
|
||||||
|
<span>{{ $option['name'] }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Title --}}
|
{{-- Title --}}
|
||||||
<div class="input-row input-row--1">
|
<div class="input-row input-row--1">
|
||||||
@ -137,6 +186,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- recurrence --}}
|
||||||
<div id="tab-repeat" role="tabpanel" aria-labelledby="tab-btn-repeat" hidden>
|
<div id="tab-repeat" role="tabpanel" aria-labelledby="tab-btn-repeat" hidden>
|
||||||
<div class="input-row input-row--1">
|
<div class="input-row input-row--1">
|
||||||
<div class="input-cell">
|
<div class="input-cell">
|
||||||
@ -248,7 +298,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tab-invitees" role="tabpanel" aria-labelledby="tab-btn-invitees" hidden>
|
{{-- attendees --}}
|
||||||
|
<div id="tab-attendees" role="tabpanel" aria-labelledby="tab-btn-attendees" hidden>
|
||||||
<div class="input-row input-row--1">
|
<div class="input-row input-row--1">
|
||||||
<div class="input-cell">
|
<div class="input-cell">
|
||||||
<input type="hidden" name="attendees_present" value="1">
|
<input type="hidden" name="attendees_present" value="1">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user