diff --git a/app/Http/Controllers/Meet/Concerns/ResolvesEventsLinkContext.php b/app/Http/Controllers/Meet/Concerns/ResolvesEventsLinkContext.php new file mode 100644 index 0000000..e1e546a --- /dev/null +++ b/app/Http/Controllers/Meet/Concerns/ResolvesEventsLinkContext.php @@ -0,0 +1,24 @@ +>} */ + protected function eventsLinkContext(Request $request, Room $room): array + { + $canManageEvents = app(MeetPermissions::class)->can($this->member($request), 'meetings.manage'); + + $linkableEvents = $canManageEvents && ! EventsSourceLink::isLinked($room) + ? app(EventsLinkService::class)->linkableEvents($request->user()) + : []; + + return compact('canManageEvents', 'linkableEvents'); + } +} diff --git a/app/Http/Controllers/Meet/ConferenceController.php b/app/Http/Controllers/Meet/ConferenceController.php index ec8ccb5..5ecb1f2 100644 --- a/app/Http/Controllers/Meet/ConferenceController.php +++ b/app/Http/Controllers/Meet/ConferenceController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Meet; use App\Http\Controllers\Controller; use App\Http\Controllers\Meet\Concerns\BuildsMeetIndexPage; +use App\Http\Controllers\Meet\Concerns\ResolvesEventsLinkContext; use App\Http\Controllers\Meet\Concerns\ScopesToAccount; use App\Models\Member; use App\Models\Room; @@ -20,7 +21,7 @@ use Illuminate\View\View; class ConferenceController extends Controller { - use BuildsMeetIndexPage, ScopesToAccount; + use BuildsMeetIndexPage, ResolvesEventsLinkContext, ScopesToAccount; public function __construct( protected RoomService $rooms, @@ -124,7 +125,9 @@ class ConferenceController extends Controller $room->load(['sessions.participants', 'invitations', 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']); - return view('meet.conferences.show', compact('room')); + extract($this->eventsLinkContext($request, $room)); + + return view('meet.conferences.show', compact('room', 'canManageEvents', 'linkableEvents')); } public function updateSpeakers(Request $request, Room $room): RedirectResponse diff --git a/app/Http/Controllers/Meet/EventsLinkController.php b/app/Http/Controllers/Meet/EventsLinkController.php index f2122fc..d702139 100644 --- a/app/Http/Controllers/Meet/EventsLinkController.php +++ b/app/Http/Controllers/Meet/EventsLinkController.php @@ -7,10 +7,8 @@ use App\Http\Controllers\Meet\Concerns\ScopesToAccount; use App\Models\Room; use App\Services\Integrations\EventsClient; use App\Services\Integrations\EventsLinkService; -use App\Support\EventsSourceLink; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\View\View; class EventsLinkController extends Controller { @@ -21,7 +19,7 @@ class EventsLinkController extends Controller protected EventsClient $eventsClient, ) {} - public function show(Request $request, Room $room): View|RedirectResponse + public function show(Request $request, Room $room): RedirectResponse { $this->authorizeAbility($request, 'meetings.manage'); $this->authorizeOwner($request, $room); @@ -33,16 +31,9 @@ class EventsLinkController extends Controller ->withErrors(['events' => 'Ladill Events linking is not configured. Set MEET_API_KEY_EVENTS in Meet (and the matching EVENTS_API_KEY_MEET in Events), then try again.']); } - $events = $this->links->linkableEvents($request->user()); - $expectedType = $room->isConference() ? 'town_hall' : 'webinar'; - - return view('meet.events.link', [ - 'room' => $room, - 'events' => $events, - 'expectedType' => $expectedType, - 'eventsAppUrl' => EventsSourceLink::baseUrl(), - 'isLinked' => EventsSourceLink::isLinked($room), - ]); + return redirect() + ->route($room->isWebinar() ? 'meet.webinars.show' : 'meet.conferences.show', $room) + ->with('open_events_link_modal', true); } public function store(Request $request, Room $room): RedirectResponse @@ -51,11 +42,21 @@ class EventsLinkController extends Controller $this->authorizeOwner($request, $room); abort_unless($room->isWebinar() || $room->isConference(), 404); - $validated = $request->validate([ + $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [ 'event_id' => ['required', 'integer', 'min:1'], 'virtual_session_id' => ['nullable', 'string', 'max:64'], ]); + if ($validator->fails()) { + return redirect() + ->route($room->isWebinar() ? 'meet.webinars.show' : 'meet.conferences.show', $room) + ->withErrors($validator) + ->withInput() + ->with('open_events_link_modal', true); + } + + $validated = $validator->validated(); + try { $this->links->link( $room, @@ -64,7 +65,11 @@ class EventsLinkController extends Controller $validated['virtual_session_id'] ?? null, ); } catch (\RuntimeException $e) { - return back()->withErrors(['events' => $e->getMessage()])->withInput(); + return redirect() + ->route($room->isWebinar() ? 'meet.webinars.show' : 'meet.conferences.show', $room) + ->withErrors(['events' => $e->getMessage()]) + ->withInput() + ->with('open_events_link_modal', true); } $route = $room->isWebinar() ? 'meet.webinars.show' : 'meet.conferences.show'; diff --git a/app/Http/Controllers/Meet/WebinarController.php b/app/Http/Controllers/Meet/WebinarController.php index 442bd89..817d23d 100644 --- a/app/Http/Controllers/Meet/WebinarController.php +++ b/app/Http/Controllers/Meet/WebinarController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Meet; use App\Http\Controllers\Controller; use App\Http\Controllers\Meet\Concerns\BuildsMeetIndexPage; +use App\Http\Controllers\Meet\Concerns\ResolvesEventsLinkContext; use App\Http\Controllers\Meet\Concerns\ScopesToAccount; use App\Models\Room; use App\Services\Meet\CalendarService; @@ -16,7 +17,7 @@ use Illuminate\View\View; class WebinarController extends Controller { - use BuildsMeetIndexPage, ScopesToAccount; + use BuildsMeetIndexPage, ResolvesEventsLinkContext, ScopesToAccount; public function __construct( protected RoomService $rooms, @@ -113,7 +114,9 @@ class WebinarController extends Controller $room->load(['sessions.participants', 'invitations', 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']); - return view('meet.webinars.show', compact('room')); + extract($this->eventsLinkContext($request, $room)); + + return view('meet.webinars.show', compact('room', 'canManageEvents', 'linkableEvents')); } public function start(Request $request, Room $room): RedirectResponse diff --git a/resources/views/meet/events/link.blade.php b/resources/views/meet/events/link.blade.php index 0fb9f77..4799830 100644 --- a/resources/views/meet/events/link.blade.php +++ b/resources/views/meet/events/link.blade.php @@ -13,50 +13,12 @@ @endif - @if ($events === []) -
-

No virtual or hybrid events found in your Ladill Events account.

- Create event in Events -
- @else -
- @csrf - -
- - -
- -
- -

Pick an empty session matching this {{ $room->isConference() ? 'conference' : 'webinar' }}, or leave blank to attach to the first available {{ $expectedType === 'town_hall' ? 'conference' : 'webinar' }} slot.

- -
- - -
- @endif +
+ @include('meet.partials.events-link-form', [ + 'room' => $room, + 'events' => $events, + 'expectedType' => $expectedType, + ]) +
diff --git a/resources/views/meet/partials/events-integration.blade.php b/resources/views/meet/partials/events-integration.blade.php index ce8de73..18b8a5f 100644 --- a/resources/views/meet/partials/events-integration.blade.php +++ b/resources/views/meet/partials/events-integration.blade.php @@ -7,6 +7,11 @@ $eventsConfigured = EventsSourceLink::isApiConfigured(); $createEventUrl = EventsSourceLink::createEventUrl($room); $kind = $room->isConference() ? 'conference' : 'webinar'; + $expectedType = $room->isConference() ? 'town_hall' : 'webinar'; + $linkableEvents = $linkableEvents ?? []; + $canManageEvents = $canManageEvents ?? false; + $unlinkModalName = 'events-unlink-'.$room->uuid; + $linkModalName = 'events-link-'.$room->uuid; @endphp
@@ -24,12 +29,19 @@

@endif
- @if ($isLinked) -
- @csrf - @method('DELETE') - -
+ @if ($isLinked && $canManageEvents) + + + + + @endif @@ -84,8 +96,14 @@

Programme speakers and panelists are configured on the event programme in Events and sync to this session when linked.

@else
- @if ($eventsConfigured) - Link to Ladill Events + @if ($eventsConfigured && $canManageEvents) + + @elseif ($eventsConfigured) + Link to Ladill Events @else Link to Ladill Events @endif @@ -93,3 +111,34 @@
@endif + +@if (! $isLinked && $eventsConfigured && $canManageEvents) + @php + $openLinkModal = session('open_events_link_modal') + || $errors->hasAny(['events', 'event_id', 'virtual_session_id']); + @endphp + +
+

Link to Ladill Events

+

+ Choose a virtual or hybrid event. Registration, bulk invitations, speakers, and QR codes will be managed in Events. +

+ + @if ($errors->has('events')) +
+ {{ $errors->first('events') }} +
+ @endif + +
+ @include('meet.partials.events-link-form', [ + 'room' => $room, + 'events' => $linkableEvents, + 'expectedType' => $expectedType, + 'submitLabel' => 'Link event', + 'submitClass' => 'btn-primary btn-primary-sm', + ]) +
+
+
+@endif diff --git a/resources/views/meet/partials/events-link-form.blade.php b/resources/views/meet/partials/events-link-form.blade.php new file mode 100644 index 0000000..e94889d --- /dev/null +++ b/resources/views/meet/partials/events-link-form.blade.php @@ -0,0 +1,53 @@ +@props([ + 'room', + 'events' => [], + 'expectedType' => 'town_hall', + 'submitLabel' => 'Link event', + 'submitClass' => 'btn-primary btn-primary-lg w-full', +]) + +@if ($events === []) +
+

No virtual or hybrid events found in your Ladill Events account.

+ Create event in Events +
+@else +
+ @csrf + +
+ + +
+ +
+ +

Pick an empty session matching this {{ $room->isConference() ? 'conference' : 'webinar' }}, or leave blank to attach to the first available {{ $expectedType === 'town_hall' ? 'conference' : 'webinar' }} slot.

+ +
+ + +
+@endif diff --git a/tests/Feature/MeetWebTest.php b/tests/Feature/MeetWebTest.php index b470443..30e4b5f 100644 --- a/tests/Feature/MeetWebTest.php +++ b/tests/Feature/MeetWebTest.php @@ -2008,6 +2008,57 @@ class MeetWebTest extends TestCase ->assertDontSee('/qr-codes/create', false); } + public function test_conference_events_link_uses_app_modals_not_native_confirm(): void + { + config([ + 'meet.events_api_url' => 'https://events.test/api/service/v1', + 'meet.events_api_key' => 'test-events-key', + ]); + + Http::fake([ + 'https://events.test/api/service/v1/events*' => Http::response(['events' => []]), + ]); + + $room = Room::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'host_user_ref' => $this->user->public_id, + 'title' => 'Leadership conference', + 'type' => 'town_hall', + 'status' => 'scheduled', + 'scheduled_at' => now()->addDay(), + 'timezone' => 'UTC', + 'settings' => config('meet.default_settings'), + 'source' => [ + 'app' => 'events', + 'entity_type' => 'virtual_session', + 'entity_id' => 'vs-1', + 'event_id' => '42', + 'event_title' => 'Summit', + ], + ]); + + $this->actingAs($this->user) + ->get(route('meet.conferences.show', $room)) + ->assertOk() + ->assertSee('Remove Events link?', false) + ->assertSee('data-modal-name="events-unlink-'.$room->uuid.'"', false) + ->assertDontSee("confirm('Remove the Events link?", false); + + $room->update(['source' => null]); + + $this->actingAs($this->user) + ->get(route('meet.conferences.show', $room)) + ->assertOk() + ->assertSee('open-modal', false) + ->assertSee('data-modal-name="events-link-'.$room->uuid.'"', false) + ->assertSee('Link to Ladill Events', false); + + $this->actingAs($this->user) + ->get(route('meet.events.link', $room)) + ->assertRedirect(route('meet.conferences.show', $room)); + } + public function test_afia_chat_returns_reply_when_ai_configured(): void { config([