Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/Auth/UserRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ public function destroy(User $user, ?int $newOwnerId = null)
// Delete user profile images
$this->userAvatar->destroyAllForUser($user);

// Delete related activities
setting()->deleteUserSettings($user->id);

if (!empty($newOwnerId)) {
$newOwner = User::query()->find($newOwnerId);
if (!is_null($newOwner)) {
Expand Down
2 changes: 2 additions & 0 deletions app/Config/setting-defaults.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

// User-level default settings
'user' => [
'ui-shortcuts' => '{}',
'ui-shortcuts-enabled' => false,
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'),
Expand Down
82 changes: 43 additions & 39 deletions app/Http/Controllers/UserPreferencesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers;

use BookStack\Auth\UserRepo;
use BookStack\Settings\UserShortcutMap;
use Illuminate\Http\Request;

class UserPreferencesController extends Controller
Expand All @@ -15,70 +16,76 @@ public function __construct(UserRepo $userRepo)
}

/**
* Update the user's preferred book-list display setting.
* Show the user-specific interface shortcuts.
*/
public function switchBooksView(Request $request, int $id)
public function showShortcuts()
{
return $this->switchViewType($id, $request, 'books');
}
$shortcuts = UserShortcutMap::fromUserPreferences();
$enabled = setting()->getForCurrentUser('ui-shortcuts-enabled', false);

/**
* Update the user's preferred shelf-list display setting.
*/
public function switchShelvesView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'bookshelves');
return view('users.preferences.shortcuts', [
'shortcuts' => $shortcuts,
'enabled' => $enabled,
]);
}

/**
* Update the user's preferred shelf-view book list display setting.
* Update the user-specific interface shortcuts.
*/
public function switchShelfView(Request $request, int $id)
public function updateShortcuts(Request $request)
{
return $this->switchViewType($id, $request, 'bookshelf');
$enabled = $request->get('enabled') === 'true';
$providedShortcuts = $request->get('shortcut', []);
$shortcuts = new UserShortcutMap($providedShortcuts);

setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
setting()->putForCurrentUser('ui-shortcuts-enabled', $enabled);

$this->showSuccessNotification(trans('preferences.shortcuts_update_success'));

return redirect('/preferences/shortcuts');
}

/**
* For a type of list, switch with stored view type for a user.
* Update the preferred view format for a list view of the given type.
*/
protected function switchViewType(int $userId, Request $request, string $listName)
public function changeView(Request $request, string $type)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$valueViewTypes = ['books', 'bookshelves', 'bookshelf'];
if (!in_array($type, $valueViewTypes)) {
return redirect()->back(500);
}

$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
$view = $request->get('view');
if (!in_array($view, ['grid', 'list'])) {
$view = 'list';
}

$user = $this->userRepo->getById($userId);
$key = $listName . '_view_type';
setting()->putUser($user, $key, $viewType);
$key = $type . '_view_type';
setting()->putForCurrentUser($key, $view);

return redirect()->back(302, [], "/settings/users/$userId");
return redirect()->back(302, [], "/");
}

/**
* 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'];
if (!in_array($type, $validSortTypes)) {
return redirect()->back(500);
}

$this->checkPermissionOrCurrentUser('users-manage', $id);

$sort = substr($request->get('sort') ?: 'name', 0, 50);
$order = $request->get('order') === 'desc' ? 'desc' : 'asc';

$user = $this->userRepo->getById($id);
$sortKey = $type . '_sort';
$orderKey = $type . '_sort_order';
setting()->putUser($user, $sortKey, $sort);
setting()->putUser($user, $orderKey, $order);
setting()->putForCurrentUser($sortKey, $sort);
setting()->putForCurrentUser($orderKey, $order);

return redirect()->back(302, [], "/settings/users/{$id}");
return redirect()->back(302, [], "/");
}

/**
Expand All @@ -87,26 +94,23 @@ public function changeSort(Request $request, string $id, string $type)
public function toggleDarkMode()
{
$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();
}

/**
* 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);
$keyWhitelist = ['home-details'];
if (!in_array($key, $keyWhitelist)) {
$typeWhitelist = ['home-details'];
if (!in_array($type, $typeWhitelist)) {
return response('Invalid key', 500);
}

$newState = $request->get('expand', 'false');

$user = $this->userRepo->getById($id);
setting()->putUser($user, 'section_expansion#' . $key, $newState);
setting()->putForCurrentUser('section_expansion#' . $type, $newState);

return response('', 204);
}
Expand All @@ -129,6 +133,6 @@ public function updateCodeLanguageFavourite(Request $request)
array_splice($currentFavorites, $index, 1);
}

setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
setting()->putForCurrentUser('code-language-favourites', implode(',', $currentFavorites));
}
}
12 changes: 12 additions & 0 deletions app/Settings/SettingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ protected function formatArrayValue(array $value): string

/**
* 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
{
Expand All @@ -206,6 +208,16 @@ public function putUser(User $user, string $key, string $value): bool
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.
*/
Expand Down
82 changes: 82 additions & 0 deletions app/Settings/UserShortcutMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace BookStack\Settings;

class UserShortcutMap
{
protected const DEFAULTS = [
// Header actions
"home_view" => "1",
"shelves_view" => "2",
"books_view" => "3",
"settings_view" => "4",
"favourites_view" => "5",
"profile_view" => "6",
"global_search" => "/",
"logout" => "0",

// Common actions
"edit" => "e",
"new" => "n",
"copy" => "c",
"delete" => "d",
"favourite" => "f",
"export" => "x",
"sort" => "s",
"permissions" => "p",
"move" => "m",
"revisions" => "r",

// Navigation
"next" => "ArrowRight",
"previous" => "ArrowLeft",
];

/**
* @var array<string, string>
*/
protected array $mapping;

public function __construct(array $map)
{
$this->mapping = static::DEFAULTS;
$this->merge($map);
}

/**
* Merge the given map into the current shortcut mapping.
*/
protected function merge(array $map): void
{
foreach ($map as $key => $value) {
if (is_string($value) && isset($this->mapping[$key])) {
$this->mapping[$key] = $value;
}
}
}

/**
* Get the shortcut defined for the given ID.
*/
public function getShortcut(string $id): string
{
return $this->mapping[$id] ?? '';
}

/**
* Convert this mapping to JSON.
*/
public function toJson(): string
{
return json_encode($this->mapping);
}

/**
* Create a new instance from the current user's preferences.
*/
public static function fromUserPreferences(): self
{
$userKeyMap = setting()->getForCurrentUser('ui-shortcuts');
return new self(json_decode($userKeyMap, true) ?: []);
}
}
1 change: 1 addition & 0 deletions resources/icons/shortcuts.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion resources/js/components/code-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class CodeEditor {
isFavorite ? this.favourites.add(language) : this.favourites.delete(language);
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,
active: isFavorite
});
Expand Down
4 changes: 4 additions & 0 deletions resources/js/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import popup from "./popup.js"
import settingAppColorPicker from "./setting-app-color-picker.js"
import settingColorPicker from "./setting-color-picker.js"
import shelfSort from "./shelf-sort.js"
import shortcuts from "./shortcuts";
import shortcutInput from "./shortcut-input";
import sidebar from "./sidebar.js"
import sortableList from "./sortable-list.js"
import submitOnChange from "./submit-on-change.js"
Expand Down Expand Up @@ -101,6 +103,8 @@ const componentMapping = {
"setting-app-color-picker": settingAppColorPicker,
"setting-color-picker": settingColorPicker,
"shelf-sort": shelfSort,
"shortcuts": shortcuts,
"shortcut-input": shortcutInput,
"sidebar": sidebar,
"sortable-list": sortableList,
"submit-on-change": submitOnChange,
Expand Down
57 changes: 57 additions & 0 deletions resources/js/components/shortcut-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Keys to ignore when recording shortcuts.
* @type {string[]}
*/
const ignoreKeys = ['Control', 'Alt', 'Shift', 'Meta', 'Super', ' ', '+', 'Tab', 'Escape'];

/**
* @extends {Component}
*/
class ShortcutInput {

setup() {
this.input = this.$el;

this.setupListeners();
}

setupListeners() {
this.listenerRecordKey = this.listenerRecordKey.bind(this);

this.input.addEventListener('focus', () => {
this.startListeningForInput();
});

this.input.addEventListener('blur', () => {
this.stopListeningForInput();
})
}

startListeningForInput() {
this.input.addEventListener('keydown', this.listenerRecordKey)
}

/**
* @param {KeyboardEvent} event
*/
listenerRecordKey(event) {
if (ignoreKeys.includes(event.key)) {
return;
}

const keys = [
event.ctrlKey ? 'Ctrl' : '',
event.metaKey ? 'Cmd' : '',
event.key,
];

this.input.value = keys.filter(s => Boolean(s)).join(' + ');
}

stopListeningForInput() {
this.input.removeEventListener('keydown', this.listenerRecordKey);
}

}

export default ShortcutInput;
Loading