mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Merge pull request #3830 from BookStackApp/shortcuts
User interface shortcuts system
This commit is contained in:
commit
d520d6cab8
@ -158,6 +158,9 @@ class UserRepo
|
||||
// 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)) {
|
||||
|
@ -26,6 +26,8 @@ return [
|
||||
|
||||
// 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'),
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Settings\UserShortcutMap;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserPreferencesController extends Controller
|
||||
@ -15,70 +16,76 @@ 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);
|
||||
|
||||
return view('users.preferences.shortcuts', [
|
||||
'shortcuts' => $shortcuts,
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's preferred shelf-list display setting.
|
||||
* Update the user-specific interface shortcuts.
|
||||
*/
|
||||
public function switchShelvesView(Request $request, int $id)
|
||||
public function updateShortcuts(Request $request)
|
||||
{
|
||||
return $this->switchViewType($id, $request, 'bookshelves');
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's preferred shelf-view book list display setting.
|
||||
* Update the preferred view format for a list view of the given type.
|
||||
*/
|
||||
public function switchShelfView(Request $request, int $id)
|
||||
public function changeView(Request $request, string $type)
|
||||
{
|
||||
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';
|
||||
$valueViewTypes = ['books', 'bookshelves', 'bookshelf'];
|
||||
if (!in_array($type, $valueViewTypes)) {
|
||||
return redirect()->back(500);
|
||||
}
|
||||
|
||||
$user = $this->userRepo->getById($userId);
|
||||
$key = $listName . '_view_type';
|
||||
setting()->putUser($user, $key, $viewType);
|
||||
$view = $request->get('view');
|
||||
if (!in_array($view, ['grid', 'list'])) {
|
||||
$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.
|
||||
*/
|
||||
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, [], "/");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,7 +94,7 @@ class UserPreferencesController extends Controller
|
||||
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();
|
||||
}
|
||||
@ -95,18 +102,15 @@ class UserPreferencesController extends Controller
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@ -129,6 +133,6 @@ class UserPreferencesController extends Controller
|
||||
array_splice($currentFavorites, $index, 1);
|
||||
}
|
||||
|
||||
setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
|
||||
setting()->putForCurrentUser('code-language-favourites', implode(',', $currentFavorites));
|
||||
}
|
||||
}
|
||||
|
@ -194,6 +194,8 @@ class SettingService
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
@ -206,6 +208,16 @@ class SettingService
|
||||
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.
|
||||
*/
|
||||
|
82
app/Settings/UserShortcutMap.php
Normal file
82
app/Settings/UserShortcutMap.php
Normal file
@ -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
resources/icons/shortcuts.svg
Normal file
1
resources/icons/shortcuts.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 367 B |
@ -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
|
||||
});
|
||||
|
@ -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"
|
||||
@ -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,
|
||||
|
57
resources/js/components/shortcut-input.js
Normal file
57
resources/js/components/shortcut-input.js
Normal file
@ -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;
|
164
resources/js/components/shortcuts.js
Normal file
164
resources/js/components/shortcuts.js
Normal file
@ -0,0 +1,164 @@
|
||||
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;
|
@ -25,6 +25,7 @@ return [
|
||||
'actions' => 'Actions',
|
||||
'view' => 'View',
|
||||
'view_all' => 'View All',
|
||||
'new' => 'New',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'edit' => 'Edit',
|
||||
@ -80,12 +81,14 @@ return [
|
||||
'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',
|
||||
|
18
resources/lang/en/preferences.php
Normal file
18
resources/lang/en/preferences.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?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!',
|
||||
];
|
@ -982,4 +982,32 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
}
|
||||
.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;
|
||||
}
|
@ -473,4 +473,10 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
|
||||
.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;
|
||||
}
|
@ -37,7 +37,7 @@
|
||||
<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>
|
||||
|
@ -94,13 +94,13 @@
|
||||
<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>
|
||||
@ -109,29 +109,29 @@
|
||||
<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>
|
||||
|
@ -108,7 +108,7 @@
|
||||
<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>
|
||||
@ -117,31 +117,31 @@
|
||||
<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>
|
||||
@ -149,7 +149,7 @@
|
||||
|
||||
@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>
|
||||
|
@ -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() }}
|
||||
{{ method_field('patch') }}
|
||||
@if(setting()->getForCurrentUser('dark-mode-enabled'))
|
||||
|
@ -2,7 +2,7 @@
|
||||
<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
|
||||
@ -22,6 +22,7 @@
|
||||
<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>
|
||||
@ -33,14 +34,14 @@
|
||||
@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
|
||||
|
||||
@ -61,13 +62,13 @@
|
||||
</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>
|
||||
@ -82,13 +83,19 @@
|
||||
<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>
|
||||
|
@ -9,7 +9,7 @@
|
||||
action="{{ url()->current() }}"
|
||||
method="get"
|
||||
@else
|
||||
action="{{ url("/settings/users/". user()->id ."/change-sort/{$type}") }}"
|
||||
action="{{ url("/preferences/change-sort/{$type}") }}"
|
||||
method="post"
|
||||
@endif
|
||||
>
|
||||
|
@ -2,8 +2,13 @@
|
||||
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>
|
||||
|
@ -5,7 +5,7 @@
|
||||
{{ 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>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<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">
|
||||
@ -14,7 +14,7 @@
|
||||
</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">
|
||||
|
@ -1,15 +1,15 @@
|
||||
<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>
|
||||
|
@ -4,7 +4,7 @@ $key - Unique key for checking existing stored state.
|
||||
--}}
|
||||
<?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>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<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>
|
||||
|
@ -31,7 +31,12 @@
|
||||
<!-- 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')
|
||||
|
@ -145,37 +145,37 @@
|
||||
|
||||
{{--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>
|
||||
|
@ -10,13 +10,13 @@
|
||||
<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>
|
||||
|
@ -112,32 +112,32 @@
|
||||
<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>
|
||||
|
@ -0,0 +1,12 @@
|
||||
<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>
|
75
resources/views/users/preferences/shortcuts.blade.php
Normal file
75
resources/views/users/preferences/shortcuts.blade.php
Normal file
@ -0,0 +1,75 @@
|
||||
@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
|
@ -246,13 +246,14 @@ Route::middleware('auth')->group(function () {
|
||||
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']);
|
||||
|
@ -225,18 +225,18 @@ class BookTest extends TestCase
|
||||
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'));
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
$guest = User::getDefault();
|
||||
|
@ -6,12 +6,51 @@ use Tests\TestCase;
|
||||
|
||||
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',
|
||||
]);
|
||||
@ -34,7 +73,7 @@ class UserPreferencesTest extends TestCase
|
||||
$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',
|
||||
]);
|
||||
@ -49,7 +88,7 @@ class UserPreferencesTest extends TestCase
|
||||
$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', [
|
||||
@ -58,7 +97,7 @@ class UserPreferencesTest extends TestCase
|
||||
]);
|
||||
$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);
|
||||
}
|
||||
|
||||
@ -69,7 +108,7 @@ class UserPreferencesTest extends TestCase
|
||||
$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'));
|
||||
|
||||
@ -123,7 +162,7 @@ class UserPreferencesTest extends TestCase
|
||||
->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())
|
||||
@ -140,14 +179,14 @@ class UserPreferencesTest extends TestCase
|
||||
$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);
|
||||
|
Loading…
Reference in New Issue
Block a user