// 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)) {
// 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'),
namespace BookStack\Http\Controllers;
use BookStack\Auth\UserRepo;
+use BookStack\Settings\UserShortcutMap;
use Illuminate\Http\Request;
class UserPreferencesController extends Controller
}
/**
- * 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, [], "/");
}
/**
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);
}
array_splice($currentFavorites, $index, 1);
}
- setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
+ setting()->putForCurrentUser('code-language-favourites', implode(',', $currentFavorites));
}
}
/**
* 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
{
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.
*/
--- /dev/null
+<?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) ?: []);
+ }
+}
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm8 7H9c-.55 0-1-.45-1-1s.45-1 1-1h6c.55 0 1 .45 1 1s-.45 1-1 1zm1-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z"/></svg>
\ No newline at end of file
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
});
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"
"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,
--- /dev/null
+/**
+ * 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;
\ No newline at end of file
--- /dev/null
+function reverseMap(map) {
+ const reversed = {};
+ for (const [key, value] of Object.entries(map)) {
+ reversed[value] = key;
+ }
+ return reversed;
+}
+
+/**
+ * @extends {Component}
+ */
+class Shortcuts {
+
+ setup() {
+ this.container = this.$el;
+ this.mapById = JSON.parse(this.$opts.keyMap);
+ this.mapByShortcut = reverseMap(this.mapById);
+
+ this.hintsShowing = false;
+
+ this.hideHints = this.hideHints.bind(this);
+
+ this.setupListeners();
+ }
+
+ setupListeners() {
+ window.addEventListener('keydown', event => {
+
+ if (event.target.closest('input, select, textarea')) {
+ return;
+ }
+
+ this.handleShortcutPress(event);
+ });
+
+ window.addEventListener('keydown', event => {
+ if (event.key === '?') {
+ this.hintsShowing ? this.hideHints() : this.showHints();
+ }
+ });
+ }
+
+ /**
+ * @param {KeyboardEvent} event
+ */
+ handleShortcutPress(event) {
+
+ const keys = [
+ event.ctrlKey ? 'Ctrl' : '',
+ event.metaKey ? 'Cmd' : '',
+ event.key,
+ ];
+
+ const combo = keys.filter(s => Boolean(s)).join(' + ');
+
+ const shortcutId = this.mapByShortcut[combo];
+ if (shortcutId) {
+ const wasHandled = this.runShortcut(shortcutId);
+ if (wasHandled) {
+ event.preventDefault();
+ }
+ }
+ }
+
+ /**
+ * Run the given shortcut, and return a boolean to indicate if the event
+ * was successfully handled by a shortcut action.
+ * @param {String} id
+ * @return {boolean}
+ */
+ runShortcut(id) {
+ const el = this.container.querySelector(`[data-shortcut="${id}"]`);
+ if (!el) {
+ return false;
+ }
+
+ if (el.matches('input, textarea, select')) {
+ el.focus();
+ return true;
+ }
+
+ if (el.matches('a, button')) {
+ el.click();
+ return true;
+ }
+
+ if (el.matches('div[tabindex]')) {
+ el.click();
+ el.focus();
+ return true;
+ }
+
+ console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
+
+ return false;
+ }
+
+ showHints() {
+ const wrapper = document.createElement('div');
+ wrapper.classList.add('shortcut-container');
+ this.container.append(wrapper);
+
+ const shortcutEls = this.container.querySelectorAll('[data-shortcut]');
+ const displayedIds = new Set();
+ for (const shortcutEl of shortcutEls) {
+ const id = shortcutEl.getAttribute('data-shortcut');
+ if (displayedIds.has(id)) {
+ continue;
+ }
+
+ const key = this.mapById[id];
+ this.showHintLabel(shortcutEl, key, wrapper);
+ displayedIds.add(id);
+ }
+
+ window.addEventListener('scroll', this.hideHints);
+ window.addEventListener('focus', this.hideHints);
+ window.addEventListener('blur', this.hideHints);
+ window.addEventListener('click', this.hideHints);
+
+ this.hintsShowing = true;
+ }
+
+ /**
+ * @param {Element} targetEl
+ * @param {String} key
+ * @param {Element} wrapper
+ */
+ showHintLabel(targetEl, key, wrapper) {
+ const targetBounds = targetEl.getBoundingClientRect();
+
+ const label = document.createElement('div');
+ label.classList.add('shortcut-hint');
+ label.textContent = key;
+
+ const linkage = document.createElement('div');
+ linkage.classList.add('shortcut-linkage');
+ linkage.style.left = targetBounds.x + 'px';
+ linkage.style.top = targetBounds.y + 'px';
+ linkage.style.width = targetBounds.width + 'px';
+ linkage.style.height = targetBounds.height + 'px';
+
+ wrapper.append(label, linkage);
+
+ const labelBounds = label.getBoundingClientRect();
+
+ label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;
+ label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
+ }
+
+ hideHints() {
+ const wrapper = this.container.querySelector('.shortcut-container');
+ wrapper.remove();
+
+ window.removeEventListener('scroll', this.hideHints);
+ window.removeEventListener('focus', this.hideHints);
+ window.removeEventListener('blur', this.hideHints);
+ window.removeEventListener('click', this.hideHints);
+
+ this.hintsShowing = false;
+ }
+}
+
+export default Shortcuts;
\ No newline at end of file
'actions' => 'Actions',
'view' => 'View',
'view_all' => 'View All',
+ 'new' => 'New',
'create' => 'Create',
'update' => 'Update',
'edit' => 'Edit',
'none' => 'None',
// Header
+ 'homepage' => 'Homepage',
'header_menu_expand' => 'Expand Header Menu',
'profile_menu' => 'Profile Menu',
'view_profile' => 'View Profile',
'edit_profile' => 'Edit Profile',
'dark_mode' => 'Dark Mode',
'light_mode' => 'Light Mode',
+ 'global_search' => 'Global Search',
// Layout tabs
'tab_info' => 'Info',
--- /dev/null
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+ 'shortcuts' => 'Shortcuts',
+ 'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+ 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+ 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+ 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+ 'shortcuts_section_navigation' => 'Navigation',
+ 'shortcuts_section_actions' => 'Common Actions',
+ 'shortcuts_save' => 'Save Shortcuts',
+ 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+ 'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
}
.status-indicator-inactive {
background-color: $negative;
+}
+
+.shortcut-container {
+ background-color: rgba(0, 0, 0, 0.25);
+ pointer-events: none;
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 99;
+}
+.shortcut-linkage {
+ position: fixed;
+ box-shadow: 0 0 4px 0 #FFF;
+ border-radius: 3px;
+}
+.shortcut-hint {
+ position: fixed;
+ padding: $-xxs $-xxs;
+ font-size: .85rem;
+ font-weight: 700;
+ line-height: 1;
+ background-color: #eee;
+ border-radius: 3px;
+ border: 1px solid #b4b4b4;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
+ color: #333;
}
\ No newline at end of file
.custom-file-input:focus + label {
border-color: var(--color-primary);
outline: 1px solid var(--color-primary);
+}
+
+input.shortcut-input {
+ width: auto;
+ max-width: 120px;
+ height: auto;
}
\ No newline at end of file
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@if(user()->can('book-create-all'))
- <a href="{{ url("/create-book") }}" class="icon-list-item">
+ <a href="{{ url("/create-book") }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span>
<span>{{ trans('entities.books_create') }}</span>
</a>
<div class="icon-list text-primary">
@if(userCan('page-create', $book))
- <a href="{{ $book->getUrl('/create-page') }}" class="icon-list-item">
+ <a href="{{ $book->getUrl('/create-page') }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span>
<span>{{ trans('entities.pages_new') }}</span>
</a>
@endif
@if(userCan('chapter-create', $book))
- <a href="{{ $book->getUrl('/create-chapter') }}" class="icon-list-item">
+ <a href="{{ $book->getUrl('/create-chapter') }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span>
<span>{{ trans('entities.chapters_new') }}</span>
</a>
<hr class="primary-background">
@if(userCan('book-update', $book))
- <a href="{{ $book->getUrl('/edit') }}" class="icon-list-item">
+ <a href="{{ $book->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
<span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span>
</a>
- <a href="{{ $book->getUrl('/sort') }}" class="icon-list-item">
+ <a href="{{ $book->getUrl('/sort') }}" data-shortcut="sort" class="icon-list-item">
<span>@icon('sort')</span>
<span>{{ trans('common.sort') }}</span>
</a>
@endif
@if(userCan('book-create-all'))
- <a href="{{ $book->getUrl('/copy') }}" class="icon-list-item">
+ <a href="{{ $book->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
<span>@icon('copy')</span>
<span>{{ trans('common.copy') }}</span>
</a>
@endif
@if(userCan('restrictions-manage', $book))
- <a href="{{ $book->getUrl('/permissions') }}" class="icon-list-item">
+ <a href="{{ $book->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span>
</a>
@endif
@if(userCan('book-delete', $book))
- <a href="{{ $book->getUrl('/delete') }}" class="icon-list-item">
+ <a href="{{ $book->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span>
</a>
<div class="icon-list text-primary">
@if(userCan('page-create', $chapter))
- <a href="{{ $chapter->getUrl('/create-page') }}" class="icon-list-item">
+ <a href="{{ $chapter->getUrl('/create-page') }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span>
<span>{{ trans('entities.pages_new') }}</span>
</a>
<hr class="primary-background"/>
@if(userCan('chapter-update', $chapter))
- <a href="{{ $chapter->getUrl('/edit') }}" class="icon-list-item">
+ <a href="{{ $chapter->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
<span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span>
</a>
@endif
@if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCan('chapter-create-all') || userCan('chapter-create-own'))
- <a href="{{ $chapter->getUrl('/copy') }}" class="icon-list-item">
+ <a href="{{ $chapter->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
<span>@icon('copy')</span>
<span>{{ trans('common.copy') }}</span>
</a>
@endif
@if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter))
- <a href="{{ $chapter->getUrl('/move') }}" class="icon-list-item">
+ <a href="{{ $chapter->getUrl('/move') }}" data-shortcut="move" class="icon-list-item">
<span>@icon('folder')</span>
<span>{{ trans('common.move') }}</span>
</a>
@endif
@if(userCan('restrictions-manage', $chapter))
- <a href="{{ $chapter->getUrl('/permissions') }}" class="icon-list-item">
+ <a href="{{ $chapter->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span>
</a>
@endif
@if(userCan('chapter-delete', $chapter))
- <a href="{{ $chapter->getUrl('/delete') }}" class="icon-list-item">
+ <a href="{{ $chapter->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span>
</a>
@if($chapter->book && userCan('book-update', $chapter->book))
<hr class="primary-background"/>
- <a href="{{ $chapter->book->getUrl('/sort') }}" class="icon-list-item">
+ <a href="{{ $chapter->book->getUrl('/sort') }}" data-shortcut="sort" class="icon-list-item">
<span>@icon('sort')</span>
<span>{{ trans('entities.chapter_sort_book') }}</span>
</a>
-<form action="{{ url('/settings/users/toggle-dark-mode') }}" method="post">
+<form action="{{ url('/preferences/toggle-dark-mode') }}" method="post">
{{ csrf_field() }}
{{ method_field('patch') }}
@if(setting()->getForCurrentUser('dark-mode-enabled'))
<div class="grid mx-l">
<div>
- <a href="{{ url('/') }}" class="logo">
+ <a href="{{ url('/') }}" data-shortcut="home_view" class="logo">
@if(setting('app-logo', '') !== 'none')
<img class="logo-image" src="{{ setting('app-logo', '') === '' ? url('/logo.png') : url(setting('app-logo', '')) }}" alt="Logo">
@endif
<form action="{{ url('/search') }}" method="GET" class="search-box" role="search">
<button id="header-search-box-button" type="submit" aria-label="{{ trans('common.search') }}" tabindex="-1">@icon('search') </button>
<input id="header-search-box-input" type="text" name="term"
+ data-shortcut="global_search"
aria-label="{{ trans('common.search') }}" placeholder="{{ trans('common.search') }}"
value="{{ isset($searchTerm) ? $searchTerm : '' }}">
</form>
@if (hasAppAccess())
<a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
@if(userCanOnAny('view', \BookStack\Entities\Models\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
- <a href="{{ url('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
+ <a href="{{ url('/shelves') }}" data-shortcut="shelves_view">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif
- <a href="{{ url('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
+ <a href="{{ url('/books') }}" data-shortcut="books_view">@icon('books'){{ trans('entities.books') }}</a>
@if(signedInUser() && userCan('settings-manage'))
- <a href="{{ url('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
+ <a href="{{ url('/settings') }}" data-shortcut="settings_view">@icon('settings'){{ trans('settings.settings') }}</a>
@endif
@if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
- <a href="{{ url('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
+ <a href="{{ url('/settings/users') }}" data-shortcut="settings_view">@icon('users'){{ trans('settings.users') }}</a>
@endif
@endif
</span>
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li>
- <a href="{{ url('/favourites') }}" class="icon-item">
+ <a href="{{ url('/favourites') }}" data-shortcut="favourites_view" class="icon-item">
@icon('star')
<div>{{ trans('entities.my_favourites') }}</div>
</a>
</li>
<li>
- <a href="{{ $currentUser->getProfileUrl() }}" class="icon-item">
+ <a href="{{ $currentUser->getProfileUrl() }}" data-shortcut="profile_view" class="icon-item">
@icon('user')
<div>{{ trans('common.view_profile') }}</div>
</a>
<form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
method="post">
{{ csrf_field() }}
- <button class="icon-item">
+ <button class="icon-item" data-shortcut="logout">
@icon('logout')
<div>{{ trans('auth.logout') }}</div>
</button>
</form>
</li>
<li><hr></li>
+ <li>
+ <a href="{{ url('/preferences/shortcuts') }}" class="icon-item">
+ @icon('shortcuts')
+ <div>{{ trans('preferences.shortcuts') }}</div>
+ </a>
+ </li>
<li>
@include('common.dark-mode-toggle', ['classes' => 'icon-item'])
</li>
action="{{ url()->current() }}"
method="get"
@else
- action="{{ url("/settings/users/". user()->id ."/change-sort/{$type}") }}"
+ action="{{ url("/preferences/change-sort/{$type}") }}"
method="post"
@endif
>
class="dropdown-container"
id="export-menu">
- <div refs="dropdown@toggle" class="icon-list-item"
- aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('entities.export') }}" tabindex="0">
+ <div refs="dropdown@toggle"
+ class="icon-list-item"
+ aria-haspopup="true"
+ aria-expanded="false"
+ aria-label="{{ trans('entities.export') }}"
+ data-shortcut="export"
+ tabindex="0">
<span>@icon('export')</span>
<span>{{ trans('entities.export') }}</span>
</div>
{{ csrf_field() }}
<input type="hidden" name="type" value="{{ get_class($entity) }}">
<input type="hidden" name="id" value="{{ $entity->id }}">
- <button type="submit" class="icon-list-item text-primary">
+ <button type="submit" data-shortcut="favourite" class="icon-list-item text-primary">
<span>@icon($isFavourite ? 'star' : 'star-outline')</span>
<span>{{ $isFavourite ? trans('common.unfavourite') : trans('common.favourite') }}</span>
</button>
<div id="sibling-navigation" class="grid half collapse-xs items-center mb-m px-m no-row-gap fade-in-when-active print-hidden">
<div>
@if($previous)
- <a href="{{ $previous->getUrl() }}" class="outline-hover no-link-style block rounded">
+ <a href="{{ $previous->getUrl() }}" data-shortcut="previous" class="outline-hover no-link-style block rounded">
<div class="px-m pt-xs text-muted">{{ trans('common.previous') }}</div>
<div class="inline-block">
<div class="icon-list-item no-hover">
</div>
<div>
@if($next)
- <a href="{{ $next->getUrl() }}" class="outline-hover no-link-style block rounded text-xs-right">
+ <a href="{{ $next->getUrl() }}" data-shortcut="next" class="outline-hover no-link-style block rounded text-xs-right">
<div class="px-m pt-xs text-muted text-xs-right">{{ trans('common.next') }}</div>
<div class="inline block">
<div class="icon-list-item no-hover">
<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() !!}
{!! method_field('PATCH') !!}
- <input type="hidden" value="{{ $view === 'list'? 'grid' : 'list' }}" name="view_type">
+
@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>{{ trans('common.grid_view') }}</span>
</button>
@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>{{ trans('common.list_view') }}</span>
</button>
--}}
<?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
<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' }}"
class="icon-list-item {{ $classes ?? '' }}">
<span>@icon('expand-text')</span>
<span>{{ trans('entities.shelves_new_action') }}</span>
</a>
@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('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
</div>
<!-- Translations for JS -->
@stack('translations')
</head>
-<body class="@stack('body-class')">
+<body
+ @if(setting()->getForCurrentUser('ui-shortcuts-enabled', false))
+ component="shortcuts"
+ option:shortcuts:key-map="{{ \BookStack\Settings\UserShortcutMap::fromUserPreferences()->toJson() }}"
+ @endif
+ class="@stack('body-class')">
@include('layouts.parts.base-body-start')
@include('common.skip-to-content')
{{--User Actions--}}
@if(userCan('page-update', $page))
- <a href="{{ $page->getUrl('/edit') }}" class="icon-list-item">
+ <a href="{{ $page->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
<span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span>
</a>
@endif
@if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCanOnAny('create', \BookStack\Entities\Models\Chapter::class) || userCan('page-create-all') || userCan('page-create-own'))
- <a href="{{ $page->getUrl('/copy') }}" class="icon-list-item">
+ <a href="{{ $page->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
<span>@icon('copy')</span>
<span>{{ trans('common.copy') }}</span>
</a>
@endif
@if(userCan('page-update', $page))
@if(userCan('page-delete', $page))
- <a href="{{ $page->getUrl('/move') }}" class="icon-list-item">
+ <a href="{{ $page->getUrl('/move') }}" data-shortcut="move" class="icon-list-item">
<span>@icon('folder')</span>
<span>{{ trans('common.move') }}</span>
</a>
@endif
@endif
- <a href="{{ $page->getUrl('/revisions') }}" class="icon-list-item">
+ <a href="{{ $page->getUrl('/revisions') }}" data-shortcut="revisions" class="icon-list-item">
<span>@icon('history')</span>
<span>{{ trans('entities.revisions') }}</span>
</a>
@if(userCan('restrictions-manage', $page))
- <a href="{{ $page->getUrl('/permissions') }}" class="icon-list-item">
+ <a href="{{ $page->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span>
</a>
@endif
@if(userCan('page-delete', $page))
- <a href="{{ $page->getUrl('/delete') }}" class="icon-list-item">
+ <a href="{{ $page->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span>
</a>
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@if(userCan('bookshelf-create-all'))
- <a href="{{ url("/create-shelf") }}" class="icon-list-item">
+ <a href="{{ url("/create-shelf") }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span>
<span>{{ trans('entities.shelves_new_action') }}</span>
</a>
@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">
<span>@icon('tag')</span>
<div class="icon-list text-primary">
@if(userCan('book-create-all') && userCan('bookshelf-update', $shelf))
- <a href="{{ $shelf->getUrl('/create-book') }}" class="icon-list-item">
+ <a href="{{ $shelf->getUrl('/create-book') }}" data-shortcut="new" class="icon-list-item">
<span class="icon">@icon('add')</span>
<span>{{ trans('entities.books_new_action') }}</span>
</a>
@endif
- @include('entities.view-toggle', ['view' => $view, 'type' => 'shelf'])
+ @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelf'])
<hr class="primary-background">
@if(userCan('bookshelf-update', $shelf))
- <a href="{{ $shelf->getUrl('/edit') }}" class="icon-list-item">
+ <a href="{{ $shelf->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
<span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span>
</a>
@endif
@if(userCan('restrictions-manage', $shelf))
- <a href="{{ $shelf->getUrl('/permissions') }}" class="icon-list-item">
+ <a href="{{ $shelf->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span>
</a>
@endif
@if(userCan('bookshelf-delete', $shelf))
- <a href="{{ $shelf->getUrl('/delete') }}" class="icon-list-item">
+ <a href="{{ $shelf->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span>
</a>
--- /dev/null
+<div class="flex-container-row justify-space-between items-center gap-m item-list-row">
+ <label for="shortcut-{{ $id }}" class="bold flex px-m py-xs">{{ $label }}</label>
+ <div class="px-m py-xs">
+ <input type="text"
+ component="shortcut-input"
+ class="small flex-none shortcut-input px-s py-xs"
+ id="shortcut-{{ $id }}"
+ name="shortcut[{{ $id }}]"
+ readonly
+ value="{{ $shortcuts->getShortcut($id) }}">
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+@extends('layouts.simple')
+
+@section('body')
+ <div class="container small my-xl">
+
+ <section class="card content-wrap">
+ <form action="{{ url('/preferences/shortcuts') }}" method="post">
+ {{ method_field('put') }}
+ {{ csrf_field() }}
+
+ <h1 class="list-heading">{{ trans('preferences.shortcuts_interface') }}</h1>
+
+ <div class="flex-container-row items-center gap-m wrap mb-m">
+ <p class="flex mb-none min-width-m text-small text-muted">
+ {{ trans('preferences.shortcuts_toggle_desc') }}
+ {{ trans('preferences.shortcuts_customize_desc') }}
+ </p>
+ <div class="flex min-width-m text-m-center">
+ @include('form.toggle-switch', [
+ 'name' => 'enabled',
+ 'value' => $enabled,
+ 'label' => trans('preferences.shortcuts_toggle_label'),
+ ])
+ </div>
+ </div>
+
+ <hr>
+
+ <h2 class="list-heading mb-m">{{ trans('preferences.shortcuts_section_navigation') }}</h2>
+ <div class="flex-container-row wrap gap-m mb-xl">
+ <div class="flex min-width-l item-list">
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.homepage'), 'id' => 'home_view'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.shelves'), 'id' => 'shelves_view'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.books'), 'id' => 'books_view'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('settings.settings'), 'id' => 'settings_view'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.my_favourites'), 'id' => 'favourites_view'])
+ </div>
+ <div class="flex min-width-l item-list">
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.view_profile'), 'id' => 'profile_view'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('auth.logout'), 'id' => 'logout'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.global_search'), 'id' => 'global_search'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.next'), 'id' => 'next'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.previous'), 'id' => 'previous'])
+ </div>
+ </div>
+
+ <h2 class="list-heading mb-m">{{ trans('preferences.shortcuts_section_actions') }}</h2>
+ <div class="flex-container-row wrap gap-m mb-xl">
+ <div class="flex min-width-l item-list">
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.new'), 'id' => 'new'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.edit'), 'id' => 'edit'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.copy'), 'id' => 'copy'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.delete'), 'id' => 'delete'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.favourite'), 'id' => 'favourite'])
+ </div>
+ <div class="flex min-width-l item-list">
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.export'), 'id' => 'export'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.sort'), 'id' => 'sort'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.permissions'), 'id' => 'permissions'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('common.move'), 'id' => 'move'])
+ @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.revisions'), 'id' => 'revisions'])
+ </div>
+ </div>
+
+ <p class="text-small text-muted">{{ trans('preferences.shortcuts_overlay_desc') }}</p>
+
+ <div class="form-group text-right">
+ <button class="button">{{ trans('preferences.shortcuts_save') }}</button>
+ </div>
+
+ </form>
+ </section>
+
+ </div>
+@stop
Route::delete('/settings/users/{id}', [UserController::class, 'destroy']);
// User Preferences
- Route::patch('/settings/users/{id}/switch-books-view', [UserPreferencesController::class, 'switchBooksView']);
- Route::patch('/settings/users/{id}/switch-shelves-view', [UserPreferencesController::class, 'switchShelvesView']);
- Route::patch('/settings/users/{id}/switch-shelf-view', [UserPreferencesController::class, 'switchShelfView']);
- Route::patch('/settings/users/{id}/change-sort/{type}', [UserPreferencesController::class, 'changeSort']);
- Route::patch('/settings/users/{id}/update-expansion-preference/{key}', [UserPreferencesController::class, 'updateExpansionPreference']);
- Route::patch('/settings/users/toggle-dark-mode', [UserPreferencesController::class, 'toggleDarkMode']);
- Route::patch('/settings/users/update-code-language-favourite', [UserPreferencesController::class, 'updateCodeLanguageFavourite']);
+ Route::redirect('/preferences', '/');
+ Route::get('/preferences/shortcuts', [UserPreferencesController::class, 'showShortcuts']);
+ Route::put('/preferences/shortcuts', [UserPreferencesController::class, 'updateShortcuts']);
+ Route::patch('/preferences/change-view/{type}', [UserPreferencesController::class, 'changeView']);
+ Route::patch('/preferences/change-sort/{type}', [UserPreferencesController::class, 'changeSort']);
+ Route::patch('/preferences/change-expansion/{type}', [UserPreferencesController::class, 'changeExpansion']);
+ Route::patch('/preferences/toggle-dark-mode', [UserPreferencesController::class, 'toggleDarkMode']);
+ Route::patch('/preferences/update-code-language-favourite', [UserPreferencesController::class, 'updateCodeLanguageFavourite']);
// User API Tokens
Route::get('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'create']);
setting()->putUser($editor, 'books_view_type', 'list');
$resp = $this->actingAs($editor)->get('/books');
- $this->withHtml($resp)->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'Grid View');
- $this->withHtml($resp)->assertElementExists('input[name="view_type"][value="grid"]');
+ $this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'Grid View');
+ $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();
$this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));
$resp = $this->actingAs($editor)->get('/books');
- $this->withHtml($resp)->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'List View');
- $this->withHtml($resp)->assertElementExists('input[name="view_type"][value="list"]');
+ $this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'List View');
+ $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();
$this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));
}
]);
}
+ 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()
{
$guest = User::getDefault();
class UserPreferencesTest extends TestCase
{
+ public function test_interface_shortcuts_updating()
+ {
+ $this->asEditor();
+
+ // View preferences with defaults
+ $resp = $this->get('/preferences/shortcuts');
+ $resp->assertSee('Interface Keyboard Shortcuts');
+
+ $html = $this->withHtml($resp);
+ $html->assertFieldHasValue('enabled', 'false');
+ $html->assertFieldHasValue('shortcut[home_view]', '1');
+
+ // Update preferences
+ $resp = $this->put('/preferences/shortcuts', [
+ 'enabled' => 'true',
+ 'shortcut' => ['home_view' => 'Ctrl + 1'],
+ ]);
+
+ $resp->assertRedirect('/preferences/shortcuts');
+ $resp->assertSessionHas('success', 'Shortcut preferences have been updated!');
+
+ // View updates to preferences page
+ $resp = $this->get('/preferences/shortcuts');
+ $html = $this->withHtml($resp);
+ $html->assertFieldHasValue('enabled', 'true');
+ $html->assertFieldHasValue('shortcut[home_view]', 'Ctrl + 1');
+ }
+
+ public function test_body_has_shortcuts_component_when_active()
+ {
+ $editor = $this->getEditor();
+ $this->actingAs($editor);
+
+ $this->withHtml($this->get('/'))->assertElementNotExists('body[component="shortcuts"]');
+
+ setting()->putUser($editor, 'ui-shortcuts-enabled', 'true');
+ $this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]');
+ }
+
public function test_update_sort_preference()
{
$editor = $this->getEditor();
$this->actingAs($editor);
- $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/books', [
+ $updateRequest = $this->patch('/preferences/change-sort/books', [
'sort' => 'created_at',
'order' => 'desc',
]);
$editor = $this->getEditor();
$this->actingAs($editor);
- $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/dogs', [
+ $updateRequest = $this->patch('/preferences/change-sort/dogs', [
'sort' => 'name',
'order' => 'asc',
]);
$editor = $this->getEditor();
$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);
$this->assertDatabaseHas('settings', [
]);
$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);
}
$this->withHtml($home)->assertElementNotExists('.dark-mode');
$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();
$this->assertEquals(true, setting()->getForCurrentUser('dark-mode-enabled'));
->assertElementNotExists('.featured-image-container')
->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());
$resp = $this->actingAs($editor)->get($shelf->getUrl())
$page = $this->entities->page();
$this->actingAs($editor);
- $this->patch('/settings/users/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' => 'php', 'active' => true]);
+ $this->patch('/preferences/update-code-language-favourite', ['language' => 'javascript', 'active' => true]);
$resp = $this->get($page->getUrl('/edit'));
$resp->assertSee('option:code-editor:favourites="php,javascript"', false);
- $this->patch('/settings/users/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' => 'ruby', 'active' => true]);
+ $this->patch('/preferences/update-code-language-favourite', ['language' => 'php', 'active' => false]);
$resp = $this->get($page->getUrl('/edit'));
$resp->assertSee('option:code-editor:favourites="javascript,ruby"', false);