Aligned user preference endpoints in style and behaviour

Changes their endpoints and remove the user id from the URLs.
Simplifies list changes to share a single endpoint, which aligns it to
the behaviour of the existing sort preference endpoint.
Also added test to ensure user preferences are deleted on user delete.
This commit is contained in:
Dan Brown 2022-11-09 19:30:08 +00:00
parent 24a7e8500d
commit a3fcc98d6e
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
16 changed files with 89 additions and 88 deletions

View File

@ -158,6 +158,9 @@ class UserRepo
// Delete user profile images // Delete user profile images
$this->userAvatar->destroyAllForUser($user); $this->userAvatar->destroyAllForUser($user);
// Delete related activities
setting()->deleteUserSettings($user->id);
if (!empty($newOwnerId)) { if (!empty($newOwnerId)) {
$newOwner = User::query()->find($newOwnerId); $newOwner = User::query()->find($newOwnerId);
if (!is_null($newOwner)) { if (!is_null($newOwner)) {

View File

@ -38,8 +38,8 @@ class UserPreferencesController extends Controller
$providedShortcuts = $request->get('shortcut', []); $providedShortcuts = $request->get('shortcut', []);
$shortcuts = new UserShortcutMap($providedShortcuts); $shortcuts = new UserShortcutMap($providedShortcuts);
setting()->putUser(user(), 'ui-shortcuts', $shortcuts->toJson()); setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
setting()->putUser(user(), 'ui-shortcuts-enabled', $enabled); setting()->putForCurrentUser('ui-shortcuts-enabled', $enabled);
$this->showSuccessNotification('Shortcut preferences have been updated!'); $this->showSuccessNotification('Shortcut preferences have been updated!');
@ -47,70 +47,45 @@ class UserPreferencesController extends Controller
} }
/** /**
* Update the user's preferred book-list display setting. * Update the preferred view format for a list view of the given type.
*/ */
public function switchBooksView(Request $request, int $id) public function changeView(Request $request, string $type)
{ {
return $this->switchViewType($id, $request, 'books'); $valueViewTypes = ['books', 'bookshelves', 'bookshelf'];
} if (!in_array($type, $valueViewTypes)) {
return redirect()->back(500);
/**
* Update the user's preferred shelf-list display setting.
*/
public function switchShelvesView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'bookshelves');
}
/**
* Update the user's preferred shelf-view book list display setting.
*/
public function switchShelfView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'bookshelf');
}
/**
* For a type of list, switch with stored view type for a user.
*/
protected function switchViewType(int $userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
} }
$user = $this->userRepo->getById($userId); $view = $request->get('view');
$key = $listName . '_view_type'; if (!in_array($view, ['grid', 'list'])) {
setting()->putUser($user, $key, $viewType); $view = 'list';
}
return redirect()->back(302, [], "/settings/users/$userId"); $key = $type . '_view_type';
setting()->putForCurrentUser($key, $view);
return redirect()->back(302, [], "/");
} }
/** /**
* Change the stored sort type for a particular view. * Change the stored sort type for a particular view.
*/ */
public function changeSort(Request $request, string $id, string $type) public function changeSort(Request $request, string $type)
{ {
$validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks', 'tags', 'page_revisions']; $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks', 'tags', 'page_revisions'];
if (!in_array($type, $validSortTypes)) { if (!in_array($type, $validSortTypes)) {
return redirect()->back(500); return redirect()->back(500);
} }
$this->checkPermissionOrCurrentUser('users-manage', $id);
$sort = substr($request->get('sort') ?: 'name', 0, 50); $sort = substr($request->get('sort') ?: 'name', 0, 50);
$order = $request->get('order') === 'desc' ? 'desc' : 'asc'; $order = $request->get('order') === 'desc' ? 'desc' : 'asc';
$user = $this->userRepo->getById($id);
$sortKey = $type . '_sort'; $sortKey = $type . '_sort';
$orderKey = $type . '_sort_order'; $orderKey = $type . '_sort_order';
setting()->putUser($user, $sortKey, $sort); setting()->putForCurrentUser($sortKey, $sort);
setting()->putUser($user, $orderKey, $order); setting()->putForCurrentUser($orderKey, $order);
return redirect()->back(302, [], "/settings/users/{$id}"); return redirect()->back(302, [], "/");
} }
/** /**
@ -119,7 +94,7 @@ class UserPreferencesController extends Controller
public function toggleDarkMode() public function toggleDarkMode()
{ {
$enabled = setting()->getForCurrentUser('dark-mode-enabled', false); $enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
setting()->putUser(user(), 'dark-mode-enabled', $enabled ? 'false' : 'true'); setting()->putForCurrentUser('dark-mode-enabled', $enabled ? 'false' : 'true');
return redirect()->back(); return redirect()->back();
} }
@ -127,18 +102,15 @@ class UserPreferencesController extends Controller
/** /**
* Update the stored section expansion preference for the given user. * Update the stored section expansion preference for the given user.
*/ */
public function updateExpansionPreference(Request $request, string $id, string $key) public function changeExpansion(Request $request, string $type)
{ {
$this->checkPermissionOrCurrentUser('users-manage', $id); $typeWhitelist = ['home-details'];
$keyWhitelist = ['home-details']; if (!in_array($type, $typeWhitelist)) {
if (!in_array($key, $keyWhitelist)) {
return response('Invalid key', 500); return response('Invalid key', 500);
} }
$newState = $request->get('expand', 'false'); $newState = $request->get('expand', 'false');
setting()->putForCurrentUser('section_expansion#' . $type, $newState);
$user = $this->userRepo->getById($id);
setting()->putUser($user, 'section_expansion#' . $key, $newState);
return response('', 204); return response('', 204);
} }
@ -161,6 +133,6 @@ class UserPreferencesController extends Controller
array_splice($currentFavorites, $index, 1); array_splice($currentFavorites, $index, 1);
} }
setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites)); setting()->putForCurrentUser('code-language-favourites', implode(',', $currentFavorites));
} }
} }

View File

@ -194,6 +194,8 @@ class SettingService
/** /**
* Put a user-specific setting into the database. * Put a user-specific setting into the database.
* Can only take string value types since this may use
* the session which is less flexible to data types.
*/ */
public function putUser(User $user, string $key, string $value): bool public function putUser(User $user, string $key, string $value): bool
{ {
@ -206,6 +208,16 @@ class SettingService
return $this->put($this->userKey($user->id, $key), $value); return $this->put($this->userKey($user->id, $key), $value);
} }
/**
* Put a user-specific setting into the database for the current access user.
* Can only take string value types since this may use
* the session which is less flexible to data types.
*/
public function putForCurrentUser(string $key, string $value)
{
return $this->putUser(user(), $key, $value);
}
/** /**
* Convert a setting key into a user-specific key. * Convert a setting key into a user-specific key.
*/ */

View File

@ -73,7 +73,7 @@ class CodeEditor {
isFavorite ? this.favourites.add(language) : this.favourites.delete(language); isFavorite ? this.favourites.add(language) : this.favourites.delete(language);
button.setAttribute('data-favourite', isFavorite ? 'true' : 'false'); button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
window.$http.patch('/settings/users/update-code-language-favourite', { window.$http.patch('/preferences/update-code-language-favourite', {
language: language, language: language,
active: isFavorite active: isFavorite
}); });

View File

@ -70,7 +70,6 @@ class Shortcuts {
*/ */
runShortcut(id) { runShortcut(id) {
const el = this.container.querySelector(`[data-shortcut="${id}"]`); const el = this.container.querySelector(`[data-shortcut="${id}"]`);
console.info('Shortcut run', el);
if (!el) { if (!el) {
return false; return false;
} }

View File

@ -1,4 +1,4 @@
<form action="{{ url('/settings/users/toggle-dark-mode') }}" method="post"> <form action="{{ url('/preferences/toggle-dark-mode') }}" method="post">
{{ csrf_field() }} {{ csrf_field() }}
{{ method_field('patch') }} {{ method_field('patch') }}
@if(setting()->getForCurrentUser('dark-mode-enabled')) @if(setting()->getForCurrentUser('dark-mode-enabled'))

View File

@ -9,7 +9,7 @@
action="{{ url()->current() }}" action="{{ url()->current() }}"
method="get" method="get"
@else @else
action="{{ url("/settings/users/". user()->id ."/change-sort/{$type}") }}" action="{{ url("/preferences/change-sort/{$type}") }}"
method="post" method="post"
@endif @endif
> >

View File

@ -1,15 +1,15 @@
<div> <div>
<form action="{{ url("/settings/users/". user()->id ."/switch-${type}-view") }}" method="POST" class="inline"> <form action="{{ url("/preferences/change-view/" . $type) }}" method="POST" class="inline">
{!! csrf_field() !!} {!! csrf_field() !!}
{!! method_field('PATCH') !!} {!! method_field('PATCH') !!}
<input type="hidden" value="{{ $view === 'list'? 'grid' : 'list' }}" name="view_type">
@if ($view === 'list') @if ($view === 'list')
<button type="submit" class="icon-list-item text-primary"> <button type="submit" name="view" value="grid" class="icon-list-item text-primary">
<span class="icon">@icon('grid')</span> <span class="icon">@icon('grid')</span>
<span>{{ trans('common.grid_view') }}</span> <span>{{ trans('common.grid_view') }}</span>
</button> </button>
@else @else
<button type="submit" class="icon-list-item text-primary"> <button type="submit" name="view" value="list" class="icon-list-item text-primary">
<span>@icon('list')</span> <span>@icon('list')</span>
<span>{{ trans('common.list_view') }}</span> <span>{{ trans('common.list_view') }}</span>
</button> </button>

View File

@ -4,7 +4,7 @@ $key - Unique key for checking existing stored state.
--}} --}}
<?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?> <?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
<button type="button" expand-toggle="{{ $target }}" <button type="button" expand-toggle="{{ $target }}"
expand-toggle-update-endpoint="{{ url('/settings/users/'. user()->id .'/update-expansion-preference/' . $key) }}" expand-toggle-update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}" expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
class="icon-list-item {{ $classes ?? '' }}"> class="icon-list-item {{ $classes ?? '' }}">
<span>@icon('expand-text')</span> <span>@icon('expand-text')</span>

View File

@ -18,7 +18,7 @@
<span>{{ trans('entities.shelves_new_action') }}</span> <span>{{ trans('entities.shelves_new_action') }}</span>
</a> </a>
@endif @endif
@include('entities.view-toggle', ['view' => $view, 'type' => 'shelves']) @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelves'])
@include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details']) @include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
@include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary']) @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
</div> </div>

View File

@ -16,7 +16,7 @@
</a> </a>
@endif @endif
@include('entities.view-toggle', ['view' => $view, 'type' => 'shelves']) @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelves'])
<a href="{{ url('/tags') }}" class="icon-list-item"> <a href="{{ url('/tags') }}" class="icon-list-item">
<span>@icon('tag')</span> <span>@icon('tag')</span>

View File

@ -118,7 +118,7 @@
</a> </a>
@endif @endif
@include('entities.view-toggle', ['view' => $view, 'type' => 'shelf']) @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelf'])
<hr class="primary-background"> <hr class="primary-background">

View File

@ -249,13 +249,11 @@ Route::middleware('auth')->group(function () {
Route::redirect('/preferences', '/'); Route::redirect('/preferences', '/');
Route::get('/preferences/shortcuts', [UserPreferencesController::class, 'showShortcuts']); Route::get('/preferences/shortcuts', [UserPreferencesController::class, 'showShortcuts']);
Route::put('/preferences/shortcuts', [UserPreferencesController::class, 'updateShortcuts']); Route::put('/preferences/shortcuts', [UserPreferencesController::class, 'updateShortcuts']);
Route::patch('/settings/users/{id}/switch-books-view', [UserPreferencesController::class, 'switchBooksView']); Route::patch('/preferences/change-view/{type}', [UserPreferencesController::class, 'changeView']);
Route::patch('/settings/users/{id}/switch-shelves-view', [UserPreferencesController::class, 'switchShelvesView']); Route::patch('/preferences/change-sort/{type}', [UserPreferencesController::class, 'changeSort']);
Route::patch('/settings/users/{id}/switch-shelf-view', [UserPreferencesController::class, 'switchShelfView']); Route::patch('/preferences/change-expansion/{type}', [UserPreferencesController::class, 'changeExpansion']);
Route::patch('/settings/users/{id}/change-sort/{type}', [UserPreferencesController::class, 'changeSort']); Route::patch('/preferences/toggle-dark-mode', [UserPreferencesController::class, 'toggleDarkMode']);
Route::patch('/settings/users/{id}/update-expansion-preference/{key}', [UserPreferencesController::class, 'updateExpansionPreference']); Route::patch('/preferences/update-code-language-favourite', [UserPreferencesController::class, 'updateCodeLanguageFavourite']);
Route::patch('/settings/users/toggle-dark-mode', [UserPreferencesController::class, 'toggleDarkMode']);
Route::patch('/settings/users/update-code-language-favourite', [UserPreferencesController::class, 'updateCodeLanguageFavourite']);
// User API Tokens // User API Tokens
Route::get('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'create']); Route::get('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'create']);

View File

@ -225,18 +225,18 @@ class BookTest extends TestCase
setting()->putUser($editor, 'books_view_type', 'list'); setting()->putUser($editor, 'books_view_type', 'list');
$resp = $this->actingAs($editor)->get('/books'); $resp = $this->actingAs($editor)->get('/books');
$this->withHtml($resp)->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'Grid View'); $this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'Grid View');
$this->withHtml($resp)->assertElementExists('input[name="view_type"][value="grid"]'); $this->withHtml($resp)->assertElementExists('button[name="view"][value="grid"]');
$resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'grid']); $resp = $this->patch("/preferences/change-view/books", ['view' => 'grid']);
$resp->assertRedirect(); $resp->assertRedirect();
$this->assertEquals('grid', setting()->getUser($editor, 'books_view_type')); $this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));
$resp = $this->actingAs($editor)->get('/books'); $resp = $this->actingAs($editor)->get('/books');
$this->withHtml($resp)->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'List View'); $this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'List View');
$this->withHtml($resp)->assertElementExists('input[name="view_type"][value="list"]'); $this->withHtml($resp)->assertElementExists('button[name="view"][value="list"]');
$resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'list']); $resp = $this->patch("/preferences/change-view/books", ['view_type' => 'list']);
$resp->assertRedirect(); $resp->assertRedirect();
$this->assertEquals('list', setting()->getUser($editor, 'books_view_type')); $this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));
} }

View File

@ -160,6 +160,23 @@ class UserManagementTest extends TestCase
]); ]);
} }
public function test_delete_removes_user_preferences()
{
$editor = $this->getEditor();
setting()->putUser($editor, 'dark-mode-enabled', 'true');
$this->assertDatabaseHas('settings', [
'setting_key' => 'user:' . $editor->id . ':dark-mode-enabled',
'value' => 'true',
]);
$this->asAdmin()->delete("settings/users/{$editor->id}");
$this->assertDatabaseMissing('settings', [
'setting_key' => 'user:' . $editor->id . ':dark-mode-enabled',
]);
}
public function test_guest_profile_shows_limited_form() public function test_guest_profile_shows_limited_form()
{ {
$guest = User::getDefault(); $guest = User::getDefault();

View File

@ -50,7 +50,7 @@ class UserPreferencesTest extends TestCase
$editor = $this->getEditor(); $editor = $this->getEditor();
$this->actingAs($editor); $this->actingAs($editor);
$updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/books', [ $updateRequest = $this->patch('/preferences/change-sort/books', [
'sort' => 'created_at', 'sort' => 'created_at',
'order' => 'desc', 'order' => 'desc',
]); ]);
@ -73,7 +73,7 @@ class UserPreferencesTest extends TestCase
$editor = $this->getEditor(); $editor = $this->getEditor();
$this->actingAs($editor); $this->actingAs($editor);
$updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/dogs', [ $updateRequest = $this->patch('/preferences/change-sort/dogs', [
'sort' => 'name', 'sort' => 'name',
'order' => 'asc', 'order' => 'asc',
]); ]);
@ -88,7 +88,7 @@ class UserPreferencesTest extends TestCase
$editor = $this->getEditor(); $editor = $this->getEditor();
$this->actingAs($editor); $this->actingAs($editor);
$updateRequest = $this->patch('/settings/users/' . $editor->id . '/update-expansion-preference/home-details', ['expand' => 'true']); $updateRequest = $this->patch('/preferences/change-expansion/home-details', ['expand' => 'true']);
$updateRequest->assertStatus(204); $updateRequest->assertStatus(204);
$this->assertDatabaseHas('settings', [ $this->assertDatabaseHas('settings', [
@ -97,7 +97,7 @@ class UserPreferencesTest extends TestCase
]); ]);
$this->assertEquals(true, setting()->getForCurrentUser('section_expansion#home-details')); $this->assertEquals(true, setting()->getForCurrentUser('section_expansion#home-details'));
$invalidKeyRequest = $this->patch('/settings/users/' . $editor->id . '/update-expansion-preference/my-home-details', ['expand' => 'true']); $invalidKeyRequest = $this->patch('/preferences/change-expansion/my-home-details', ['expand' => 'true']);
$invalidKeyRequest->assertStatus(500); $invalidKeyRequest->assertStatus(500);
} }
@ -108,7 +108,7 @@ class UserPreferencesTest extends TestCase
$this->withHtml($home)->assertElementNotExists('.dark-mode'); $this->withHtml($home)->assertElementNotExists('.dark-mode');
$this->assertEquals(false, setting()->getForCurrentUser('dark-mode-enabled', false)); $this->assertEquals(false, setting()->getForCurrentUser('dark-mode-enabled', false));
$prefChange = $this->patch('/settings/users/toggle-dark-mode'); $prefChange = $this->patch('/preferences/toggle-dark-mode');
$prefChange->assertRedirect(); $prefChange->assertRedirect();
$this->assertEquals(true, setting()->getForCurrentUser('dark-mode-enabled')); $this->assertEquals(true, setting()->getForCurrentUser('dark-mode-enabled'));
@ -162,7 +162,7 @@ class UserPreferencesTest extends TestCase
->assertElementNotExists('.featured-image-container') ->assertElementNotExists('.featured-image-container')
->assertElementExists('.content-wrap .entity-list-item'); ->assertElementExists('.content-wrap .entity-list-item');
$req = $this->patch("/settings/users/{$editor->id}/switch-shelf-view", ['view_type' => 'grid']); $req = $this->patch("/preferences/change-view/bookshelf", ['view' => 'grid']);
$req->assertRedirect($shelf->getUrl()); $req->assertRedirect($shelf->getUrl());
$resp = $this->actingAs($editor)->get($shelf->getUrl()) $resp = $this->actingAs($editor)->get($shelf->getUrl())
@ -179,14 +179,14 @@ class UserPreferencesTest extends TestCase
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($editor); $this->actingAs($editor);
$this->patch('/settings/users/update-code-language-favourite', ['language' => 'php', 'active' => true]); $this->patch('/preferences/update-code-language-favourite', ['language' => 'php', 'active' => true]);
$this->patch('/settings/users/update-code-language-favourite', ['language' => 'javascript', 'active' => true]); $this->patch('/preferences/update-code-language-favourite', ['language' => 'javascript', 'active' => true]);
$resp = $this->get($page->getUrl('/edit')); $resp = $this->get($page->getUrl('/edit'));
$resp->assertSee('option:code-editor:favourites="php,javascript"', false); $resp->assertSee('option:code-editor:favourites="php,javascript"', false);
$this->patch('/settings/users/update-code-language-favourite', ['language' => 'ruby', 'active' => true]); $this->patch('/preferences/update-code-language-favourite', ['language' => 'ruby', 'active' => true]);
$this->patch('/settings/users/update-code-language-favourite', ['language' => 'php', 'active' => false]); $this->patch('/preferences/update-code-language-favourite', ['language' => 'php', 'active' => false]);
$resp = $this->get($page->getUrl('/edit')); $resp = $this->get($page->getUrl('/edit'));
$resp->assertSee('option:code-editor:favourites="javascript,ruby"', false); $resp->assertSee('option:code-editor:favourites="javascript,ruby"', false);