Merge pull request #3593 from BookStackApp/code-editor-favorites

Code-editor lang favorites system
This commit is contained in:
Dan Brown 2022-07-25 19:16:11 +01:00 committed by GitHub
commit 050ae01f94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 161 additions and 47 deletions

View File

@ -289,6 +289,27 @@ class UserController extends Controller
return response('', 204); return response('', 204);
} }
public function updateCodeLanguageFavourite(Request $request)
{
$validated = $this->validate($request, [
'language' => ['required', 'string', 'max:20'],
'active' => ['required', 'bool'],
]);
$currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
$currentFavorites = array_filter(explode(',', $currentFavoritesStr));
$isFav = in_array($validated['language'], $currentFavorites);
if (!$isFav && $validated['active']) {
$currentFavorites[] = $validated['language'];
} else if ($isFav && !$validated['active']) {
$index = array_search($validated['language'], $currentFavorites);
array_splice($currentFavorites, $index, 1);
}
setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
}
/** /**
* Changed the stored preference for a list sort order. * Changed the stored preference for a list sort order.
*/ */

View File

@ -39,6 +39,7 @@ import 'codemirror/addon/scroll/scrollpastend';
// Value can be a mode string or a function that will receive the code content & return the mode string. // Value can be a mode string or a function that will receive the code content & return the mode string.
// The function option is used in the event the exact mode could be dynamic depending on the code. // The function option is used in the event the exact mode could be dynamic depending on the code.
const modeMap = { const modeMap = {
bash: 'shell',
css: 'css', css: 'css',
c: 'text/x-csrc', c: 'text/x-csrc',
java: 'text/x-java', java: 'text/x-java',
@ -88,7 +89,6 @@ const modeMap = {
shell: 'shell', shell: 'shell',
sh: 'shell', sh: 'shell',
stext: 'text/x-stex', stext: 'text/x-stex',
bash: 'shell',
toml: 'toml', toml: 'toml',
ts: 'text/typescript', ts: 'text/typescript',
typescript: 'text/typescript', typescript: 'text/typescript',

View File

@ -10,17 +10,20 @@ class CodeEditor {
this.container = this.$refs.container; this.container = this.$refs.container;
this.popup = this.$el; this.popup = this.$el;
this.editorInput = this.$refs.editor; this.editorInput = this.$refs.editor;
this.languageLinks = this.$manyRefs.languageLink; this.languageButtons = this.$manyRefs.languageButton;
this.languageOptionsContainer = this.$refs.languageOptionsContainer;
this.saveButton = this.$refs.saveButton; this.saveButton = this.$refs.saveButton;
this.languageInput = this.$refs.languageInput; this.languageInput = this.$refs.languageInput;
this.historyDropDown = this.$refs.historyDropDown; this.historyDropDown = this.$refs.historyDropDown;
this.historyList = this.$refs.historyList; this.historyList = this.$refs.historyList;
this.favourites = new Set(this.$opts.favourites.split(','));
this.callback = null; this.callback = null;
this.editor = null; this.editor = null;
this.history = {}; this.history = {};
this.historyKey = 'code_history'; this.historyKey = 'code_history';
this.setupListeners(); this.setupListeners();
this.setupFavourites();
} }
setupListeners() { setupListeners() {
@ -30,7 +33,7 @@ class CodeEditor {
} }
}); });
onSelect(this.languageLinks, event => { onSelect(this.languageButtons, event => {
const language = event.target.dataset.lang; const language = event.target.dataset.lang;
this.languageInput.value = language; this.languageInput.value = language;
this.languageInputChange(language); this.languageInputChange(language);
@ -49,6 +52,58 @@ class CodeEditor {
}); });
} }
setupFavourites() {
for (const button of this.languageButtons) {
this.setupFavouritesForButton(button);
}
this.sortLanguageList();
}
/**
* @param {HTMLButtonElement} button
*/
setupFavouritesForButton(button) {
const language = button.dataset.lang;
let isFavorite = this.favourites.has(language);
button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
onChildEvent(button.parentElement, '.lang-option-favorite-toggle', 'click', () => {
isFavorite = !isFavorite;
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', {
language: language,
active: isFavorite
});
this.sortLanguageList();
if (isFavorite) {
button.scrollIntoView({block: "center", behavior: "smooth"});
}
});
}
sortLanguageList() {
const sortedParents = this.languageButtons.sort((a, b) => {
const aFav = a.dataset.favourite === 'true';
const bFav = b.dataset.favourite === 'true';
if (aFav && !bFav) {
return -1;
} else if (bFav && !aFav) {
return 1;
}
return a.dataset.lang > b.dataset.lang ? 1 : -1;
}).map(button => button.parentElement);
for (const parent of sortedParents) {
this.languageOptionsContainer.append(parent);
}
}
save() { save() {
if (this.callback) { if (this.callback) {
this.callback(this.editor.getValue(), this.languageInput.value); this.callback(this.editor.getValue(), this.languageInput.value);
@ -94,15 +149,13 @@ class CodeEditor {
languageInputChange(language) { languageInputChange(language) {
this.updateEditorMode(language); this.updateEditorMode(language);
const inputLang = language.toLowerCase(); const inputLang = language.toLowerCase();
let matched = false;
for (const link of this.languageLinks) { for (const link of this.languageButtons) {
const lang = link.dataset.lang.toLowerCase().trim(); const lang = link.dataset.lang.toLowerCase().trim();
const isMatch = inputLang && lang.startsWith(inputLang); const isMatch = inputLang === lang;
link.classList.toggle('active', isMatch); link.classList.toggle('active', isMatch);
if (isMatch && !matched) { if (isMatch) {
link.scrollIntoView({block: "center", behavior: "smooth"}); link.scrollIntoView({block: "center", behavior: "smooth"});
matched = true;
} }
} }
} }

View File

@ -666,17 +666,48 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
text-align: left; text-align: left;
font-family: $mono; font-family: $mono;
font-size: 0.7rem; font-size: 0.7rem;
padding-left: 24px + $-xs;
&:hover, &.active { &:hover, &.active {
background-color: var(--color-primary-light); background-color: var(--color-primary-light);
color: var(--color-primary); color: var(--color-primary);
} }
} }
.code-editor button.lang-option-favorite-toggle {
position: absolute;
top: 0;
left: 0;
width: 28px;
font-size: 1rem;
border: 0;
line-height: 1;
padding: 2px;
z-index: 2;
height: 100%;
text-align: center;
color: var(--color-primary);
svg {
margin: 0;
}
}
.code-editor button[data-favourite="true"] ~ .action-favourite,
.code-editor button[data-favourite="false"] ~ .action-unfavourite {
display: none;
}
.code-editor .action-favourite {
opacity: 0.5;
}
.code-editor button:hover ~ .action-favourite {
opacity: 1;
}
.code-editor label { .code-editor label {
background-color: var(--color-primary-light); background-color: var(--color-primary-light);
width: 100%; width: 100%;
color: var(--color-primary); color: var(--color-primary);
padding: $-xxs $-m; padding: $-xxs $-s;
margin-bottom: 0; margin-bottom: 0;
} }
@ -691,7 +722,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
border-radius: 0; border-radius: 0;
border: 0; border: 0;
border-bottom: 1px solid #DDD; border-bottom: 1px solid #DDD;
padding: $-xs $-m; padding: $-xs $-s;
height: auto;
} }
.code-editor-main { .code-editor-main {
@ -705,6 +737,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
} }
} }
.code-editor-body-wrap {
height: 80vh;
}
@include smaller-than($s) { @include smaller-than($s) {
.code-editor .lang-options { .code-editor .lang-options {
display: none; display: none;

View File

@ -1,5 +1,7 @@
<div> <div>
<div components="popup code-editor" class="popup-background code-editor"> <div components="popup code-editor"
option:code-editor:favourites="{{ setting()->getForCurrentUser('code-language-favourites', '') }}"
class="popup-background code-editor">
<div refs="code-editor@container" class="popup-body" tabindex="-1"> <div refs="code-editor@container" class="popup-body" tabindex="-1">
<div class="popup-header flex-container-row primary-background"> <div class="popup-header flex-container-row primary-background">
@ -18,42 +20,23 @@
<div class="code-editor-language-list flex-container-column flex-fill"> <div class="code-editor-language-list flex-container-column flex-fill">
<label for="code-editor-language">{{ trans('components.code_language') }}</label> <label for="code-editor-language">{{ trans('components.code_language') }}</label>
<input refs="code-editor@languageInput" id="code-editor-language" type="text"> <input refs="code-editor@languageInput" id="code-editor-language" type="text">
<div class="lang-options"> <div refs="code-editor@language-options-container" class="lang-options">
<button type="button" refs="code-editor@languageLink" data-lang="CSS">CSS</button> @php
<button type="button" refs="code-editor@languageLink" data-lang="C">C</button> $languages = [
<button type="button" refs="code-editor@languageLink" data-lang="C++">C++</button> 'Bash', 'CSS', 'C', 'C++', 'C#', 'Diff', 'Fortran', 'F#', 'Go', 'Haskell', 'HTML', 'INI',
<button type="button" refs="code-editor@languageLink" data-lang="C#">C#</button> 'Java', 'JavaScript', 'JSON', 'Julia', 'Kotlin', 'LaTeX', 'Lua', 'MarkDown', 'Nginx', 'OCaml',
<button type="button" refs="code-editor@languageLink" data-lang="diff">Diff</button> 'Pascal', 'Perl', 'PHP', 'Powershell', 'Python', 'Ruby', 'Rust', 'Shell', 'SQL', 'TypeScript',
<button type="button" refs="code-editor@languageLink" data-lang="Fortran">Fortran</button> 'VBScript', 'VB.NET', 'XML', 'YAML',
<button type="button" refs="code-editor@languageLink" data-lang="F#">F#</button> ];
<button type="button" refs="code-editor@languageLink" data-lang="Go">Go</button> @endphp
<button type="button" refs="code-editor@languageLink" data-lang="Haskell">Haskell</button>
<button type="button" refs="code-editor@languageLink" data-lang="HTML">HTML</button> @foreach($languages as $language)
<button type="button" refs="code-editor@languageLink" data-lang="INI">INI</button> <div class="relative">
<button type="button" refs="code-editor@languageLink" data-lang="Java">Java</button> <button type="button" refs="code-editor@language-button" data-favourite="false" data-lang="{{ strtolower($language) }}">{{ $language }}</button>
<button type="button" refs="code-editor@languageLink" data-lang="JavaScript">JavaScript</button> <button class="lang-option-favorite-toggle action-favourite" data-title="{{ trans('common.favourite') }}">@icon('star-outline')</button>
<button type="button" refs="code-editor@languageLink" data-lang="JSON">JSON</button> <button class="lang-option-favorite-toggle action-unfavourite" data-title="{{ trans('common.unfavourite') }}">@icon('star')</button>
<button type="button" refs="code-editor@languageLink" data-lang="Julia">Julia</button> </div>
<button type="button" refs="code-editor@languageLink" data-lang="kotlin">Kotlin</button> @endforeach
<button type="button" refs="code-editor@languageLink" data-lang="LaTeX">LaTeX</button>
<button type="button" refs="code-editor@languageLink" data-lang="Lua">Lua</button>
<button type="button" refs="code-editor@languageLink" data-lang="MarkDown">MarkDown</button>
<button type="button" refs="code-editor@languageLink" data-lang="Nginx">Nginx</button>
<button type="button" refs="code-editor@languageLink" data-lang="ocaml">OCaml</button>
<button type="button" refs="code-editor@languageLink" data-lang="PASCAL">Pascal</button>
<button type="button" refs="code-editor@languageLink" data-lang="Perl">Perl</button>
<button type="button" refs="code-editor@languageLink" data-lang="PHP">PHP</button>
<button type="button" refs="code-editor@languageLink" data-lang="Powershell">Powershell</button>
<button type="button" refs="code-editor@languageLink" data-lang="Python">Python</button>
<button type="button" refs="code-editor@languageLink" data-lang="Ruby">Ruby</button>
<button type="button" refs="code-editor@languageLink" data-lang="rust">Rust</button>
<button type="button" refs="code-editor@languageLink" data-lang="shell">Shell/Bash</button>
<button type="button" refs="code-editor@languageLink" data-lang="SQL">SQL</button>
<button type="button" refs="code-editor@languageLink" data-lang="typescript">TypeScript</button>
<button type="button" refs="code-editor@languageLink" data-lang="VBScript">VBScript</button>
<button type="button" refs="code-editor@languageLink" data-lang="VB.NET">VB.NET</button>
<button type="button" refs="code-editor@languageLink" data-lang="XML">XML</button>
<button type="button" refs="code-editor@languageLink" data-lang="YAML">YAML</button>
</div> </div>
</div> </div>

View File

@ -235,6 +235,7 @@ Route::middleware('auth')->group(function () {
Route::patch('/settings/users/{id}/change-sort/{type}', [UserController::class, 'changeSort']); Route::patch('/settings/users/{id}/change-sort/{type}', [UserController::class, 'changeSort']);
Route::patch('/settings/users/{id}/update-expansion-preference/{key}', [UserController::class, 'updateExpansionPreference']); Route::patch('/settings/users/{id}/update-expansion-preference/{key}', [UserController::class, 'updateExpansionPreference']);
Route::patch('/settings/users/toggle-dark-mode', [UserController::class, 'toggleDarkMode']); Route::patch('/settings/users/toggle-dark-mode', [UserController::class, 'toggleDarkMode']);
Route::patch('/settings/users/update-code-language-favourite', [UserController::class, 'updateCodeLanguageFavourite']);
Route::post('/settings/users/create', [UserController::class, 'store']); Route::post('/settings/users/create', [UserController::class, 'store']);
Route::get('/settings/users/{id}', [UserController::class, 'edit']); Route::get('/settings/users/{id}', [UserController::class, 'edit']);
Route::put('/settings/users/{id}', [UserController::class, 'update']); Route::put('/settings/users/{id}', [UserController::class, 'update']);

View File

@ -3,6 +3,7 @@
namespace Tests\User; namespace Tests\User;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Page;
use Tests\TestCase; use Tests\TestCase;
class UserPreferencesTest extends TestCase class UserPreferencesTest extends TestCase
@ -150,4 +151,23 @@ class UserPreferencesTest extends TestCase
->assertElementExists('.featured-image-container') ->assertElementExists('.featured-image-container')
->assertElementNotExists('.content-wrap .entity-list-item'); ->assertElementNotExists('.content-wrap .entity-list-item');
} }
public function test_update_code_language_favourite()
{
$editor = $this->getEditor();
$page = Page::query()->first();
$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]);
$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]);
$resp = $this->get($page->getUrl('/edit'));
$resp->assertSee('option:code-editor:favourites="javascript,ruby"', false);
}
} }