diff --git a/app/Activity/ActivityType.php b/app/Activity/ActivityType.php index 5ec9b9cf0..4a648da6c 100644 --- a/app/Activity/ActivityType.php +++ b/app/Activity/ActivityType.php @@ -71,6 +71,10 @@ class ActivityType const IMPORT_RUN = 'import_run'; const IMPORT_DELETE = 'import_delete'; + const SORT_SET_CREATE = 'sort_set_create'; + const SORT_SET_UPDATE = 'sort_set_update'; + const SORT_SET_DELETE = 'sort_set_delete'; + /** * Get all the possible values. */ diff --git a/app/Sorting/SortSet.php b/app/Sorting/SortSet.php index ee45c211f..971b3e29a 100644 --- a/app/Sorting/SortSet.php +++ b/app/Sorting/SortSet.php @@ -2,6 +2,7 @@ namespace BookStack\Sorting; +use BookStack\Activity\Models\Loggable; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; @@ -12,16 +13,14 @@ use Illuminate\Database\Eloquent\Model; * @property Carbon $created_at * @property Carbon $updated_at */ -class SortSet extends Model +class SortSet extends Model implements Loggable { /** * @return SortSetOperation[] */ public function getOperations(): array { - $strOptions = explode(',', $this->sequence); - $options = array_map(fn ($val) => SortSetOperation::tryFrom($val), $strOptions); - return array_filter($options); + return SortSetOperation::fromSequence($this->sequence); } /** @@ -32,4 +31,14 @@ class SortSet extends Model $values = array_map(fn (SortSetOperation $opt) => $opt->value, $options); $this->sequence = implode(',', $values); } + + public function logDescriptor(): string + { + return "({$this->id}) {$this->name}"; + } + + public function getUrl(): string + { + return url("/settings/sorting/sets/{$this->id}"); + } } diff --git a/app/Sorting/SortSetController.php b/app/Sorting/SortSetController.php index 4ef148295..0d77bd88f 100644 --- a/app/Sorting/SortSetController.php +++ b/app/Sorting/SortSetController.php @@ -2,7 +2,9 @@ namespace BookStack\Sorting; +use BookStack\Activity\ActivityType; use BookStack\Http\Controller; +use BookStack\Http\Request; class SortSetController extends Controller { @@ -14,6 +16,73 @@ class SortSetController extends Controller public function create() { + $this->setPageTitle(trans('settings.sort_set_create')); + return view('settings.sort-sets.create'); } + + public function store(Request $request) + { + $this->validate($request, [ + 'name' => ['required', 'string', 'min:1', 'max:200'], + 'sequence' => ['required', 'string', 'min:1'], + ]); + + $operations = SortSetOperation::fromSequence($request->input('sequence')); + if (count($operations) === 0) { + return redirect()->withInput()->withErrors(['sequence' => 'No operations set.']); + } + + $set = new SortSet(); + $set->name = $request->input('name'); + $set->setOperations($operations); + $set->save(); + + $this->logActivity(ActivityType::SORT_SET_CREATE, $set); + + return redirect('/settings/sorting'); + } + + public function edit(string $id) + { + $set = SortSet::query()->findOrFail($id); + + $this->setPageTitle(trans('settings.sort_set_edit')); + + return view('settings.sort-sets.edit', ['set' => $set]); + } + + public function update(string $id, Request $request) + { + $this->validate($request, [ + 'name' => ['required', 'string', 'min:1', 'max:200'], + 'sequence' => ['required', 'string', 'min:1'], + ]); + + $set = SortSet::query()->findOrFail($id); + $operations = SortSetOperation::fromSequence($request->input('sequence')); + if (count($operations) === 0) { + return redirect()->withInput()->withErrors(['sequence' => 'No operations set.']); + } + + $set->name = $request->input('name'); + $set->setOperations($operations); + $set->save(); + + $this->logActivity(ActivityType::SORT_SET_UPDATE, $set); + + return redirect('/settings/sorting'); + } + + public function destroy(string $id) + { + $set = SortSet::query()->findOrFail($id); + + // TODO - Check if it's in use + + $set->delete(); + $this->logActivity(ActivityType::SORT_SET_DELETE, $set); + + return redirect('/settings/sorting'); + } } diff --git a/app/Sorting/SortSetOperation.php b/app/Sorting/SortSetOperation.php index 12fda669f..a6dd860f5 100644 --- a/app/Sorting/SortSetOperation.php +++ b/app/Sorting/SortSetOperation.php @@ -39,6 +39,21 @@ enum SortSetOperation: string public static function allExcluding(array $operations): array { $all = SortSetOperation::cases(); - return array_diff($all, $operations); + $filtered = array_filter($all, function (SortSetOperation $operation) use ($operations) { + return !in_array($operation, $operations); + }); + return array_values($filtered); + } + + /** + * Create a set of operations from a string sequence representation. + * (values seperated by commas). + * @return SortSetOperation[] + */ + public static function fromSequence(string $sequence): array + { + $strOptions = explode(',', $sequence); + $options = array_map(fn ($val) => SortSetOperation::tryFrom($val), $strOptions); + return array_filter($options); } } diff --git a/lang/en/activities.php b/lang/en/activities.php index 7c3454d41..7db872c0c 100644 --- a/lang/en/activities.php +++ b/lang/en/activities.php @@ -127,6 +127,14 @@ return [ 'comment_update' => 'updated comment', 'comment_delete' => 'deleted comment', + // Sort Sets + 'sort_set_create' => 'created sort set', + 'sort_set_create_notification' => 'Sort set successfully created', + 'sort_set_update' => 'updated sort set', + 'sort_set_update_notification' => 'Sort set successfully update', + 'sort_set_delete' => 'deleted sort set', + 'sort_set_delete_notification' => 'Sort set successfully deleted', + // Other 'permissions_update' => 'updated permissions', ]; diff --git a/lang/en/settings.php b/lang/en/settings.php index 8bb2f6ef4..cda097590 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -82,6 +82,8 @@ return [ 'sorting_sets_desc' => 'These are predefined sorting operations which can be applied to content in the system.', 'sort_set_create' => 'Create Sort Set', 'sort_set_edit' => 'Edit Sort Set', + 'sort_set_delete' => 'Delete Sort Set', + 'sort_set_delete_desc' => 'Remove this sort set from the system. Deletion will only go ahead if the sort is not in active use.', 'sort_set_details' => 'Sort Set Details', 'sort_set_details_desc' => 'Set a name for this sort set, which will appear in lists when users are selecting a sort.', 'sort_set_operations' => 'Sort Operations', diff --git a/resources/views/settings/categories/sorting.blade.php b/resources/views/settings/categories/sorting.blade.php index 9de11bb6f..b5d613840 100644 --- a/resources/views/settings/categories/sorting.blade.php +++ b/resources/views/settings/categories/sorting.blade.php @@ -52,6 +52,17 @@ </div> </div> -{{-- TODO--}} + @php + $sortSets = \BookStack\Sorting\SortSet::query()->orderBy('name', 'asc')->get(); + @endphp + @if(empty($sortSets)) + <p class="italic text-muted">{{ trans('common.no_items') }}</p> + @else + <div class="item-list"> + @foreach($sortSets as $set) + @include('settings.sort-sets.parts.sort-set-list-item', ['set' => $set]) + @endforeach + </div> + @endif </div> @endsection \ No newline at end of file diff --git a/resources/views/settings/sort-sets/edit.blade.php b/resources/views/settings/sort-sets/edit.blade.php new file mode 100644 index 000000000..3b88c1243 --- /dev/null +++ b/resources/views/settings/sort-sets/edit.blade.php @@ -0,0 +1,44 @@ +@extends('layouts.simple') + +@section('body') + + <div class="container small"> + + @include('settings.parts.navbar', ['selected' => 'settings']) + + <div class="card content-wrap auto-height"> + <h1 class="list-heading">{{ trans('settings.sort_set_edit') }}</h1> + + <form action="{{ $set->getUrl() }}" method="POST"> + {{ method_field('PUT') }} + {{ csrf_field() }} + + @include('settings.sort-sets.parts.form', ['model' => $set]) + + <div class="form-group text-right"> + <a href="{{ url("/settings/sorting") }}" class="button outline">{{ trans('common.cancel') }}</a> + <button type="submit" class="button">{{ trans('common.save') }}</button> + </div> + </form> + </div> + + <div class="card content-wrap auto-height"> + <div class="flex-container-row items-center gap-l"> + <div> + <h2 class="list-heading">{{ trans('settings.sort_set_delete') }}</h2> + <p class="text-muted">{{ trans('settings.sort_set_delete_desc') }}</p> + </div> + <div class="flex"> + <form action="{{ $set->getUrl() }}" method="POST"> + {{ method_field('DELETE') }} + {{ csrf_field() }} + <div class="text-right"> + <button type="submit" class="button outline">{{ trans('common.delete') }}</button> + </div> + </form> + </div> + </div> + </div> + </div> + +@stop diff --git a/resources/views/settings/sort-sets/parts/form.blade.php b/resources/views/settings/sort-sets/parts/form.blade.php index 3f2220947..38db840ac 100644 --- a/resources/views/settings/sort-sets/parts/form.blade.php +++ b/resources/views/settings/sort-sets/parts/form.blade.php @@ -15,9 +15,14 @@ <div component="sort-set-manager"> <label class="setting-list-label">{{ trans('settings.sort_set_operations') }}</label> <p class="text-muted text-small">{{ trans('settings.sort_set_operations_desc') }}</p> + @include('form.errors', ['name' => 'sequence']) - <input refs="sort-set-manager@input" type="hidden" name="books" - value="{{ $model?->sequence ?? '' }}"> + <input refs="sort-set-manager@input" type="hidden" name="sequence" + value="{{ old('sequence') ?? $model?->sequence ?? '' }}"> + + @php + $configuredOps = old('sequence') ? \BookStack\Sorting\SortSetOperation::fromSequence(old('sequence')) : ($model?->getOperations() ?? []); + @endphp <div class="grid half"> <div class="form-group"> @@ -27,8 +32,9 @@ aria-labelledby="sort-set-configured-operations" class="scroll-box configured-option-list"> <li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_set_configured_operations_empty') }}</li> - @foreach(($model?->getOperations() ?? []) as $option) - @include('settings.sort-sets.parts.operation') + + @foreach($configuredOps as $operation) + @include('settings.sort-sets.parts.operation', ['operation' => $operation]) @endforeach </ul> </div> @@ -40,7 +46,7 @@ aria-labelledby="sort-set-available-operations" class="scroll-box available-option-list"> <li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_set_available_operations_empty') }}</li> - @foreach(\BookStack\Sorting\SortSetOperation::allExcluding($model?->getOperations() ?? []) as $operation) + @foreach(\BookStack\Sorting\SortSetOperation::allExcluding($configuredOps) as $operation) @include('settings.sort-sets.parts.operation', ['operation' => $operation]) @endforeach </ul> diff --git a/resources/views/settings/sort-sets/parts/sort-set-list-item.blade.php b/resources/views/settings/sort-sets/parts/sort-set-list-item.blade.php new file mode 100644 index 000000000..e5ee1fb87 --- /dev/null +++ b/resources/views/settings/sort-sets/parts/sort-set-list-item.blade.php @@ -0,0 +1,8 @@ +<div class="item-list-row flex-container-row py-xs items-center"> + <div class="py-xs px-m flex-2"> + <a href="{{ $set->getUrl() }}">{{ $set->name }}</a> + </div> + <div class="px-m text-small text-muted"> + {{ implode(', ', array_map(fn ($op) => $op->getLabel(), $set->getOperations())) }} + </div> +</div> \ No newline at end of file