Skip to content

Commit

Permalink
Add bulk editing and deletion via API (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kovah committed Jul 17, 2024
1 parent 22388ad commit fdd9fb6
Show file tree
Hide file tree
Showing 25 changed files with 616 additions and 203 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
/.tmp
/.vscode
/.vagrant
/.phpunit.cache
Homestead.json
Homestead.yaml
npm-debug.log
Expand Down
71 changes: 71 additions & 0 deletions app/Http/Controllers/API/BulkEditController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use App\Http\Requests\Models\Api\BulkDeleteRequest;
use App\Http\Requests\Models\Api\BulkEditLinksRequest;
use App\Http\Requests\Models\Api\BulkEditListsRequest;
use App\Http\Requests\Models\Api\BulkEditTagsRequest;
use App\Models\Link;
use App\Models\LinkList;
use App\Models\Tag;
use App\Repositories\LinkRepository;
use App\Repositories\ListRepository;
use App\Repositories\TagRepository;
use Illuminate\Support\Facades\Log;

class BulkEditController extends Controller
{
public function updateLinks(BulkEditLinksRequest $request)
{
$models = $request->input('models');

$results = LinkRepository::bulkUpdate($models, $request->input());

return response()->json($results);
}

public function updateLists(BulkEditListsRequest $request)
{
$models = $request->input('models');

$results = ListRepository::bulkUpdate($models, $request->input());

return response()->json($results);
}

public function updateTags(BulkEditTagsRequest $request)
{
$models = $request->input('models');

$results = TagRepository::bulkUpdate($models, $request->input());

return response()->json($results);
}

public function delete(BulkDeleteRequest $request)
{
$type = $request->input('type');
$formModels = $request->input('models');
$models = match ($type) {
'links' => Link::whereIn('id', $formModels)->get(),
'lists' => LinkList::whereIn('id', $formModels)->get(),
'tags' => Tag::whereIn('id', $formModels)->get(),
};

$results = $models->mapWithKeys(function ($model) use ($type) {
if (!auth()->user()->can('delete', $model)) {
Log::warning('Could not delete ' . $type . ' ' . $model->id . ' during bulk deletion: Permission denied!');
return [$model->id => false];
}
return match ($type) {
'links' => [$model->id => LinkRepository::delete($model) === true],
'lists' => [$model->id => ListRepository::delete($model) === true],
'tags' => [$model->id => TagRepository::delete($model) === true],
};
});

return response()->json($results);
}
}
57 changes: 6 additions & 51 deletions app/Http/Controllers/Models/BulkEditController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,81 +34,36 @@ public function form(BulkEditFormRequest $request)
public function updateLinks(BulkEditLinksRequest $request)
{
$models = explode(',', $request->input('models'));
$links = Link::whereIn('id', $models)->with([
'tags:id',
'lists:id',
])->get();

$results = $links->map(function (Link $link) use ($request) {
if (!auth()->user()->can('update', $link)) {
Log::warning('Could not update ' . $link->id . ' during bulk update: Permission denied!');
return null;
}

$newTags = explode(',', $request->input('tags'));
$newLists = explode(',', $request->input('lists'));

$linkData = $link->toArray();
$linkData['tags'] = $request->input('tags_mode') === 'replace'
? $newTags
: array_merge($link->tags->pluck('id')->toArray(), $newTags);
$linkData['lists'] = $request->input('lists_mode') === 'replace'
? $newLists
: array_merge($link->lists->pluck('id')->toArray(), $newLists);
$linkData['visibility'] = $request->input('visibility') ?: $linkData['visibility'];

return LinkRepository::update($link, $linkData);
});
$results = LinkRepository::bulkUpdate($models, $request->input());

$successCount = $results->filter(fn($e) => $e !== null)->count();

flash(trans('link.bulk_edit_success', ['success' => $successCount, 'selected' => $links->count()]));
flash(trans('link.bulk_edit_success', ['success' => $successCount, 'selected' => count($models)]));
return redirect()->route('links.index');
}

public function updateLists(BulkEditListsRequest $request)
{
$models = explode(',', $request->input('models'));
$lists = LinkList::whereIn('id', $models)->get();

$results = $lists->map(function (LinkList $list) use ($request) {
if (!auth()->user()->can('update', $list)) {
Log::warning('Could not update list ' . $list->id . ' during bulk update: Permission denied!');
return null;
}

$listData = $list->toArray();
$listData['visibility'] = $request->input('visibility') ?: $listData['visibility'];

return ListRepository::update($list, $listData);
});
$results = ListRepository::bulkUpdate($models, $request->input());

$successCount = $results->filter(fn($e) => $e !== null)->count();

flash(trans('list.bulk_edit_success', ['success' => $successCount, 'selected' => $lists->count()]));
flash(trans('list.bulk_edit_success', ['success' => $successCount, 'selected' => count($models)]));
return redirect()->route('lists.index');
}

public function updateTags(BulkEditTagsRequest $request)
{
$models = explode(',', $request->input('models'));
$tags = Tag::whereIn('id', $models)->get();

$results = $tags->map(function (Tag $tag) use ($request) {
if (!auth()->user()->can('update', $tag)) {
Log::warning('Could not update tag ' . $tag->id . ' during bulk update: Permission denied!');
return null;
}

$tagData = $tag->toArray();
$tagData['visibility'] = $request->input('visibility') ?: $tagData['visibility'];

return TagRepository::update($tag, $tagData);
});
$results = TagRepository::bulkUpdate($models, $request->input());

$successCount = $results->filter(fn($e) => $e !== null)->count();

flash(trans('tag.bulk_edit_success', ['success' => $successCount, 'selected' => $tags->count()]));
flash(trans('tag.bulk_edit_success', ['success' => $successCount, 'selected' => count($models)]));
return redirect()->route('tags.index');
}

Expand Down
16 changes: 16 additions & 0 deletions app/Http/Requests/Models/Api/BulkDeleteRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Http\Requests\Models\Api;

use Illuminate\Foundation\Http\FormRequest;

class BulkDeleteRequest extends FormRequest
{
public function rules(): array
{
return [
'type' => ['required', 'in:links,lists,tags'],
'models' => ['required', 'array'],
];
}
}
21 changes: 21 additions & 0 deletions app/Http/Requests/Models/Api/BulkEditLinksRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Http\Requests\Models\Api;

use App\Rules\ModelVisibility;
use Illuminate\Foundation\Http\FormRequest;

class BulkEditLinksRequest extends FormRequest
{
public function rules(): array
{
return [
'models' => ['required', 'array'],
'tags' => ['nullable', 'array'],
'tags_mode' => ['required_with:tags', 'in:append,replace'],
'lists' => ['nullable', 'array'],
'lists_mode' => ['required_with:lists', 'in:append,replace'],
'visibility' => ['nullable', new ModelVisibility],
];
}
}
17 changes: 17 additions & 0 deletions app/Http/Requests/Models/Api/BulkEditListsRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Http\Requests\Models\Api;

use App\Rules\ModelVisibility;
use Illuminate\Foundation\Http\FormRequest;

class BulkEditListsRequest extends FormRequest
{
public function rules(): array
{
return [
'models' => ['required', 'array'],
'visibility' => ['nullable', new ModelVisibility],
];
}
}
17 changes: 17 additions & 0 deletions app/Http/Requests/Models/Api/BulkEditTagsRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Http\Requests\Models\Api;

use App\Rules\ModelVisibility;
use Illuminate\Foundation\Http\FormRequest;

class BulkEditTagsRequest extends FormRequest
{
public function rules(): array
{
return [
'models' => ['required', 'array'],
'visibility' => ['nullable', new ModelVisibility],
];
}
}
30 changes: 30 additions & 0 deletions app/Repositories/LinkRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Models\LinkList;
use App\Models\Tag;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
Expand Down Expand Up @@ -68,6 +69,35 @@ public static function update(Link $link, array $data): Link
return $link;
}

public static function bulkUpdate(array $models, array $data): Collection
{
$links = Link::whereIn('id', $models)->with([
'tags:id',
'lists:id',
])->get();

$newTags = is_array($data['tags']) ? $data['tags'] : explode(',', $data['tags']);
$newLists = is_array($data['lists']) ? $data['lists'] : explode(',', $data['lists']);

return $links->map(function (Link $link) use ($data, $newTags, $newLists) {
if (!auth()->user()->can('update', $link)) {
Log::warning('Could not update ' . $link->id . ' during bulk update: Permission denied!');
return null;
}

$linkData = $link->toArray();
$linkData['tags'] = $data['tags_mode'] === 'replace'
? $newTags
: array_merge($link->tags->pluck('id')->toArray(), $newTags);
$linkData['lists'] = $data['lists_mode'] === 'replace'
? $newLists
: array_merge($link->lists->pluck('id')->toArray(), $newLists);
$linkData['visibility'] = $data['visibility'] ?: $linkData['visibility'];

return LinkRepository::update($link, $linkData);
});
}

/**
* Try to delete a given link. If it fails, log the error.
*
Expand Down
17 changes: 17 additions & 0 deletions app/Repositories/ListRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ public static function update(LinkList $list, array $data): LinkList
return $list;
}

public static function bulkUpdate(array $models, array $data): \Illuminate\Support\Collection
{
$lists = LinkList::whereIn('id', $models)->get();

return $lists->map(function (LinkList $list) use ($data) {
if (!auth()->user()->can('update', $list)) {
Log::warning('Could not update list ' . $list->id . ' during bulk update: Permission denied!');
return null;
}

$listData = $list->toArray();
$listData['visibility'] = $data['visibility'] ?: $listData['visibility'];

return ListRepository::update($list, $listData);
});
}

public static function delete(LinkList $list): bool
{
try {
Expand Down
17 changes: 17 additions & 0 deletions app/Repositories/TagRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ public static function update(Tag $tag, array $data): Tag
return $tag;
}

public static function bulkUpdate(array $models, array $data): \Illuminate\Support\Collection
{
$tags = Tag::whereIn('id', $models)->get();

return $tags->map(function (Tag $tag) use ($data) {
if (!auth()->user()->can('update', $tag)) {
Log::warning('Could not update tag ' . $tag->id . ' during bulk update: Permission denied!');
return null;
}

$tagData = $tag->toArray();
$tagData['visibility'] = $data['visibility'] ?: $tagData['visibility'];

return TagRepository::update($tag, $tagData);
});
}

public static function delete(Tag $tag): bool
{
try {
Expand Down
16 changes: 6 additions & 10 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">

<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>

<testsuites>
<testsuite name="Commands">
<directory suffix="Test.php">./tests/Commands</directory>
Expand All @@ -34,9 +28,11 @@
</testsuite>
</testsuites>

<extensions>
<extension class="Tests\Bootstrap"/>
</extensions>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>

<php>
<server name="APP_ENV" value="testing"/>
Expand Down
8 changes: 7 additions & 1 deletion routes/api.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use App\Http\Controllers\API\BulkEditController;
use App\Http\Controllers\API\LinkCheckController;
use App\Http\Controllers\API\LinkController;
use App\Http\Controllers\API\LinkNotesController;
Expand All @@ -22,7 +23,7 @@
|
*/

Route::prefix('v1')->middleware(['auth:sanctum'])->group(function () {
Route::prefix('v2')->middleware(['auth:sanctum'])->group(function () {

Route::get('links/check', LinkCheckController::class)
->name('api.links.check');
Expand Down Expand Up @@ -71,6 +72,11 @@
])
->except(['index', 'show']);

Route::patch('bulk/links', [BulkEditController::class, 'updateLinks'])->name('api.bulk.links');
Route::patch('bulk/lists', [BulkEditController::class, 'updateLists'])->name('api.bulk.lists');
Route::patch('bulk/tags', [BulkEditController::class, 'updateTags'])->name('api.bulk.tags');
Route::delete('bulk/delete', [BulkEditController::class, 'delete'])->name('api.bulk.delete');

Route::get('search/links', [SearchController::class, 'searchLinks'])
->name('api.search.links');
Route::get('search/tags', [SearchController::class, 'searchByTags'])
Expand Down
Loading

0 comments on commit fdd9fb6

Please sign in to comment.