mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-03-31 12:15:12 +08:00
Added user-select input
This commit is contained in:
parent
33e35c9a8a
commit
8833b5bc3b
31
app/Http/Controllers/UserSearchController.php
Normal file
31
app/Http/Controllers/UserSearchController.php
Normal 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'));
|
||||
}
|
||||
}
|
@ -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;
|
79
resources/js/components/dropdown-search.js
Normal file
79
resources/js/components/dropdown-search.js
Normal 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;
|
@ -17,6 +17,7 @@ class DropDown {
|
||||
this.body = document.body;
|
||||
this.showing = false;
|
||||
this.setupListeners();
|
||||
this.hide = this.hide.bind(this);
|
||||
}
|
||||
|
||||
show(event = null) {
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
24
resources/js/components/user-select.js
Normal file
24
resources/js/components/user-select.js
Normal 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;
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -153,6 +153,9 @@ body.flexbox {
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
6
resources/views/components/user-select-list.blade.php
Normal file
6
resources/views/components/user-select-list.blade.php
Normal 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
|
30
resources/views/components/user-select.blade.php
Normal file
30
resources/views/components/user-select.blade.php
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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');
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user