Got webhook CRUD actions in place within the interface.
Quick manual test pass done, Needs automated tests.
namespace BookStack\Actions;
use BookStack\Interfaces\Loggable;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $name
* @property string $endpoint
+ * @property Collection $trackedEvents
*/
class Webhook extends Model implements Loggable
{
+ protected $fillable = ['name', 'endpoint'];
+
use HasFactory;
+ /**
+ * Define the tracked event relation a webhook.
+ */
+ public function trackedEvents(): HasMany
+ {
+ return $this->hasMany(WebhookTrackedEvent::class);
+ }
+
+ /**
+ * Update the tracked events for a webhook from the given list of event types.
+ */
+ public function updateTrackedEvents(array $events): void
+ {
+ $this->trackedEvents()->delete();
+
+ $eventsToStore = array_intersect($events, array_values(ActivityType::all()));
+ if (in_array('all', $events)) {
+ $eventsToStore = ['all'];
+ }
+
+ $trackedEvents = [];
+ foreach ($eventsToStore as $event) {
+ $trackedEvents[] = new WebhookTrackedEvent(['event' => $event]);
+ }
+
+ $this->trackedEvents()->saveMany($trackedEvents);
+ }
+
+ /**
+ * Check if this webhook tracks the given event.
+ */
+ public function tracksEvent(string $event): bool
+ {
+ return $this->trackedEvents->pluck('event')->contains($event);
+ }
+
+ /**
+ * Get a URL for this webhook within the settings interface.
+ */
+ public function getUrl(string $path = ''): string
+ {
+ return url('/settings/webhooks/' . $this->id . '/' . ltrim($path, '/'));
+ }
+
/**
* Get the string descriptor for this item.
*/
--- /dev/null
+<?php
+
+namespace BookStack\Actions;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * @property int $id
+ * @property int $webhook_id
+ * @property string $event
+ */
+class WebhookTrackedEvent extends Model
+{
+ protected $fillable = ['event'];
+
+ use HasFactory;
+}
*/
public function index()
{
- // TODO - Get and pass webhooks
- return view('settings.webhooks.index');
+ $webhooks = Webhook::query()
+ ->orderBy('name', 'desc')
+ ->with('trackedEvents')
+ ->get();
+ return view('settings.webhooks.index', ['webhooks' => $webhooks]);
}
/**
*/
public function store(Request $request)
{
- // TODO - Create webhook
+ $validated = $this->validate($request, [
+ 'name' => ['required', 'max:150'],
+ 'endpoint' => ['required', 'url', 'max:500'],
+ 'events' => ['required', 'array']
+ ]);
+
+ $webhook = new Webhook($validated);
+ $webhook->save();
+ $webhook->updateTrackedEvents(array_values($validated['events']));
+
$this->logActivity(ActivityType::WEBHOOK_CREATE, $webhook);
return redirect('/settings/webhooks');
}
public function edit(string $id)
{
/** @var Webhook $webhook */
- $webhook = Webhook::query()->findOrFail($id);
+ $webhook = Webhook::query()
+ ->with('trackedEvents')
+ ->findOrFail($id);
return view('settings.webhooks.edit', ['webhook' => $webhook]);
}
*/
public function update(Request $request, string $id)
{
+ $validated = $this->validate($request, [
+ 'name' => ['required', 'max:150'],
+ 'endpoint' => ['required', 'url', 'max:500'],
+ 'events' => ['required', 'array']
+ ]);
+
/** @var Webhook $webhook */
$webhook = Webhook::query()->findOrFail($id);
- // TODO - Update
+ $webhook->fill($validated)->save();
+ $webhook->updateTrackedEvents($validated['events']);
$this->logActivity(ActivityType::WEBHOOK_UPDATE, $webhook);
return redirect('/settings/webhooks');
/** @var Webhook $webhook */
$webhook = Webhook::query()->findOrFail($id);
- // TODO - Delete event type relations
+ $webhook->trackedEvents()->delete();
$webhook->delete();
$this->logActivity(ActivityType::WEBHOOK_DELETE, $webhook);
$table->string('name', 150);
$table->string('endpoint', 500);
$table->timestamps();
+
+ $table->index('name');
+ });
+
+ Schema::create('webhook_tracked_events', function (Blueprint $table) {
+ $table->increments('id');
+ $table->integer('webhook_id');
+ $table->string('event', 50);
+ $table->timestamps();
+
+ $table->index('event');
+ $table->index('webhook_id');
});
}
setup() {
this.checkboxes = this.$el.querySelectorAll('input[type="checkbox"]');
- this.allCheckbox = this.$refs.all;
+ this.allCheckbox = this.$el.querySelector('input[type="checkbox"][value="all"]');
this.$el.addEventListener('change', event => {
if (event.target.checked && event.target === this.allCheckbox) {
'webhooks_events_all' => 'All system events',
'webhooks_name' => 'Webhook Name',
'webhooks_endpoint' => 'Webhook Endpoint',
+ 'webhook_events_table_header' => 'Events',
'webhooks_delete' => 'Delete Webhook',
'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
--- /dev/null
+@if($errors->has($name))
+ <div class="text-neg text-small">{{ $errors->first($name) }}</div>
+@endif
\ No newline at end of file
@include('settings.parts.navbar', ['selected' => 'webhooks'])
</div>
- <form action="{{ url("/settings/webhooks/new") }}" method="POST">
+ <form action="{{ url("/settings/webhooks/create") }}" method="POST">
@include('settings.webhooks.parts.form', ['title' => trans('settings.webhooks_create')])
</form>
</div>
<p>{{ trans('settings.webhooks_delete_warning', ['webhookName' => $webhook->name]) }}</p>
- <form action="{{ url("/settings/webhooks/{$role->id}") }}" method="POST">
+ <form action="{{ $webhook->getUrl() }}" method="POST">
{!! csrf_field() !!}
{!! method_field('DELETE') !!}
</div>
<div>
<div class="form-group text-right">
- <a href="{{ url("/settings/webhooks/{$role->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
+ <a href="{{ $webhook->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button">{{ trans('common.confirm') }}</button>
</div>
</div>
@include('settings.parts.navbar', ['selected' => 'webhooks'])
</div>
- <form action="{{ url("/settings/webhooks/{$webhook->id}") }}" method="POST">
+ <form action="{{ $webhook->getUrl() }}" method="POST">
{!! method_field('PUT') !!}
@include('settings.webhooks.parts.form', ['model' => $webhook, 'title' => trans('settings.webhooks_edit')])
</form>
<h1 class="list-heading">{{ trans('settings.webhooks') }}</h1>
<div class="text-right">
- <a href="{{ url("/settings/webhooks/create") }}" class="button outline">{{ trans('settings.webhooks_create') }}</a>
+ <a href="{{ url("/settings/webhooks/create") }}"
+ class="button outline">{{ trans('settings.webhooks_create') }}</a>
</div>
</div>
+ @if(count($webhooks) > 0)
+
+ <table class="table">
+ <tr>
+ <th>{{ trans('common.name') }}</th>
+ <th>{{ trans('settings.webhook_events_table_header') }}</th>
+ </tr>
+ @foreach($webhooks as $webhook)
+ <tr>
+ <td>
+ <a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a> <br>
+ <span class="small text-muted italic">{{ $webhook->endpoint }}</span>
+ </td>
+ <td>
+ @if($webhook->tracksEvent('all'))
+ {{ trans('settings.webhooks_events_all') }}
+ @else
+ {{ $webhook->trackedEvents->count() }}
+ @endif
+ </td>
+ </tr>
+ @endforeach
+ </table>
+ @else
+ <p class="text-muted empty-text">
+ {{ trans('common.no_items') }}
+ </p>
+ @endif
+
</div>
</div>
<div component="webhook-events">
<label class="setting-list-label">{{ trans('settings.webhooks_events') }}</label>
+ @include('form.errors', ['name' => 'events'])
+
<p class="small">{{ trans('settings.webhooks_events_desc') }}</p>
<p class="text-warn small">{{ trans('settings.webhooks_events_warning') }}</p>
- <div>
- <label><input type="checkbox"
- name="events[]"
- value="all"
- refs="webhook-events@all">
- {{ trans('settings.webhooks_events_all') }}</label>
+ <div class="toggle-switch-list">
+ @include('form.custom-checkbox', [
+ 'name' => 'events[]',
+ 'value' => 'all',
+ 'label' => trans('settings.webhooks_events_all'),
+ 'checked' => old('events') ? in_array('all', old('events')) : (isset($webhook) ? $webhook->tracksEvent('all') : false),
+ ])
</div>
- <hr class="my-m">
+ <hr class="my-s">
- <div class="dual-column-content">
+ <div class="dual-column-content toggle-switch-list">
@foreach(\BookStack\Actions\ActivityType::all() as $activityType)
- <label><input type="checkbox" name="events[]" value="{{ $activityType }}">{{ $activityType }}</label>
+ <div>
+ @include('form.custom-checkbox', [
+ 'name' => 'events[]',
+ 'value' => $activityType,
+ 'label' => $activityType,
+ 'checked' => old('events') ? in_array($activityType, old('events')) : (isset($webhook) ? $webhook->tracksEvent($activityType) : false),
+ ])
+ </div>
@endforeach
</div>
</div>
<div class="form-group text-right">
<a href="{{ url("/settings/webhooks") }}" class="button outline">{{ trans('common.cancel') }}</a>
@if ($webhook->id ?? false)
- <a href="{{ url("/settings/roles/delete/{$webhook->id}") }}" class="button outline">{{ trans('settings.webhooks_delete') }}</a>
+ <a href="{{ $webhook->getUrl('/delete') }}" class="button outline">{{ trans('settings.webhooks_delete') }}</a>
@endif
<button type="submit" class="button">{{ trans('settings.webhooks_save') }}</button>
</div>