Skip to content

Commit

Permalink
Merge pull request #927 from biigle/scrollable-typeahead
Browse files Browse the repository at this point in the history
Use scrollable typeahead to attach volumes/labeltrees
  • Loading branch information
mzur authored Nov 5, 2024
2 parents d273f0d + 9290905 commit c13ef5c
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 44 deletions.
11 changes: 7 additions & 4 deletions app/Http/Controllers/Api/ProjectLabelTreeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ public function index($id)
}

/**
* Display all label trees that can be used by the specified project.
* Display label trees that match the given name and can be used by the specified project.
*
* @api {get} projects/:id/label-trees/available Get all available label trees
* @api {get} projects/:id/label-trees/available/:name Get matching label trees
* @apiGroup Projects
* @apiName IndexProjectAvailableLabelTrees
* @apiPermission projectMember
* @apiDescription This endpoint lists all label trees that _can be_ used by the project (do not confuse this with the "used label trees" endpoint).
* @apiDescription This endpoint lists all matching label trees that _can be_ used by the project (do not confuse this with the "used label trees" endpoint).
*
* @apiParam {Number} id The project ID.
*
Expand All @@ -86,19 +86,22 @@ public function index($id)
* ]
*
* @param int $id Project ID
* @param string $name Labeltree name
* @return array<int, LabelTree>
*/
public function available($id)
public function available($id, $name)
{
$project = Project::findOrFail($id);
$this->authorize('access', $project);

$public = LabelTree::publicTrees()
->select('id', 'name', 'description', 'version_id')
->where('name', 'ilike', "%{$name}%")
->with('version')
->get();
$authorized = $project->authorizedLabelTrees()
->select('id', 'name', 'description', 'version_id')
->where('name', 'ilike', "%{$name}%")
->with('version')
->get();

Expand Down
10 changes: 6 additions & 4 deletions app/Http/Controllers/Api/ProjectsAttachableVolumesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
class ProjectsAttachableVolumesController extends Controller
{
/**
* Shows all volumes that can be attached to the project by the requesting user.
* Shows volumes that match the given volume name and can be attached to the project by the requesting user.
*
* @api {get} projects/:id/attachable-volumes Get attachable volumes
* @api {get} projects/:id/attachable-volumes/:name Get matching attachable volumes
* @apiGroup Projects
* @apiName IndexAttachableVolumes
* @apiPermission projectAdmin
* @apiParam {Number} id ID of the project for which the volumes should be fetched.
* @apiDescription A list of all volumes where the requesting user has admin rights for (excluding those already belonging to the specified project).
* @apiDescription A list of all matching volumes where the requesting user has admin rights for (excluding those already belonging to the specified project).
*
* @apiSuccessExample {json} Success response:
* [
Expand All @@ -34,10 +34,11 @@ class ProjectsAttachableVolumesController extends Controller
*
* @param Request $request
* @param int $id Project ID
* @param string $name Volume name
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public function index(Request $request, $id)
public function index(Request $request, $id, $name)
{
$project = Project::findOrFail($id);
$this->authorize('update', $project);
Expand All @@ -56,6 +57,7 @@ public function index(Request $request, $id)
->where('project_id', '!=', $id);
});
})
->where('name', 'ilike', "%{$name}%")
// Do not return volumes that are already attached to this project.
// This is needed although we are already excluding the project in the
// previous statement because other projects may already share volumes with
Expand Down
2 changes: 1 addition & 1 deletion resources/assets/js/core/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default Vue.resource('api/v1/projects{/id}', {}, {
},
queryAvailableLabelTrees: {
method: 'GET',
url: 'api/v1/projects{/id}/label-trees/available',
url: 'api/v1/projects{/id}/label-trees/available{/name}',
},
attachLabelTree: {
method: 'POST',
Expand Down
49 changes: 47 additions & 2 deletions resources/assets/js/core/components/typeahead.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
ref="input"
class="form-control"
type="text"
v-model="inputText"
:disabled="disabled"
:placeholder="placeholder"
@focus="emitFocus"
Expand All @@ -15,11 +16,15 @@
:target="inputElement"
:data="items"
:force-select="true"
:limit="limit"
:limit="itemLimit"
:class="{'typeahead-scrollable': scrollable}"
item-key="name"
v-show="showTypeahead"
@selected-item-changed="handleArrowKeyScroll"
>
<template slot="item" slot-scope="props">
<component
ref="dropdown"
:is="itemComponent"
@click.native="emitInternalValue"
v-for="(item, index) in props.items"
Expand All @@ -38,6 +43,7 @@
<script>
import Typeahead from 'uiv/dist/Typeahead';
import TypeaheadItem from './typeaheadItem';
import {debounce} from './../utils';
/**
* A component that displays a typeahead to find items.
Expand Down Expand Up @@ -81,17 +87,33 @@ export default {
type: Object,
default: () => TypeaheadItem,
},
scrollable: {
type: Boolean,
default: false,
},
},
data() {
return {
inputElement: null,
internalValue: undefined,
inputText: '',
isTyping: false,
oldInput: '',
maxItemCount: 50
};
},
computed: {
itemLimit() {
return this.scrollable ? this.maxItemCount : this.limit
},
showTypeahead() {
return !this.scollable || this.scrollable && !this.isTyping;
}
},
methods: {
clear() {
this.internalValue = undefined;
this.$refs.input.value = '';
this.inputText = '';
},
emitFocus(e) {
this.$emit('focus', e);
Expand All @@ -108,11 +130,34 @@ export default {
}
}
},
handleArrowKeyScroll(index) {
if (this.scrollable && this.$refs.dropdown[index]) {
this.$refs.dropdown[index].$el.scrollIntoView({block: 'nearest'});
}
},
},
watch: {
value(value) {
this.internalValue = value;
},
inputText(v) {
this.isTyping = true;
debounce(() => {
let added = v.trim().includes(this.oldInput.trim());
let useTypeaheadFilter = this.oldInput.length > 3 && added;
if (v.length >= 3 && !useTypeaheadFilter) {
this.$emit('fetch', v);
}
this.isTyping = false;
this.oldInput = v
}, 500, 'typeahead-fetch');
},
disabled() {
// Use disabled and nextTick to show dropdown right after loading finished
if (!this.disabled) {
this.$nextTick(() => this.$refs.input.focus())
}
}
},
created() {
this.internalValue = this.value;
Expand Down
6 changes: 3 additions & 3 deletions resources/assets/js/core/components/typeaheadItem.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<li>
<a href="#" @click.prevent="props.select(item)">
<span v-html="props.highlight(item)"></span>
<span class="typeahead-item-name" v-html="props.highlight(item)" :title="item.name"></span>
<span v-if="info">
<br><small class="typeahead-item-info" v-text="info" :title="info"></small>
</span>
Expand Down Expand Up @@ -36,7 +36,7 @@ export default {
return keys.reduce(function (i, key) {
return i ? i[key] : i;
}, this.item);
}
},
},
}
};
</script>
2 changes: 1 addition & 1 deletion resources/assets/js/projects/api/attachableVolumes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
* var resource = biigle.$require('api.attachableVolumes');
* resource.get({id: projectId}).then(...);
*/
export default Vue.resource('api/v1/projects{/id}/attachable-volumes');
export default Vue.resource('api/v1/projects{/id}/attachable-volumes{/name}');
14 changes: 9 additions & 5 deletions resources/assets/js/projects/labelTreesContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Events from '../core/events';
import LabelTreeList from './components/labelTreeList';
import LoaderMixin from '../core/mixins/loader';
import ProjectsApi from '../core/api/projects';
import Typeahead from '../core/components/typeahead';
import {handleErrorResponse} from '../core/messages/store';
import Typeahead from '../core/components/typeahead';
export default {
mixins: [LoaderMixin],
Expand All @@ -19,6 +19,7 @@ export default {
labelTrees: [],
fetchedAvailableLabelTrees: false,
availableLabelTrees: [],
oldTreeName: "",
};
},
computed: {
Expand All @@ -35,13 +36,16 @@ export default {
},
},
methods: {
fetchAvailableLabelTrees() {
if (!this.fetchedAvailableLabelTrees) {
fetchAvailableLabelTrees(treeName) {
if (this.oldTreeName.trim() != treeName.trim()) {
this.fetchedAvailableLabelTrees = true;
this.startLoading();
ProjectsApi.queryAvailableLabelTrees({id: this.project.id})
ProjectsApi.queryAvailableLabelTrees({ id: this.project.id, name: treeName })
.then(this.availableLabelTreesFetched, handleErrorResponse)
.finally(this.finishLoading);
.finally(() => {
this.finishLoading();
this.oldTreeName = treeName;
});
}
},
availableLabelTreesFetched(response) {
Expand Down
20 changes: 12 additions & 8 deletions resources/assets/js/projects/volumesContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import LoaderMixin from '../core/mixins/loader';
import PreviewThumbnail from './components/previewThumbnail';
import statisticsModal from './components/statisticsModal';
import ProjectsApi from '../core/api/projects';
import Typeahead from '../core/components/typeahead';
import Typeahead from '../core/components/typeahead.vue';
import {handleErrorResponse} from '../core/messages/store';
Expand Down Expand Up @@ -37,13 +37,14 @@ export default {
currentSorting: SORTING.DATE_DOWN,
showModal: false,
statisticsData: {},
volumeUrlTemplate: ""
volumeUrlTemplate: "",
oldVolumeName: "",
};
},
components: {
previewThumbnail: PreviewThumbnail,
typeahead: Typeahead,
statisticsModal: statisticsModal
statisticsModal: statisticsModal,
typeahead: Typeahead
},
computed: {
sortedVolumes() {
Expand Down Expand Up @@ -195,12 +196,15 @@ export default {
}
}
},
fetchAttachableVolumes() {
if (!this.fetchedAttachableVolumes) {
fetchAttachableVolumes(volumeName) {
if (this.oldVolumeName.trim() != volumeName.trim()) {
this.fetchedAttachableVolumes = true;
this.startLoading();
AttachableVolumesApi.get({id: this.project.id})
.then(this.attachableVolumesFetched, handleErrorResponse)
AttachableVolumesApi.get({ id: this.project.id, name: volumeName })
.then((res) => {
this.attachableVolumesFetched(res);
this.oldVolumeName = volumeName;
}, handleErrorResponse)
.finally(this.finishLoading);
}
},
Expand Down
7 changes: 6 additions & 1 deletion resources/assets/sass/components/_typeahead.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
display: block;
}

.typeahead-item-info {
.typeahead-item-info, .typeahead-item-name {
display: inline-block;
max-width: 250px;
text-overflow: ellipsis;
overflow: hidden;
}

.typeahead-scrollable .dropdown-menu {
max-height: 200px;
overflow-y: auto;
}
2 changes: 1 addition & 1 deletion resources/views/projects/show/label-trees.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
@can('update', $project)
<loader :active="loading"></loader>
<a href="{{route('label-trees-create', ['project' => $project->id])}}" class="btn btn-default" title="Create a new label tree and attach it to this project" >Create Label Tree</a>
<typeahead :items="attachableLabelTrees" placeholder="Attach label tree" :disabled="loading" v-on:select="attachTree" :clear-on-select="true" more-info="description" title="Attach a label tree" v-on:focus="fetchAvailableLabelTrees"></typeahead>
<typeahead :scrollable="true" :items="attachableLabelTrees" :disabled="loading" placeholder="Attach label tree" v-on:select="attachTree" :clear-on-select="true" more-info="description" title="Attach a label tree" v-on:fetch="fetchAvailableLabelTrees"></typeahead>
@else
<span class="text-muted">Project admins can add and remove label trees.</span>
@endcan
Expand Down
2 changes: 1 addition & 1 deletion resources/views/projects/show/volumes.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<span class="pull-right">
<loader :active="loading"></loader>
<a href="{{ route('create-volume') }}?project={{ $project->id }}" class="btn btn-default" title="Create a new volume for this project">Create Volume</a>
<typeahead :items="attachableVolumes" placeholder="Attach volumes" v-on:select="attachVolume" :clear-on-select="true" title="Attach existing volumes of other projects" v-on:focus="fetchAttachableVolumes"></typeahead>
<typeahead :scrollable="true" :disabled="loading" :items="attachableVolumes" placeholder="Attach volumes" v-on:select="attachVolume" :clear-on-select="true" title="Attach existing volumes of other projects" v-on:fetch="fetchAttachableVolumes"></typeahead>
</span>
@endcan
</div>
Expand Down
4 changes: 2 additions & 2 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,15 @@
'parameters' => ['projects' => 'id'],
]);

$router->get('projects/{id}/attachable-volumes', 'ProjectsAttachableVolumesController@index');
$router->get('projects/{id}/attachable-volumes/{name}', 'ProjectsAttachableVolumesController@index');

$router->resource('projects.invitations', 'ProjectInvitationController', [
'only' => ['store'],
'parameters' => ['projects' => 'id'],
]);

$router->get(
'projects/{id}/label-trees/available',
'projects/{id}/label-trees/available/{name}',
'ProjectLabelTreeController@available'
);
$router->resource('projects.label-trees', 'ProjectLabelTreeController', [
Expand Down
Loading

0 comments on commit c13ef5c

Please sign in to comment.