Merge pull request #4958 from pixelfed/staging

Add Curated Onboarding Templates
This commit is contained in:
daniel 2024-02-26 21:05:24 -07:00 committed by GitHub
commit 8c1e136ce9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 590 additions and 51 deletions

View file

@ -2,69 +2,72 @@
namespace App\Http\Controllers; 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\CuratedRegisterAcceptUser;
use App\Mail\CuratedRegisterRejectUser; use App\Mail\CuratedRegisterRejectUser;
use App\Mail\CuratedRegisterRequestDetailsFromUser;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use App\Models\CuratedRegisterTemplate;
use App\User; use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class AdminCuratedRegisterController extends Controller class AdminCuratedRegisterController extends Controller
{ {
public function __construct() public function __construct()
{ {
$this->middleware(['auth','admin']); $this->middleware(['auth', 'admin']);
} }
public function index(Request $request) public function index(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses', 'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses',
'sort' => 'sometimes|in:asc,desc' 'sort' => 'sometimes|in:asc,desc',
]); ]);
$filter = $request->input('filter', 'open'); $filter = $request->input('filter', 'open');
$sort = $request->input('sort', 'asc'); $sort = $request->input('sort', 'asc');
$records = CuratedRegister::when($filter, function($q, $filter) { $records = CuratedRegister::when($filter, function ($q, $filter) {
if($filter === 'open') { if ($filter === 'open') {
return $q->where('is_rejected', false) return $q->where('is_rejected', false)
->where(function($query) { ->where(function ($query) {
return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false); return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false);
}) })
->whereNotNull('email_verified_at') ->whereNotNull('email_verified_at')
->whereIsClosed(false); ->whereIsClosed(false);
} else if($filter === 'all') { } elseif ($filter === 'all') {
return $q; return $q;
} else if($filter === 'responses') { } elseif ($filter === 'responses') {
return $q->whereIsClosed(false) return $q->whereIsClosed(false)
->whereNotNull('email_verified_at') ->whereNotNull('email_verified_at')
->where('user_has_responded', true) ->where('user_has_responded', true)
->where('is_awaiting_more_info', true); ->where('is_awaiting_more_info', true);
} elseif ($filter === 'awaiting') { } elseif ($filter === 'awaiting') {
return $q->whereIsClosed(false) return $q->whereIsClosed(false)
->where('is_rejected', false) ->where('is_rejected', false)
->where('is_approved', false) ->where('is_approved', false)
->where('user_has_responded', false) ->where('user_has_responded', false)
->where('is_awaiting_more_info', true); ->where('is_awaiting_more_info', true);
} elseif ($filter === 'approved') { } elseif ($filter === 'approved') {
return $q->whereIsClosed(true)->whereIsApproved(true); return $q->whereIsClosed(true)->whereIsApproved(true);
} elseif ($filter === 'rejected') { } elseif ($filter === 'rejected') {
return $q->whereIsClosed(true)->whereIsRejected(true); return $q->whereIsClosed(true)->whereIsRejected(true);
} }
}) })
->when($sort, function($query, $sort) { ->when($sort, function ($query, $sort) {
return $query->orderBy('id', $sort); return $query->orderBy('id', $sort);
}) })
->paginate(10) ->paginate(10)
->withQueryString(); ->withQueryString();
return view('admin.curated-register.index', compact('records', 'filter')); return view('admin.curated-register.index', compact('records', 'filter'));
} }
public function show(Request $request, $id) public function show(Request $request, $id)
{ {
$record = CuratedRegister::findOrFail($id); $record = CuratedRegister::findOrFail($id);
return view('admin.curated-register.show', compact('record')); return view('admin.curated-register.show', compact('record'));
} }
@ -80,10 +83,10 @@ class AdminCuratedRegisterController extends Controller
'message' => null, 'message' => null,
'link' => null, 'link' => null,
'timestamp' => $record->created_at, 'timestamp' => $record->created_at,
] ],
]); ]);
if($record->email_verified_at) { if ($record->email_verified_at) {
$res->push([ $res->push([
'id' => 3, 'id' => 3,
'action' => 'email_verified_at', 'action' => 'email_verified_at',
@ -99,10 +102,11 @@ class AdminCuratedRegisterController extends Controller
$idx = 4; $idx = 4;
$userResponses = collect([]); $userResponses = collect([]);
foreach($activities as $activity) { foreach ($activities as $activity) {
$idx++; $idx++;
if($activity->from_user) { if ($activity->from_user) {
$userResponses->push($activity); $userResponses->push($activity);
continue; continue;
} }
$res->push([ $res->push([
@ -116,20 +120,22 @@ class AdminCuratedRegisterController extends Controller
]); ]);
} }
foreach($userResponses as $ur) { foreach ($userResponses as $ur) {
$res = $res->map(function($r) use($ur) { $res = $res->map(function ($r) use ($ur) {
if(!isset($r['aid'])) { if (! isset($r['aid'])) {
return $r; return $r;
} }
if($ur->reply_to_id === $r['aid']) { if ($ur->reply_to_id === $r['aid']) {
$r['user_response'] = $ur; $r['user_response'] = $ur;
return $r; return $r;
} }
return $r; return $r;
}); });
} }
if($record->is_approved) { if ($record->is_approved) {
$idx++; $idx++;
$res->push([ $res->push([
'id' => $idx, 'id' => $idx,
@ -139,7 +145,7 @@ class AdminCuratedRegisterController extends Controller
'link' => null, 'link' => null,
'timestamp' => $record->action_taken_at, 'timestamp' => $record->action_taken_at,
]); ]);
} else if ($record->is_rejected) { } elseif ($record->is_rejected) {
$idx++; $idx++;
$res->push([ $res->push([
'id' => $idx, 'id' => $idx,
@ -157,13 +163,14 @@ class AdminCuratedRegisterController extends Controller
public function apiMessagePreviewStore(Request $request, $id) public function apiMessagePreviewStore(Request $request, $id)
{ {
$record = CuratedRegister::findOrFail($id); $record = CuratedRegister::findOrFail($id);
return $request->all(); return $request->all();
} }
public function apiMessageSendStore(Request $request, $id) public function apiMessageSendStore(Request $request, $id)
{ {
$this->validate($request, [ $this->validate($request, [
'message' => 'required|string|min:5|max:1000' 'message' => 'required|string|min:5|max:1000',
]); ]);
$record = CuratedRegister::findOrFail($id); $record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); 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->user_has_responded = false;
$record->save(); $record->save();
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity)); Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
return $request->all(); return $request->all();
} }
@ -188,22 +196,23 @@ class AdminCuratedRegisterController extends Controller
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$activity = new CuratedRegisterActivity; $activity = new CuratedRegisterActivity;
$activity->message = $request->input('message'); $activity->message = $request->input('message');
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity); return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
} }
public function previewMessageShow(Request $request, $id) public function previewMessageShow(Request $request, $id)
{ {
$record = CuratedRegister::findOrFail($id); $record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$record->message = $request->input('message'); $record->message = $request->input('message');
return new \App\Mail\CuratedRegisterSendMessage($record); return new \App\Mail\CuratedRegisterSendMessage($record);
} }
public function apiHandleReject(Request $request, $id) public function apiHandleReject(Request $request, $id)
{ {
$this->validate($request, [ $this->validate($request, [
'action' => 'required|in:reject-email,reject-silent' 'action' => 'required|in:reject-email,reject-silent',
]); ]);
$action = $request->input('action'); $action = $request->input('action');
$record = CuratedRegister::findOrFail($id); $record = CuratedRegister::findOrFail($id);
@ -212,9 +221,10 @@ class AdminCuratedRegisterController extends Controller
$record->is_closed = true; $record->is_closed = true;
$record->action_taken_at = now(); $record->action_taken_at = now();
$record->save(); $record->save();
if($action === 'reject-email') { if ($action === 'reject-email') {
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record)); Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
} }
return [200]; return [200];
} }
@ -233,10 +243,89 @@ class AdminCuratedRegisterController extends Controller
'password' => $record->password, 'password' => $record->password,
'app_register_ip' => $record->ip_address, 'app_register_ip' => $record->ip_address,
'email_verified_at' => now(), 'email_verified_at' => now(),
'register_source' => 'cur_onboarding' 'register_source' => 'cur_onboarding',
]); ]);
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record)); Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
return [200]; 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);
}
} }

View file

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CuratedRegisterTemplate extends Model
{
use HasFactory;
protected $fillable = [
'name', 'description', 'content', 'is_active', 'order',
];
protected $casts = [
'is_active' => 'boolean',
];
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('curated_register_templates', function (Blueprint $table) {
$table->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');
}
};

View file

@ -46,6 +46,21 @@
<p class="lead font-weight-bold text-center">Request Additional Details</p> <p class="lead font-weight-bold text-center">Request Additional Details</p>
<p class="text-muted">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.</p> <p class="text-muted">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.</p>
<div v-if="responseTemplates && responseTemplates.length" class="my-3">
<p class="small font-weight-bold mb-1">Template Responses</p>
<div class="d-grid">
<template v-for="tmpl in responseTemplates">
<button
class="btn btn-lighter btn-sm py-1 font-weight-bold rounded-lg text-dark border border-muted px-3"
style="font-size: 13px;"
@click="useTemplate(tmpl)">
<i class="far fa-plus mr-1 text-muted"></i> @{{ tmpl.name.slice(0, 25) }}
</button>
</template>
</div>
</div>
<div class="request-form"> <div class="request-form">
<div class="form-group"> <div class="form-group">
<label for="requestDetailsMessageInput" class="small text-muted">Your Message:</label> <label for="requestDetailsMessageInput" class="small text-muted">Your Message:</label>
@ -60,7 +75,7 @@
<p class="help-text small text-right"> <p class="help-text small text-right">
<span>@{{ composeMessage && composeMessage.length ? composeMessage.length : 0 }}</span> <span>@{{ composeMessage && composeMessage.length ? composeMessage.length : 0 }}</span>
<span>/</span> <span>/</span>
<span>500</span> <span>2000</span>
</p> </p>
</div> </div>
<div class="d-flex"> <div class="d-flex">
@ -76,6 +91,12 @@
target="_blank"> target="_blank">
Preview Preview
</a> </a>
<a
v-if="composeMessage && composeMessage.length"
class="btn btn-outline-danger text-danger rounded-pill btn-sm px-4"
@click="composeMessage = null">
Clear
</a>
</div> </div>
</div> </div>
</div> </div>
@ -90,6 +111,21 @@
<p class="lead font-weight-bold text-center">Send Message</p> <p class="lead font-weight-bold text-center">Send Message</p>
<p class="text-muted">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.</p> <p class="text-muted">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.</p>
<div v-if="responseTemplates && responseTemplates.length" class="my-3">
<p class="small font-weight-bold mb-1">Template Responses</p>
<div class="d-grid">
<template v-for="tmpl in responseTemplates">
<button
class="btn btn-lighter btn-sm py-1 font-weight-bold rounded-lg text-dark border border-muted px-3"
style="font-size: 13px;"
@click="useTemplateMessage(tmpl)">
<i class="far fa-plus mr-1 text-muted"></i> @{{ tmpl.name.slice(0, 25) }}
</button>
</template>
</div>
</div>
<div class="request-form"> <div class="request-form">
<div class="form-group"> <div class="form-group">
<label for="sendMessageInput" class="small text-muted">Your Message:</label> <label for="sendMessageInput" class="small text-muted">Your Message:</label>
@ -187,11 +223,13 @@
messageFormOpen: false, messageFormOpen: false,
composeMessage: null, composeMessage: null,
messageBody: null, messageBody: null,
responseTemplates: [],
} }
}, },
mounted() { mounted() {
setTimeout(() => { setTimeout(() => {
this.fetchResponseTemplates();
this.fetchActivities(); this.fetchActivities();
}, 1000) }, 1000)
}, },
@ -233,10 +271,16 @@
return str; return str;
}, },
fetchResponseTemplates() {
axios.get('/i/admin/api/curated-onboarding/templates/get')
.then(res => {
this.responseTemplates = res.data;
})
},
fetchActivities() { fetchActivities() {
axios.get('/i/admin/api/curated-onboarding/show/{{$id}}/activity-log') axios.get('/i/admin/api/curated-onboarding/show/{{$id}}/activity-log')
.then(res => { .then(res => {
console.log(res.data);
this.activities = res.data; this.activities = res.data;
}) })
.finally(() => { .finally(() => {
@ -379,7 +423,15 @@
openUserResponse(activity) { openUserResponse(activity) {
swal('User Response', activity.user_response.message) swal('User Response', activity.user_response.message)
} },
useTemplate(tmpl) {
this.composeMessage = tmpl.content;
},
useTemplateMessage(tmpl) {
this.messageBody = tmpl.content;
},
} }
}); });

View file

@ -15,7 +15,7 @@
<div class="col-12"> <div class="col-12">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{request()->has('filter') ? '':'active'}}" href="/i/admin/curated-onboarding/home">Open Applications</a> <a class="nav-link {{!request()->is('*home') || request()->has('filter') ? '':'active'}}" href="/i/admin/curated-onboarding/home">Open Applications</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{request()->has('filter') && request()->filter == 'responses' ? 'active':''}}" href="/i/admin/curated-onboarding/home?filter=responses">User Response Replies</a> <a class="nav-link {{request()->has('filter') && request()->filter == 'responses' ? 'active':''}}" href="/i/admin/curated-onboarding/home?filter=responses">User Response Replies</a>
@ -32,6 +32,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{request()->has('filter') && request()->filter == 'all' ? 'active':''}}" href="/i/admin/curated-onboarding/home?filter=all&sort=desc">All Applications</a> <a class="nav-link {{request()->has('filter') && request()->filter == 'all' ? 'active':''}}" href="/i/admin/curated-onboarding/home?filter=all&sort=desc">All Applications</a>
</li> </li>
<li class="nav-item">
<a class="nav-link {{ request()->is('*templates*') ? 'active' : ''}}" href="/i/admin/curated-onboarding/templates">Templates</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -0,0 +1,98 @@
@extends('admin.partial.template-full')
@section('section')
</div><div class="header bg-primary pb-3 mt-n4">
<div class="container-fluid">
<div class="header-body">
<div class="row align-items-center py-4">
<div class="col-lg-8 col-12">
<p class="display-1 text-white d-inline-block mb-0">Curated Onboarding</p>
<p class="text-white mb-0">The ideal solution for communities seeking a balance between open registration and invite-only membership</p>
</div>
</div>
</div>
</div>
</div>
@if((bool) config_cache('instance.curated_registration.enabled'))
<div class="m-n2 m-lg-4">
<div class="container-fluid mt-4">
@include('admin.curated-register.partials.nav')
<div class="row justify-content-center">
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-body">
<h2 class="display-4">Create Template</h2>
<p class="lead my-0">Create re-usable templates of messages and application requests.</p>
</div>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
<p class="font-weight-bold mb-0">{{ $error }}</p>
@endforeach
</div>
@endif
<form method="post">
@csrf
<div class="form-group">
<label class="small font-weight-bold">Shortcut/Name</label>
<input
class="form-control"
name="name"
value="{{old('name')}}"
placeholder="An optional name/shortcut for easy access" />
</div>
<div class="form-group">
<label class="small font-weight-bold">Content</label>
<textarea
class="form-control"
name="content"
value="{{old('content')}}"
rows="8"
placeholder="Add your custom message template here..."></textarea>
</div>
<p class="font-weight-bold">
<a class="font-weight-bold small" data-toggle="collapse" href="#collapseDescription" aria-expanded="false" aria-controls="collapseDescription">
Add optional description
</a>
</p>
<div class="collapse" id="collapseDescription">
<div class="form-group">
<label class="small font-weight-bold">Description</label>
<textarea
class="form-control"
name="description"
rows="4"
placeholder="Add an optional description that is only visible to admins..."></textarea>
</div>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="active" name="active" checked>
<label class="custom-control-label font-weight-bold" for="active">Mark as Active</label>
</div>
<hr>
<button class="btn btn-primary btn-block rounded-pill font-weight-bold">Create Template</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
@endsection

View file

@ -0,0 +1,148 @@
@extends('admin.partial.template-full')
@section('section')
</div><div class="header bg-primary pb-3 mt-n4">
<div class="container-fluid">
<div class="header-body">
<div class="row align-items-center py-4">
<div class="col-lg-8 col-12">
<p class="display-1 text-white d-inline-block mb-0">Curated Onboarding</p>
<p class="text-white mb-0">The ideal solution for communities seeking a balance between open registration and invite-only membership</p>
</div>
</div>
</div>
</div>
</div>
@if((bool) config_cache('instance.curated_registration.enabled'))
<div class="m-n2 m-lg-4">
<div class="container-fluid mt-4">
@include('admin.curated-register.partials.nav')
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
@if (session('status'))
<div class="alert alert-success font-weight-bold lead" id="shm">
{{ session('status') }}
</div>
<script>
setTimeout(() => document.getElementById('shm').classList.add('animate__animated', 'animate__bounceOutLeft'), 2000);
setTimeout(() => document.getElementById('shm').style.display = 'none', 2500);
</script>
@endif
<div class="card">
<div class="card-body">
<h2 class="display-4 mb-0">Edit Template</h2>
</div>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<div class="card">
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
<p class="font-weight-bold mb-0">{{ $error }}</p>
@endforeach
</div>
@endif
<form method="post" id="updateForm">
@csrf
<div class="form-group">
<label class="small font-weight-bold">Shortcut/Name</label>
<input
class="form-control"
name="name"
value="{{$template->name}}"
placeholder="An optional name/shortcut for easy access" />
</div>
<div class="form-group">
<label class="small font-weight-bold">Content</label>
<textarea
class="form-control"
name="content"
rows="{{$template->content && strlen($template->content) > 500 ? 16 : 5}}"
placeholder="Add your custom message template here...">{{$template->content}}</textarea>
</div>
@if($template->description == null)
<p class="font-weight-bold">
<a class="font-weight-bold small" data-toggle="collapse" href="#collapseDescription" aria-expanded="false" aria-controls="collapseDescription">
Add optional description
</a>
</p>
@endif
<div class="collapse {{ $template->description === null ? '':'show'}}" id="collapseDescription">
<div class="form-group">
<label class="small font-weight-bold">Description</label>
<textarea
class="form-control"
name="description"
rows="4"
placeholder="Add an optional description that is only visible to admins...">{{ $template->description }}</textarea>
</div>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="active" name="active" {{ $template->is_active ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="active">Mark as Active</label>
</div>
<hr>
<div class="d-flex">
<button type="button" class="btn btn-primary flex-grow-1 rounded-pill font-weight-bold" id="saveBtn">Save</button>
<button type="button" class="btn btn-danger rounded-pill font-weight-bold" id="deleteBtn">Delete</button>
</div>
</form>
<form method="post" id="deleteForm">
@method('DELETE')
@csrf
</form>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
@endsection
@push('scripts')
<script>
$('#saveBtn').click(() => {
$('#updateForm').submit()
})
$('#deleteBtn').click(() => {
swal({
title: 'Confirm Deletion',
text: 'Are you sure you want to delete this template? It will not be recoverable',
icon: 'warning',
dangerMode: true,
buttons: {
close: {
text: "Close",
value: "close",
close: true,
className: "swal-button--cancel"
},
confirm: {
text: "Delete",
value: "delete",
className: "btn-danger"
}
}
}).then(res => {
if(res == 'delete') {
$('#deleteForm').submit();
// window.location.href = '/i/admin/curated-onboarding/templates';
}
})
})
</script>
@endpush

View file

@ -0,0 +1,91 @@
@extends('admin.partial.template-full')
@section('section')
</div><div class="header bg-primary pb-3 mt-n4">
<div class="container-fluid">
<div class="header-body">
<div class="row align-items-center py-4">
<div class="col-lg-8 col-12">
<p class="display-1 text-white d-inline-block mb-0">Curated Onboarding</p>
<p class="text-white mb-0">The ideal solution for communities seeking a balance between open registration and invite-only membership</p>
</div>
</div>
</div>
</div>
</div>
@if((bool) config_cache('instance.curated_registration.enabled'))
<div class="m-n2 m-lg-4">
<div class="container-fluid mt-4">
@include('admin.curated-register.partials.nav')
<div class="row">
<div class="col-12">
@if (session('status'))
<div class="alert alert-success font-weight-bold lead" id="shm">
{{ session('status') }}
</div>
<script>
setTimeout(() => document.getElementById('shm').classList.add('animate__animated', 'animate__bounceOutLeft'), 2000);
setTimeout(() => document.getElementById('shm').style.display = 'none', 2500);
</script>
@endif
<div class="card">
<div class="card-body d-flex justify-content-between align-items-center">
<p class="lead my-0">Create and manage re-usable templates of messages and application requests.</p>
<a class="btn btn-primary font-weight-bold rounded-pill" href="{{route('admin.curated-onboarding.create-template')}}">Create new Template</a>
</div>
</div>
</div>
<div class="col-12">
<div class="table-responsive rounded">
<table class="table table-dark">
<thead class="thead-dark">
<tr>
<th scope="col">ID</th>
<th scope="col">Shortcut/Name</th>
<th scope="col">Content</th>
<th scope="col">Active</th>
<th scope="col">Created</th>
</tr>
</thead>
<tbody>
@foreach($templates as $template)
<tr>
<td class="align-middle">
<a
href="/i/admin/curated-onboarding/templates/edit/{{$template->id}}"
class="font-weight-bold">
{{ $template->id }}
</a>
</td>
<td class="align-middle">
{{ $template->name }}
</td>
<td class="align-middle">
{{ str_limit($template->content, 80) }}
</td>
<td class="align-middle">
{{ $template->is_active ? '✅' : '❌' }}
</td>
<td class="align-middle">
{{ $template->created_at->format('M d Y') }}
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="d-flex mt-3">
{{ $templates->links() }}
</div>
</div>
</div>
</div>
</div>
</div>
@endif
@endsection

View file

@ -106,6 +106,12 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::get('asf/home', 'AdminShadowFilterController@home'); Route::get('asf/home', 'AdminShadowFilterController@home');
Route::redirect('curated-onboarding/', 'curated-onboarding/home'); Route::redirect('curated-onboarding/', 'curated-onboarding/home');
Route::get('curated-onboarding/home', 'AdminCuratedRegisterController@index')->name('admin.curated-onboarding'); 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-details-message', 'AdminCuratedRegisterController@previewDetailsMessageShow');
Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow'); Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow');
Route::get('curated-onboarding/show/{id}', 'AdminCuratedRegisterController@show'); 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}/message/send', 'AdminCuratedRegisterController@apiMessageSendStore');
Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject'); Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject');
Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove'); Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove');
Route::get('curated-onboarding/templates/get', 'AdminCuratedRegisterController@getActiveTemplates');
}); });
}); });