From 071163b47b4b86994c0a74b9fafa8009a350eb84 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 26 Feb 2024 20:41:27 -0700 Subject: [PATCH] Add Curated Onboarding Templates --- .../AdminCuratedRegisterController.php | 183 +++++++++++++----- app/Models/CuratedRegisterTemplate.php | 19 ++ ...reate_curated_register_templates_table.php | 32 +++ .../partials/activity-log.blade.php | 58 +++++- .../curated-register/partials/nav.blade.php | 5 +- .../template-create.blade.php | 98 ++++++++++ .../curated-register/template-edit.blade.php | 148 ++++++++++++++ .../curated-register/templates.blade.php | 91 +++++++++ routes/web-admin.php | 7 + 9 files changed, 590 insertions(+), 51 deletions(-) create mode 100644 app/Models/CuratedRegisterTemplate.php create mode 100644 database/migrations/2024_02_24_105641_create_curated_register_templates_table.php create mode 100644 resources/views/admin/curated-register/template-create.blade.php create mode 100644 resources/views/admin/curated-register/template-edit.blade.php create mode 100644 resources/views/admin/curated-register/templates.blade.php diff --git a/app/Http/Controllers/AdminCuratedRegisterController.php b/app/Http/Controllers/AdminCuratedRegisterController.php index 1bed66b7..91400afb 100644 --- a/app/Http/Controllers/AdminCuratedRegisterController.php +++ b/app/Http/Controllers/AdminCuratedRegisterController.php @@ -2,69 +2,72 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\Models\CuratedRegister; -use App\Models\CuratedRegisterActivity; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Mail; -use App\Mail\CuratedRegisterRequestDetailsFromUser; use App\Mail\CuratedRegisterAcceptUser; use App\Mail\CuratedRegisterRejectUser; +use App\Mail\CuratedRegisterRequestDetailsFromUser; +use App\Models\CuratedRegister; +use App\Models\CuratedRegisterActivity; +use App\Models\CuratedRegisterTemplate; use App\User; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Str; class AdminCuratedRegisterController extends Controller { public function __construct() { - $this->middleware(['auth','admin']); + $this->middleware(['auth', 'admin']); } public function index(Request $request) { $this->validate($request, [ 'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses', - 'sort' => 'sometimes|in:asc,desc' + 'sort' => 'sometimes|in:asc,desc', ]); $filter = $request->input('filter', 'open'); $sort = $request->input('sort', 'asc'); - $records = CuratedRegister::when($filter, function($q, $filter) { - if($filter === 'open') { - return $q->where('is_rejected', false) - ->where(function($query) { + $records = CuratedRegister::when($filter, function ($q, $filter) { + if ($filter === 'open') { + return $q->where('is_rejected', false) + ->where(function ($query) { return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false); }) ->whereNotNull('email_verified_at') ->whereIsClosed(false); - } else if($filter === 'all') { - return $q; - } else if($filter === 'responses') { - return $q->whereIsClosed(false) - ->whereNotNull('email_verified_at') - ->where('user_has_responded', true) - ->where('is_awaiting_more_info', true); - } elseif ($filter === 'awaiting') { - return $q->whereIsClosed(false) - ->where('is_rejected', false) - ->where('is_approved', false) - ->where('user_has_responded', false) - ->where('is_awaiting_more_info', true); - } elseif ($filter === 'approved') { - return $q->whereIsClosed(true)->whereIsApproved(true); - } elseif ($filter === 'rejected') { - return $q->whereIsClosed(true)->whereIsRejected(true); - } - }) - ->when($sort, function($query, $sort) { + } elseif ($filter === 'all') { + return $q; + } elseif ($filter === 'responses') { + return $q->whereIsClosed(false) + ->whereNotNull('email_verified_at') + ->where('user_has_responded', true) + ->where('is_awaiting_more_info', true); + } elseif ($filter === 'awaiting') { + return $q->whereIsClosed(false) + ->where('is_rejected', false) + ->where('is_approved', false) + ->where('user_has_responded', false) + ->where('is_awaiting_more_info', true); + } elseif ($filter === 'approved') { + return $q->whereIsClosed(true)->whereIsApproved(true); + } elseif ($filter === 'rejected') { + return $q->whereIsClosed(true)->whereIsRejected(true); + } + }) + ->when($sort, function ($query, $sort) { return $query->orderBy('id', $sort); }) ->paginate(10) ->withQueryString(); + return view('admin.curated-register.index', compact('records', 'filter')); } public function show(Request $request, $id) { $record = CuratedRegister::findOrFail($id); + return view('admin.curated-register.show', compact('record')); } @@ -80,10 +83,10 @@ class AdminCuratedRegisterController extends Controller 'message' => null, 'link' => null, 'timestamp' => $record->created_at, - ] + ], ]); - if($record->email_verified_at) { + if ($record->email_verified_at) { $res->push([ 'id' => 3, 'action' => 'email_verified_at', @@ -99,10 +102,11 @@ class AdminCuratedRegisterController extends Controller $idx = 4; $userResponses = collect([]); - foreach($activities as $activity) { + foreach ($activities as $activity) { $idx++; - if($activity->from_user) { + if ($activity->from_user) { $userResponses->push($activity); + continue; } $res->push([ @@ -116,20 +120,22 @@ class AdminCuratedRegisterController extends Controller ]); } - foreach($userResponses as $ur) { - $res = $res->map(function($r) use($ur) { - if(!isset($r['aid'])) { + foreach ($userResponses as $ur) { + $res = $res->map(function ($r) use ($ur) { + if (! isset($r['aid'])) { return $r; } - if($ur->reply_to_id === $r['aid']) { + if ($ur->reply_to_id === $r['aid']) { $r['user_response'] = $ur; + return $r; } + return $r; }); } - if($record->is_approved) { + if ($record->is_approved) { $idx++; $res->push([ 'id' => $idx, @@ -139,7 +145,7 @@ class AdminCuratedRegisterController extends Controller 'link' => null, 'timestamp' => $record->action_taken_at, ]); - } else if ($record->is_rejected) { + } elseif ($record->is_rejected) { $idx++; $res->push([ 'id' => $idx, @@ -157,13 +163,14 @@ class AdminCuratedRegisterController extends Controller public function apiMessagePreviewStore(Request $request, $id) { $record = CuratedRegister::findOrFail($id); + return $request->all(); } public function apiMessageSendStore(Request $request, $id) { $this->validate($request, [ - 'message' => 'required|string|min:5|max:1000' + 'message' => 'required|string|min:5|max:1000', ]); $record = CuratedRegister::findOrFail($id); abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); @@ -179,6 +186,7 @@ class AdminCuratedRegisterController extends Controller $record->user_has_responded = false; $record->save(); Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity)); + return $request->all(); } @@ -188,22 +196,23 @@ class AdminCuratedRegisterController extends Controller abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); $activity = new CuratedRegisterActivity; $activity->message = $request->input('message'); + return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity); } - public function previewMessageShow(Request $request, $id) { $record = CuratedRegister::findOrFail($id); abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); $record->message = $request->input('message'); + return new \App\Mail\CuratedRegisterSendMessage($record); } public function apiHandleReject(Request $request, $id) { $this->validate($request, [ - 'action' => 'required|in:reject-email,reject-silent' + 'action' => 'required|in:reject-email,reject-silent', ]); $action = $request->input('action'); $record = CuratedRegister::findOrFail($id); @@ -212,9 +221,10 @@ class AdminCuratedRegisterController extends Controller $record->is_closed = true; $record->action_taken_at = now(); $record->save(); - if($action === 'reject-email') { + if ($action === 'reject-email') { Mail::to($record->email)->send(new CuratedRegisterRejectUser($record)); } + return [200]; } @@ -233,10 +243,89 @@ class AdminCuratedRegisterController extends Controller 'password' => $record->password, 'app_register_ip' => $record->ip_address, 'email_verified_at' => now(), - 'register_source' => 'cur_onboarding' + 'register_source' => 'cur_onboarding', ]); Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record)); + return [200]; } + + public function templates(Request $request) + { + $templates = CuratedRegisterTemplate::paginate(10); + + return view('admin.curated-register.templates', compact('templates')); + } + + public function templateCreate(Request $request) + { + return view('admin.curated-register.template-create'); + } + + public function templateEdit(Request $request, $id) + { + $template = CuratedRegisterTemplate::findOrFail($id); + + return view('admin.curated-register.template-edit', compact('template')); + } + + public function templateEditStore(Request $request, $id) + { + $this->validate($request, [ + 'name' => 'required|string|max:30', + 'content' => 'required|string|min:5|max:3000', + 'description' => 'nullable|sometimes|string|max:1000', + 'active' => 'sometimes', + ]); + $template = CuratedRegisterTemplate::findOrFail($id); + $template->name = $request->input('name'); + $template->content = $request->input('content'); + $template->description = $request->input('description'); + $template->is_active = $request->boolean('active'); + $template->save(); + + return redirect()->back()->with('status', 'Successfully updated template!'); + } + + public function templateDelete(Request $request, $id) + { + $template = CuratedRegisterTemplate::findOrFail($id); + $template->delete(); + + return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!'); + } + + public function templateStore(Request $request) + { + $this->validate($request, [ + 'name' => 'required|string|max:30', + 'content' => 'required|string|min:5|max:3000', + 'description' => 'nullable|sometimes|string|max:1000', + 'active' => 'sometimes', + ]); + CuratedRegisterTemplate::create([ + 'name' => $request->input('name'), + 'content' => $request->input('content'), + 'description' => $request->input('description'), + 'is_active' => $request->boolean('active'), + ]); + + return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!'); + } + + public function getActiveTemplates(Request $request) + { + $templates = CuratedRegisterTemplate::whereIsActive(true) + ->orderBy('order') + ->get() + ->map(function ($tmp) { + return [ + 'name' => $tmp->name, + 'content' => $tmp->content, + ]; + }); + + return response()->json($templates); + } } diff --git a/app/Models/CuratedRegisterTemplate.php b/app/Models/CuratedRegisterTemplate.php new file mode 100644 index 00000000..a5def0ce --- /dev/null +++ b/app/Models/CuratedRegisterTemplate.php @@ -0,0 +1,19 @@ + 'boolean', + ]; +} diff --git a/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php b/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php new file mode 100644 index 00000000..4829fefb --- /dev/null +++ b/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name')->nullable(); + $table->text('description')->nullable(); + $table->text('content')->nullable(); + $table->boolean('is_active')->default(false)->index(); + $table->tinyInteger('order')->default(10)->unsigned()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_register_templates'); + } +}; diff --git a/resources/views/admin/curated-register/partials/activity-log.blade.php b/resources/views/admin/curated-register/partials/activity-log.blade.php index 1d7701f4..d141a442 100644 --- a/resources/views/admin/curated-register/partials/activity-log.blade.php +++ b/resources/views/admin/curated-register/partials/activity-log.blade.php @@ -46,6 +46,21 @@

Request Additional Details

Use this form to request additional details. Once you press Send, we'll send the potential user an email with a special link they can visit with a form that they can provide additional details with. You can also Preview the email before it's sent.

+
+

Template Responses

+ +
+ +
+
+
@@ -60,7 +75,7 @@

@{{ composeMessage && composeMessage.length ? composeMessage.length : 0 }} / - 500 + 2000

@@ -76,6 +91,12 @@ target="_blank"> Preview + + Clear +
@@ -90,6 +111,21 @@

Send Message

Use this form to send a message to the applicant. Once you press Send, we'll send the potential user an email with your message. You can also Preview the email before it's sent.

+
+

Template Responses

+ +
+ +
+
+
@@ -187,11 +223,13 @@ messageFormOpen: false, composeMessage: null, messageBody: null, + responseTemplates: [], } }, mounted() { setTimeout(() => { + this.fetchResponseTemplates(); this.fetchActivities(); }, 1000) }, @@ -233,10 +271,16 @@ return str; }, + fetchResponseTemplates() { + axios.get('/i/admin/api/curated-onboarding/templates/get') + .then(res => { + this.responseTemplates = res.data; + }) + }, + fetchActivities() { axios.get('/i/admin/api/curated-onboarding/show/{{$id}}/activity-log') .then(res => { - console.log(res.data); this.activities = res.data; }) .finally(() => { @@ -379,7 +423,15 @@ openUserResponse(activity) { swal('User Response', activity.user_response.message) - } + }, + + useTemplate(tmpl) { + this.composeMessage = tmpl.content; + }, + + useTemplateMessage(tmpl) { + this.messageBody = tmpl.content; + }, } }); diff --git a/resources/views/admin/curated-register/partials/nav.blade.php b/resources/views/admin/curated-register/partials/nav.blade.php index b23cc8dc..8634813a 100644 --- a/resources/views/admin/curated-register/partials/nav.blade.php +++ b/resources/views/admin/curated-register/partials/nav.blade.php @@ -15,7 +15,7 @@
diff --git a/resources/views/admin/curated-register/template-create.blade.php b/resources/views/admin/curated-register/template-create.blade.php new file mode 100644 index 00000000..5c37abfc --- /dev/null +++ b/resources/views/admin/curated-register/template-create.blade.php @@ -0,0 +1,98 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Curated Onboarding

+

The ideal solution for communities seeking a balance between open registration and invite-only membership

+
+
+
+
+
+ +@if((bool) config_cache('instance.curated_registration.enabled')) +
+
+ @include('admin.curated-register.partials.nav') + +
+
+
+
+

Create Template

+

Create re-usable templates of messages and application requests.

+
+
+
+
+
+
+
+
+ @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif +
+ @csrf + +
+ + +
+ +
+ + +
+ +

+ +

+ +
+
+ + +
+
+ +
+ + +
+ +
+ +
+
+
+
+
+
+
+@endif + +@endsection diff --git a/resources/views/admin/curated-register/template-edit.blade.php b/resources/views/admin/curated-register/template-edit.blade.php new file mode 100644 index 00000000..236b979d --- /dev/null +++ b/resources/views/admin/curated-register/template-edit.blade.php @@ -0,0 +1,148 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Curated Onboarding

+

The ideal solution for communities seeking a balance between open registration and invite-only membership

+
+
+
+
+
+ +@if((bool) config_cache('instance.curated_registration.enabled')) +
+
+ @include('admin.curated-register.partials.nav') + +
+
+ @if (session('status')) +
+ {{ session('status') }} +
+ + @endif +
+
+

Edit Template

+
+
+
+
+
+
+
+
+ @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif +
+ @csrf + +
+ + +
+ +
+ + +
+ + @if($template->description == null) +

+ +

+ @endif + +
+
+ + +
+
+ +
+ is_active ? 'checked' : ''}}> + +
+ +
+
+ + +
+
+
+ @method('DELETE') + @csrf +
+
+
+
+
+
+
+@endif + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/admin/curated-register/templates.blade.php b/resources/views/admin/curated-register/templates.blade.php new file mode 100644 index 00000000..17b10da8 --- /dev/null +++ b/resources/views/admin/curated-register/templates.blade.php @@ -0,0 +1,91 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Curated Onboarding

+

The ideal solution for communities seeking a balance between open registration and invite-only membership

+
+
+
+
+
+ +@if((bool) config_cache('instance.curated_registration.enabled')) +
+
+ @include('admin.curated-register.partials.nav') + +
+
+ @if (session('status')) +
+ {{ session('status') }} +
+ + @endif +
+
+

Create and manage re-usable templates of messages and application requests.

+ Create new Template +
+
+
+ +
+ +
+ + + + + + + + + + + + @foreach($templates as $template) + + + + + + + + @endforeach + +
IDShortcut/NameContentActiveCreated
+ + {{ $template->id }} + + + {{ $template->name }} + + {{ str_limit($template->content, 80) }} + + {{ $template->is_active ? '✅' : '❌' }} + + {{ $template->created_at->format('M d Y') }} +
+ +
+ {{ $templates->links() }} +
+
+
+
+
+
+@endif + +@endsection diff --git a/routes/web-admin.php b/routes/web-admin.php index 9f01d5c7..64a3ecff 100644 --- a/routes/web-admin.php +++ b/routes/web-admin.php @@ -106,6 +106,12 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::get('asf/home', 'AdminShadowFilterController@home'); Route::redirect('curated-onboarding/', 'curated-onboarding/home'); Route::get('curated-onboarding/home', 'AdminCuratedRegisterController@index')->name('admin.curated-onboarding'); + Route::get('curated-onboarding/templates', 'AdminCuratedRegisterController@templates')->name('admin.curated-onboarding.templates'); + Route::get('curated-onboarding/templates/create', 'AdminCuratedRegisterController@templateCreate')->name('admin.curated-onboarding.create-template'); + Route::post('curated-onboarding/templates/create', 'AdminCuratedRegisterController@templateStore'); + Route::get('curated-onboarding/templates/edit/{id}', 'AdminCuratedRegisterController@templateEdit'); + Route::post('curated-onboarding/templates/edit/{id}', 'AdminCuratedRegisterController@templateEditStore'); + Route::delete('curated-onboarding/templates/edit/{id}', 'AdminCuratedRegisterController@templateDelete'); Route::get('curated-onboarding/show/{id}/preview-details-message', 'AdminCuratedRegisterController@previewDetailsMessageShow'); Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow'); Route::get('curated-onboarding/show/{id}', 'AdminCuratedRegisterController@show'); @@ -162,5 +168,6 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::post('curated-onboarding/show/{id}/message/send', 'AdminCuratedRegisterController@apiMessageSendStore'); Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject'); Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove'); + Route::get('curated-onboarding/templates/get', 'AdminCuratedRegisterController@getActiveTemplates'); }); });