From 8833b5bc3bc717c0303fb1a61a15c97f10b283ae Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Thu, 31 Dec 2020 17:25:20 +0000 Subject: [PATCH] Added user-select input --- app/Http/Controllers/UserSearchController.php | 31 ++++++++ resources/js/components/breadcrumb-listing.js | 55 ------------- resources/js/components/dropdown-search.js | 79 +++++++++++++++++++ resources/js/components/dropdown.js | 1 + resources/js/components/index.js | 6 +- resources/js/components/user-select.js | 24 ++++++ resources/lang/en/entities.php | 1 + resources/sass/_components.scss | 61 ++++++++++++++ resources/sass/_header.scss | 52 +----------- resources/sass/_layout.scss | 3 + .../components/user-select-list.blade.php | 6 ++ .../views/components/user-select.blade.php | 30 +++++++ .../views/form/entity-permissions.blade.php | 26 +++--- .../partials/breadcrumb-listing.blade.php | 21 +++-- routes/web.php | 3 + 15 files changed, 271 insertions(+), 128 deletions(-) create mode 100644 app/Http/Controllers/UserSearchController.php delete mode 100644 resources/js/components/breadcrumb-listing.js create mode 100644 resources/js/components/dropdown-search.js create mode 100644 resources/js/components/user-select.js create mode 100644 resources/views/components/user-select-list.blade.php create mode 100644 resources/views/components/user-select.blade.php diff --git a/app/Http/Controllers/UserSearchController.php b/app/Http/Controllers/UserSearchController.php new file mode 100644 index 000000000..1ff056cd2 --- /dev/null +++ b/app/Http/Controllers/UserSearchController.php @@ -0,0 +1,31 @@ +<?php + +namespace BookStack\Http\Controllers; + +use BookStack\Auth\User; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\Request; + +class UserSearchController extends Controller +{ + /** + * Search users in the system, with the response formatted + * for use in a select-style list. + */ + public function forSelect(Request $request) + { + $search = $request->get('search', ''); + $query = User::query()->orderBy('name', 'desc') + ->take(20); + + if (!empty($search)) { + $query->where(function(Builder $query) use ($search) { + $query->where('email', 'like', '%' . $search . '%') + ->orWhere('name', 'like', '%' . $search . '%'); + }); + } + + $users = $query->get(); + return view('form.user-select-list', compact('users')); + } +} diff --git a/resources/js/components/breadcrumb-listing.js b/resources/js/components/breadcrumb-listing.js deleted file mode 100644 index 3ce4bc77e..000000000 --- a/resources/js/components/breadcrumb-listing.js +++ /dev/null @@ -1,55 +0,0 @@ - -class BreadcrumbListing { - - setup() { - this.elem = this.$el; - this.searchInput = this.$refs.searchInput; - this.loadingElem = this.$refs.loading; - this.entityListElem = this.$refs.entityList; - - this.entityType = this.$opts.entityType; - this.entityId = Number(this.$opts.entityId); - - this.elem.addEventListener('show', this.onShow.bind(this)); - this.searchInput.addEventListener('input', this.onSearch.bind(this)); - } - - onShow() { - this.loadEntityView(); - } - - onSearch() { - const input = this.searchInput.value.toLowerCase().trim(); - const listItems = this.entityListElem.querySelectorAll('.entity-list-item'); - for (let listItem of listItems) { - const match = !input || listItem.textContent.toLowerCase().includes(input); - listItem.style.display = match ? 'flex' : 'none'; - listItem.classList.toggle('hidden', !match); - } - } - - loadEntityView() { - this.toggleLoading(true); - - const params = { - 'entity_id': this.entityId, - 'entity_type': this.entityType, - }; - - window.$http.get('/search/entity/siblings', params).then(resp => { - this.entityListElem.innerHTML = resp.data; - }).catch(err => { - console.error(err); - }).then(() => { - this.toggleLoading(false); - this.onSearch(); - }); - } - - toggleLoading(show = false) { - this.loadingElem.style.display = show ? 'block' : 'none'; - } - -} - -export default BreadcrumbListing; \ No newline at end of file diff --git a/resources/js/components/dropdown-search.js b/resources/js/components/dropdown-search.js new file mode 100644 index 000000000..8c81aae3c --- /dev/null +++ b/resources/js/components/dropdown-search.js @@ -0,0 +1,79 @@ +import {debounce} from "../services/util"; + +class DropdownSearch { + + setup() { + this.elem = this.$el; + this.searchInput = this.$refs.searchInput; + this.loadingElem = this.$refs.loading; + this.listContainerElem = this.$refs.listContainer; + + this.localSearchSelector = this.$opts.localSearchSelector; + this.url = this.$opts.url; + + this.elem.addEventListener('show', this.onShow.bind(this)); + this.searchInput.addEventListener('input', this.onSearch.bind(this)); + + this.runAjaxSearch = debounce(this.runAjaxSearch, 300, false); + } + + onShow() { + this.loadList(); + } + + onSearch() { + const input = this.searchInput.value.toLowerCase().trim(); + if (this.localSearchSelector) { + this.runLocalSearch(input); + } else { + this.toggleLoading(true); + this.runAjaxSearch(input); + } + } + + runAjaxSearch(searchTerm) { + this.loadList(searchTerm); + } + + runLocalSearch(searchTerm) { + const listItems = this.listContainerElem.querySelectorAll(this.localSearchSelector); + for (let listItem of listItems) { + const match = !searchTerm || listItem.textContent.toLowerCase().includes(searchTerm); + listItem.style.display = match ? 'flex' : 'none'; + listItem.classList.toggle('hidden', !match); + } + } + + async loadList(searchTerm = '') { + this.listContainerElem.innerHTML = ''; + this.toggleLoading(true); + + try { + const resp = await window.$http.get(this.getAjaxUrl(searchTerm)); + this.listContainerElem.innerHTML = resp.data; + } catch (err) { + console.error(err); + } + + this.toggleLoading(false); + if (this.localSearchSelector) { + this.onSearch(); + } + } + + getAjaxUrl(searchTerm = null) { + if (!searchTerm) { + return this.url; + } + + const joiner = this.url.includes('?') ? '&' : '?'; + return `${this.url}${joiner}search=${encodeURIComponent(searchTerm)}`; + } + + toggleLoading(show = false) { + this.loadingElem.style.display = show ? 'block' : 'none'; + } + +} + +export default DropdownSearch; \ No newline at end of file diff --git a/resources/js/components/dropdown.js b/resources/js/components/dropdown.js index 7b1ce3055..22402d483 100644 --- a/resources/js/components/dropdown.js +++ b/resources/js/components/dropdown.js @@ -17,6 +17,7 @@ class DropDown { this.body = document.body; this.showing = false; this.setupListeners(); + this.hide = this.hide.bind(this); } show(event = null) { diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 87c496c91..91ccdaf3a 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -5,7 +5,6 @@ import attachments from "./attachments.js" import autoSuggest from "./auto-suggest.js" import backToTop from "./back-to-top.js" import bookSort from "./book-sort.js" -import breadcrumbListing from "./breadcrumb-listing.js" import chapterToggle from "./chapter-toggle.js" import codeEditor from "./code-editor.js" import codeHighlighter from "./code-highlighter.js" @@ -13,6 +12,7 @@ import collapsible from "./collapsible.js" import customCheckbox from "./custom-checkbox.js" import detailsHighlighter from "./details-highlighter.js" import dropdown from "./dropdown.js" +import dropdownSearch from "./dropdown-search.js" import dropzone from "./dropzone.js" import editorToolbox from "./editor-toolbox.js" import entityPermissionsEditor from "./entity-permissions-editor.js" @@ -48,6 +48,7 @@ import tagManager from "./tag-manager.js" import templateManager from "./template-manager.js" import toggleSwitch from "./toggle-switch.js" import triLayout from "./tri-layout.js" +import userSelect from "./user-select.js" import wysiwygEditor from "./wysiwyg-editor.js" const componentMapping = { @@ -58,7 +59,6 @@ const componentMapping = { "auto-suggest": autoSuggest, "back-to-top": backToTop, "book-sort": bookSort, - "breadcrumb-listing": breadcrumbListing, "chapter-toggle": chapterToggle, "code-editor": codeEditor, "code-highlighter": codeHighlighter, @@ -66,6 +66,7 @@ const componentMapping = { "custom-checkbox": customCheckbox, "details-highlighter": detailsHighlighter, "dropdown": dropdown, + "dropdown-search": dropdownSearch, "dropzone": dropzone, "editor-toolbox": editorToolbox, "entity-permissions-editor": entityPermissionsEditor, @@ -101,6 +102,7 @@ const componentMapping = { "template-manager": templateManager, "toggle-switch": toggleSwitch, "tri-layout": triLayout, + "user-select": userSelect, "wysiwyg-editor": wysiwygEditor, }; diff --git a/resources/js/components/user-select.js b/resources/js/components/user-select.js new file mode 100644 index 000000000..477c11d6b --- /dev/null +++ b/resources/js/components/user-select.js @@ -0,0 +1,24 @@ +import {onChildEvent} from "../services/dom"; + +class UserSelect { + + setup() { + + this.input = this.$refs.input; + this.userInfoContainer = this.$refs.userInfo; + + this.hide = this.$el.components.dropdown.hide; + + onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this)); + } + + selectUser(event, userEl) { + const id = userEl.getAttribute('data-id'); + this.input.value = id; + this.userInfoContainer.innerHTML = userEl.innerHTML; + this.hide(); + } + +} + +export default UserSelect; \ No newline at end of file diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 5e6a63deb..6b0153844 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -40,6 +40,7 @@ return [ 'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.', 'permissions_enable' => 'Enable Custom Permissions', 'permissions_save' => 'Save Permissions', + 'permissions_owner' => 'Owner', // Search 'search_results' => 'Search Results', diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index eb40741d1..ede26c51c 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -724,4 +724,65 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { .template-item-actions button:first-child { border-top: 0; } +} + +.dropdown-search-dropdown { + box-shadow: $bs-med; + overflow: hidden; + min-height: 100px; + width: 240px; + display: none; + position: absolute; + z-index: 80; + right: -$-m; + @include rtl { + right: auto; + left: -$-m; + } + .dropdown-search-search .svg-icon { + position: absolute; + left: $-s; + @include rtl { + right: $-s; + left: auto; + } + top: 11px; + fill: #888; + pointer-events: none; + } + .dropdown-search-list { + max-height: 400px; + overflow-y: scroll; + text-align: start; + } + .dropdown-search-item { + padding: $-s $-m; + &:hover,&:focus { + background-color: #F2F2F2; + text-decoration: none; + } + } + input { + padding-inline-start: $-xl; + border-radius: 0; + border: 0; + border-bottom: 1px solid #DDD; + } +} + +@include smaller-than($m) { + .dropdown-search-dropdown { + position: fixed; + right: auto; + left: $-m; + } + .dropdown-search-dropdown .dropdown-search-list { + max-height: 240px; + } +} + +.custom-select-input { + max-width: 280px; + border: 1px solid #DDD; + border-radius: 4px; } \ No newline at end of file diff --git a/resources/sass/_header.scss b/resources/sass/_header.scss index e19bb4f61..246ef4b5b 100644 --- a/resources/sass/_header.scss +++ b/resources/sass/_header.scss @@ -269,9 +269,9 @@ header .search-box { } } -.breadcrumb-listing { +.dropdown-search { position: relative; - .breadcrumb-listing-toggle { + .dropdown-search-toggle { padding: 6px; border: 1px solid transparent; border-radius: 4px; @@ -284,54 +284,6 @@ header .search-box { } } -.breadcrumb-listing-dropdown { - box-shadow: $bs-med; - overflow: hidden; - min-height: 100px; - width: 240px; - display: none; - position: absolute; - z-index: 80; - right: -$-m; - @include rtl { - right: auto; - left: -$-m; - } - .breadcrumb-listing-search .svg-icon { - position: absolute; - left: $-s; - @include rtl { - right: $-s; - left: auto; - } - top: 11px; - fill: #888; - pointer-events: none; - } - .breadcrumb-listing-entity-list { - max-height: 400px; - overflow-y: scroll; - text-align: start; - } - input { - padding-inline-start: $-xl; - border-radius: 0; - border: 0; - border-bottom: 1px solid #DDD; - } -} - -@include smaller-than($m) { - .breadcrumb-listing-dropdown { - position: fixed; - right: auto; - left: $-m; - } - .breadcrumb-listing-dropdown .breadcrumb-listing-entity-list { - max-height: 240px; - } -} - .faded { a, button, span, span > div { color: #666; diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index c4e412f0e..e5ed608eb 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -153,6 +153,9 @@ body.flexbox { .justify-center { justify-content: center; } +.items-center { + align-items: center; +} /** diff --git a/resources/views/components/user-select-list.blade.php b/resources/views/components/user-select-list.blade.php new file mode 100644 index 000000000..2c49e965d --- /dev/null +++ b/resources/views/components/user-select-list.blade.php @@ -0,0 +1,6 @@ +@foreach($users as $user) + <a href="#" class="flex-container-row items-center dropdown-search-item" data-id="{{ $user->id }}"> + <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}"> + <span>{{ $user->name }}</span> + </a> +@endforeach \ No newline at end of file diff --git a/resources/views/components/user-select.blade.php b/resources/views/components/user-select.blade.php new file mode 100644 index 000000000..c6a30f53d --- /dev/null +++ b/resources/views/components/user-select.blade.php @@ -0,0 +1,30 @@ +<div class="dropdown-search custom-select-input" components="dropdown dropdown-search user-select" + option:dropdown-search:url="/search/users/select" +> + <input refs="user-select@input" type="hidden" name="{{ $name }}" value="{{ $user->id }}"> + <div refs="dropdown@toggle" + class="dropdown-search-toggle flex-container-row items-center" + aria-haspopup="true" aria-expanded="false" tabindex="0"> + <div refs="user-select@user-info" class="flex-container-row items-center px-s"> + <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}"> + <span>{{ $user->name }}</span> + </div> + <span style="font-size: 1.5rem; margin-left: auto;"> + @icon('caret-down') + </span> + </div> + <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu"> + <div class="dropdown-search-search"> + @icon('search') + <input refs="dropdown-search@searchInput" + aria-label="{{ trans('common.search') }}" + autocomplete="off" + placeholder="{{ trans('common.search') }}" + type="text"> + </div> + <div refs="dropdown-search@loading" class="text-center"> + @include('partials.loading-icon') + </div> + <div refs="dropdown-search@listContainer" class="dropdown-search-list"></div> + </div> +</div> \ No newline at end of file diff --git a/resources/views/form/entity-permissions.blade.php b/resources/views/form/entity-permissions.blade.php index 16e105e10..35490bed9 100644 --- a/resources/views/form/entity-permissions.blade.php +++ b/resources/views/form/entity-permissions.blade.php @@ -2,20 +2,26 @@ {!! csrf_field() !!} <input type="hidden" name="_method" value="PUT"> - <p class="mb-none">{{ trans('entities.permissions_intro') }}</p> - - <div class="grid half"> - <div class="form-group"> - @include('form.checkbox', [ - 'name' => 'restricted', - 'label' => trans('entities.permissions_enable'), - ]) + <div class="grid half left-focus v-center"> + <div> + <p class="mb-none mt-m">{{ trans('entities.permissions_intro') }}</p> + <div> + @include('form.checkbox', [ + 'name' => 'restricted', + 'label' => trans('entities.permissions_enable'), + ]) + </div> </div> - <div class="form-group"> - <label for="owner">Owner</label> + <div> + <div class="form-group"> + <label for="owner">{{ trans('entities.permissions_owner') }}</label> + @include('components.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by']) + </div> </div> </div> + <hr> + <table permissions-table class="table permissions-table toggle-switch-list" style="{{ !$model->restricted ? 'display: none' : '' }}"> <tr> <th>{{ trans('common.role') }}</th> diff --git a/resources/views/partials/breadcrumb-listing.blade.php b/resources/views/partials/breadcrumb-listing.blade.php index 160fa3c23..2a559aa7d 100644 --- a/resources/views/partials/breadcrumb-listing.blade.php +++ b/resources/views/partials/breadcrumb-listing.blade.php @@ -1,24 +1,23 @@ -<div class="breadcrumb-listing" components="dropdown breadcrumb-listing" - option:breadcrumb-listing:entity-type="{{ $entity->getType() }}" - option:breadcrumb-listing:entity-id="{{ $entity->id }}" - breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}"> - <div class="breadcrumb-listing-toggle" refs="dropdown@toggle" +<div class="dropdown-search" components="dropdown dropdown-search" + option:dropdown-search:url="/search/entity/siblings?entity_type={{$entity->getType()}}&entity_id={{ $entity->id }}" + option:dropdown-search:local-search-selector=".entity-list-item" +> + <div class="dropdown-search-toggle" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" tabindex="0"> <div class="separator">@icon('chevron-right')</div> </div> - <div refs="dropdown@menu" class="breadcrumb-listing-dropdown card" role="menu"> - <div class="breadcrumb-listing-search"> + <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu"> + <div class="dropdown-search-search"> @icon('search') - <input refs="breadcrumb-listing@searchInput" + <input refs="dropdown-search@searchInput" aria-label="{{ trans('common.search') }}" autocomplete="off" - name="entity-search" placeholder="{{ trans('common.search') }}" type="text"> </div> - <div refs="breadcrumb-listing@loading"> + <div refs="dropdown-search@loading"> @include('partials.loading-icon') </div> - <div refs="breadcrumb-listing@entityList" class="breadcrumb-listing-entity-list px-m"></div> + <div refs="dropdown-search@listContainer" class="dropdown-search-list px-m"></div> </div> </div> \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index afefcb99e..e8f217862 100644 --- a/routes/web.php +++ b/routes/web.php @@ -148,6 +148,9 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter'); Route::get('/search/entity/siblings', 'SearchController@searchSiblings'); + // User Search + Route::get('/search/users/select', 'UserSearchController@forSelect'); + Route::get('/templates', 'PageTemplateController@list'); Route::get('/templates/{templateId}', 'PageTemplateController@get');