Added user-select input

This commit is contained in:
Dan Brown 2020-12-31 17:25:20 +00:00
parent 33e35c9a8a
commit 8833b5bc3b
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
15 changed files with 271 additions and 128 deletions

View File

@ -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'));
}
}

View File

@ -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;

View File

@ -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;

View File

@ -17,6 +17,7 @@ class DropDown {
this.body = document.body;
this.showing = false;
this.setupListeners();
this.hide = this.hide.bind(this);
}
show(event = null) {

View File

@ -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,
};

View File

@ -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;

View File

@ -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',

View File

@ -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;
}

View File

@ -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;

View File

@ -153,6 +153,9 @@ body.flexbox {
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
/**

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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');