Adds calendar dropdown to new event modal
This commit is contained in:
parent
909fae5eb4
commit
1a08e0e855
@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Calendar;
|
||||
use App\Models\CalendarInstance;
|
||||
use App\Models\Event;
|
||||
use App\Models\Location;
|
||||
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->buildAttendeeFormData($request));
|
||||
$data = array_merge($data, $this->buildCalendarPickerData($request, $calendar, null));
|
||||
|
||||
if ($request->header('HX-Request') === 'true') {
|
||||
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 = array_merge($data, $this->buildRecurrenceFormData($request, $start ?? '', $tz, $rrule));
|
||||
$data = array_merge($data, $this->buildAttendeeFormData($request, $event));
|
||||
$data = array_merge($data, $this->buildCalendarPickerData($request, $calendar, $event));
|
||||
|
||||
if ($request->header('HX-Request') === 'true') {
|
||||
return view('event.partials.form-modal', $data);
|
||||
@ -202,6 +205,7 @@ class EventController extends Controller
|
||||
'location' => ['nullable', 'string'],
|
||||
'all_day' => ['sometimes', 'boolean'],
|
||||
'category' => ['nullable', 'string', 'max:50'],
|
||||
'calendar_uri' => ['nullable', 'string', 'max:255'],
|
||||
'repeat_frequency' => [
|
||||
function (string $attribute, mixed $value, $fail) {
|
||||
if ($value === null || $value === '') {
|
||||
@ -254,6 +258,8 @@ class EventController extends Controller
|
||||
}
|
||||
|
||||
$data = $request->validate($rules);
|
||||
$targetCalendar = $this->resolveTargetCalendar($request, $calendar);
|
||||
$this->authorize('update', $targetCalendar);
|
||||
|
||||
$tz = $this->displayTimezone($calendar, $request);
|
||||
$isAllDay = (bool) ($data['all_day'] ?? false);
|
||||
@ -294,7 +300,7 @@ class EventController extends Controller
|
||||
]);
|
||||
|
||||
$event = Event::create([
|
||||
'calendarid' => $calendar->id,
|
||||
'calendarid' => $targetCalendar->id,
|
||||
'uri' => Str::uuid() . '.ics',
|
||||
'lastmodified' => time(),
|
||||
'etag' => md5($ical),
|
||||
@ -323,7 +329,7 @@ class EventController extends Controller
|
||||
|
||||
$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'],
|
||||
'all_day' => ['sometimes', 'boolean'],
|
||||
'category' => ['nullable', 'string', 'max:50'],
|
||||
'calendar_uri' => ['nullable', 'string', 'max:255'],
|
||||
'repeat_frequency' => [
|
||||
function (string $attribute, mixed $value, $fail) {
|
||||
if ($value === null || $value === '') {
|
||||
@ -392,6 +399,8 @@ class EventController extends Controller
|
||||
}
|
||||
|
||||
$data = $request->validate($rules);
|
||||
$targetCalendar = $this->resolveTargetCalendar($request, $calendar);
|
||||
$this->authorize('update', $targetCalendar);
|
||||
|
||||
$tz = $this->displayTimezone($calendar, $request);
|
||||
$isAllDay = (bool) ($data['all_day'] ?? false);
|
||||
@ -435,6 +444,7 @@ class EventController extends Controller
|
||||
]);
|
||||
|
||||
$event->update([
|
||||
'calendarid' => $targetCalendar->id,
|
||||
'calendardata' => $ical,
|
||||
'etag' => md5($ical),
|
||||
'lastmodified' => time(),
|
||||
@ -456,7 +466,7 @@ class EventController extends Controller
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
@ -9,6 +9,13 @@ const SELECTORS = {
|
||||
colorPickerColor: '[data-colorpicker-color]',
|
||||
colorPickerHex: '[data-colorpicker-hex]',
|
||||
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]',
|
||||
eventStartInput: '[data-event-start]',
|
||||
eventEndInput: '[data-event-end]',
|
||||
@ -1510,6 +1517,69 @@ function initColorPickers(root = document) {
|
||||
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)
|
||||
@ -1652,6 +1722,7 @@ window.addEventListener('resize', () => {
|
||||
*/
|
||||
|
||||
function initUI() {
|
||||
initCalendarPickers();
|
||||
initColorPickers();
|
||||
initNaturalEventParser();
|
||||
initEventAllDayToggles();
|
||||
@ -1675,6 +1746,7 @@ document.addEventListener('htmx:afterSwap', (e) => {
|
||||
}
|
||||
|
||||
initColorPickers(e.target);
|
||||
initCalendarPickers(e.target);
|
||||
initNaturalEventParser(e.target);
|
||||
initEventAllDayToggles(e.target);
|
||||
initRecurrenceControls(e.target);
|
||||
|
||||
@ -25,15 +25,64 @@
|
||||
<span>Repeat</span>
|
||||
</x-button>
|
||||
</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-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>
|
||||
</li>
|
||||
</menu>
|
||||
<div class="panels">
|
||||
<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 --}}
|
||||
<div class="input-row input-row--1">
|
||||
@ -137,6 +186,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- recurrence --}}
|
||||
<div id="tab-repeat" role="tabpanel" aria-labelledby="tab-btn-repeat" hidden>
|
||||
<div class="input-row input-row--1">
|
||||
<div class="input-cell">
|
||||
@ -248,7 +298,8 @@
|
||||
</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-cell">
|
||||
<input type="hidden" name="attendees_present" value="1">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user