Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkp/pkp-lib#6675 Added extra features to the Autosuggest field (showP… #171

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions public/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ window.pkp = {
'common.pagination.label': 'View additional pages',
'common.pagination.next': 'Next page',
'common.pagination.previous': 'Previous page',
'common.pagination.loadMore': 'Load more',
'common.pagination.loadMore.description': '{$quantity} results not shown',
jonasraoni marked this conversation as resolved.
Show resolved Hide resolved
'common.remove': 'Remove',
'common.required': 'Required',
'common.save': 'Save',
Expand Down
5 changes: 5 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@
FieldBaseAutosuggest
</router-link>
</li>
<li>
<router-link to="/component/Form/fields/FieldMappedAutosuggest">
FieldMappedAutosuggest
</router-link>
</li>
<li>
<router-link to="/component/Form/fields/FieldHtml">
FieldHtml
Expand Down
2 changes: 2 additions & 0 deletions src/components/Filter/FilterAutosuggest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
<script>
import Filter from './Filter.vue';
import FieldAutosuggestPreset from '@/components/Form/fields/FieldAutosuggestPreset.vue';
import FieldMappedAutosuggest from '@/components/Form/fields/FieldMappedAutosuggest.vue';
import FieldSelectUsers from '@/components/Form/fields/FieldSelectUsers.vue';
import FieldSelectIssues from '@/components/Form/fields/FieldSelectIssues.vue';
export default {
extends: Filter,
components: {
FieldAutosuggestPreset,
FieldMappedAutosuggest,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used anywhere in a FilterAutosuggest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used in the enroll reviewer interface, but as you asked to create a custom component, this will be removed.

FieldSelectUsers,
FieldSelectIssues
},
Expand Down
144 changes: 107 additions & 37 deletions src/components/Form/fields/FieldBaseAutosuggest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,31 @@
class="pkpFormField--autosuggest__autosuggest"
v-bind="autosuggestOptions"
@selected="selectSuggestion"
/>
:style="maxHeight === null || `--maxAutosuggestHeight: ${maxHeight}`"
>
<template slot="after-suggestions" v-if="currentPage < lastPage">
<div
class="pkpFormField--autosuggest__pagination"
@click.prevent.stop=""
@mouseup.prevent.stop=""
@mousedown.prevent.stop=""
jonasraoni marked this conversation as resolved.
Show resolved Hide resolved
>
<pkp-button
:isDisabled="isLoading"
@click="setPage(currentPage + 1)"
:aria-controls="controlId"
>
{{ __('common.pagination.loadMore') }}
</pkp-button>
{{
__('common.pagination.loadMore.description', {
quantity: this.unloadedItems
})
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a spinner when loading more.

</div>
</template>
</vue-autosuggest>
<spinner class="pkpFormField--autosuggest__spinner" v-if="isLoading" />
jonasraoni marked this conversation as resolved.
Show resolved Hide resolved
<div
v-if="currentPosition === 'below'"
class="pkpFormField--autosuggest__values pkpFormField--autosuggest__values--below"
Expand Down Expand Up @@ -109,35 +133,23 @@
import FieldBase from './FieldBase.vue';
import PkpBadge from '@/components/Badge/Badge.vue';
import {VueAutosuggest} from 'vue-autosuggest';
import ajaxError from '@/mixins/ajaxError';
import debounce from 'debounce';
import fetch from '@/mixins/fetch';
import elementResizeEvent from 'element-resize-event';

export default {
name: 'FieldBaseAutosuggest',
extends: FieldBase,
mixins: [ajaxError],
mixins: [fetch],
components: {
PkpBadge,
VueAutosuggest
},
props: {
apiUrl: {
type: String,
default() {
return '';
}
},
deselectLabel: {
type: String,
required: true
},
getParams: {
type: Object,
default() {
return {};
}
},
initialPosition: {
type: String,
default() {
Expand All @@ -156,6 +168,22 @@ export default {
selectedLabel: {
type: String,
required: true
},
minInputLength: {
type: Number,
default: 0
},
maxSelectedItems: {
type: Number,
default: null
},
replaceWhenFull: {
type: Boolean,
default: true
},
maxHeight: {
type: String,
default: '260px'
}
},
data() {
Expand Down Expand Up @@ -257,6 +285,15 @@ export default {
id: this.controlId,
name: this.name
};
},

/**
* Retrieves the quantity of items which were not loaded yet
*
* @return int
*/
unloadedItems() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call this moreSuggestionsCount and update the docblock to read:

Get a count of how many suggestions are not displayed

return Math.max(0, this.itemsMax - this.suggestions.length);
}
},
methods: {
Expand All @@ -275,29 +312,30 @@ export default {
},

/**
* Get suggestions from the API url
* Update the list of items
*
* @param {Array} items
* @param {Number} itemsMax
*/
setItems(items, itemsMax) {
this.setSuggestions(items, itemsMax);
jonasraoni marked this conversation as resolved.
Show resolved Hide resolved
},

/**
* Overwrite the get method from the fetch mixin to avoid requesting a resource if there's no user input
*/
getSuggestions: debounce(function() {
get() {
if (!this.inputValue) {
this.suggestions = [];
return;
}
var self = this;
$.ajax({
url: this.apiUrl,
type: 'GET',
data: {
...this.getParams,
searchPhrase: this.inputValue
},
error(r) {
self.ajaxErrorCallback(r);
},
success(r) {
self.setSuggestions(r.items);
}
});
}, 250),
this.getSuggestions();
},

/**
* Get suggestions from the API url by leveraging the get method of the fetch mixin
*/
getSuggestions: debounce(fetch.methods.get, 250),

/**
* Add a suggested item to the list of selected items
Expand All @@ -316,7 +354,21 @@ export default {
}
item = this.suggestions[0];
}
this.currentSelected.push(item);
if (!this.currentSelected.find(({value}) => value === item.value)) {
jonasraoni marked this conversation as resolved.
Show resolved Hide resolved
const hasReachedLimit =
this.maxSelectedItems !== null &&
this.currentSelected.length >= this.maxSelectedItems;
const canReplace =
hasReachedLimit &&
this.currentSelected.length &&
this.replaceWhenFull;
if (!hasReachedLimit || canReplace) {
if (canReplace) {
this.currentSelected.pop();
}
this.currentSelected.push(item);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to get rid of the canReplace functionality. I think it will be confusing for users, who may not notice that an existing selection has been replaced.

Instead, when the max count has been reached, set inputProps.readonly to true and make sure the suggestions are empty. Then the user will have to delete their existing selection to search for a new one. I know it's a little bit more clicking, but less likely to cause user error this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I thought about renaming it to singleSelection: Boolean, then it would address my use case.
Another option would be to show a warning message "Only N items can be selected", but I saw the errors are injected from outside and didn't want to mess with adding extra messages.

}
this.inputValue = '';
this.$nextTick(() => {
this.$nextTick(() => {
Expand All @@ -342,9 +394,10 @@ export default {
* This must be implemented in a component that extends
* this component
*
* @param {Array} newItems
* @param {Array} items
* @param {Number} itemsMax
*/
setSuggestions(newItems) {
setSuggestions(items, itemsMax) {
throw new Error(
'The setSuggestions method must be implemented in any component that extends FieldBaseAutosuggest.'
);
Expand Down Expand Up @@ -404,7 +457,9 @@ export default {
if (newVal === oldVal) {
return;
}
this.getSuggestions();
if (newVal.length >= this.minInputLength) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prevents suggestions from updating when I delete characters. When the input value drops below minInputLength, go ahead and empty the suggestions array.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw this problem, but I forgot to fix 😁

this.setSearchPhrase(newVal);
}
}
},
mounted() {
Expand Down Expand Up @@ -482,9 +537,21 @@ export default {
}

.pkpFormField--autosuggest__autosuggest {
--maxAutosuggestHeight: 100%;
jonasraoni marked this conversation as resolved.
Show resolved Hide resolved
position: relative;
}

.pkpFormField--autosuggest__pagination {
border-top: @bg-border;
padding: 0.5rem 1rem;
}

.pkpFormField--autosuggest__spinner {
position: absolute;
right: 1rem;
top: 0.2rem;
}

.pkpFormField--autosuggest__input {
width: 100%;
}
Expand All @@ -497,6 +564,7 @@ export default {
max-width: 100%;
min-width: 20rem;
z-index: 9999;
background: @lift;
}

.autosuggest__results {
Expand All @@ -506,6 +574,8 @@ export default {
background: @lift;
box-shadow: 0 0.75rem 0.75rem rgba(0, 0, 0, 0.2);
font-size: @font-sml;
max-height: var(--maxAutosuggestHeight);
overflow: auto;

&:after {
content: '';
Expand Down
26 changes: 18 additions & 8 deletions src/components/Form/fields/FieldControlledVocab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export default {
this.suggestions = [];
return;
}
if (!this.suggestionsLoaded) {
if (!this.suggestionsLoaded && !this.isLoading) {
const self = this;
this.isLoading = true;
$.ajax({
url: this.apiUrl,
type: 'GET',
Expand All @@ -30,14 +31,23 @@ export default {
self.ajaxErrorCallback(r);
},
success(r) {
self.allSuggestions = r.map(v => {
return {
value: v,
label: v
};
});
self.setSuggestions.apply(self);
self.allSuggestions = r
.sort(
new Intl.Collator(
(this.localeKey || $.pkp.app.currentLocale).split('_')[0]
).compare
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather do this sorting on the server side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree! I just saw the list wasn't sorted and wanted to address it temporarily, until the pkp/pkp-lib#7529 is addressed :)

)
.map(v => {
return {
value: v,
label: v
};
});
self.setSuggestions();
self.suggestionsLoaded = true;
},
complete() {
self.isLoading = false;
}
});
}
Expand Down
30 changes: 30 additions & 0 deletions src/components/Form/fields/FieldMappedAutosuggest.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script>
import FieldBaseAutosuggest from './FieldBaseAutosuggest.vue';

export default {
name: 'FieldMappedAutosuggest',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a generic component that takes a mapping function as a prop, I'd prefer to use a specific component built for this use case. All of the other autosuggest fields are set up to use setSuggestions as the mapping function, and by the time it is compiled it won't add much to the build size to have a separate component for each.

So let's call this EnrollUserAsReviewerAutosuggest and move the mapping function into the setSuggestions method.

extends: FieldBaseAutosuggest,
props: {
dataMapper: {
type: Function
}
},
methods: {
/**
* Set the suggestions
*
* @param {Array} items List of items
* @param {Number} itemsMax Total amount of items from the API
*/
setSuggestions(items, itemsMax) {
const suggestions = this.dataMapper ? items.map(this.dataMapper) : items;
if (this.offset) {
this.suggestions.push(...suggestions);
} else {
this.suggestions = suggestions;
}
jonasraoni marked this conversation as resolved.
Show resolved Hide resolved
this.itemsMax = itemsMax;
}
}
};
</script>
30 changes: 16 additions & 14 deletions src/components/Form/fields/FieldSelectIssues.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@ export default {
/**
* Set the suggestions from an API response
*
* Maps the API response when searching for submissions
* Maps the API response when searching for issues
* to an object with value and label props.
*
* @param {Array} newItems List of submissions
* @param {Array} items List of issues
* @param {Number} itemsMax Total amount of issues based on the applied filters
*/
setSuggestions(newItems) {
const suggestions = newItems
.filter(item => {
return !this.selected.find(s => s.value === item.id);
})
.map(item => {
return {
value: item.id,
label: item.identification
};
});
this.suggestions = suggestions;
setSuggestions(items, itemsMax) {
const suggestions = items.map(item => {
return {
value: item.id,
label: item.identification
};
});
if (this.offset) {
this.suggestions.push(...suggestions);
} else {
this.suggestions = suggestions;
}
this.itemsMax = itemsMax;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than extend every autosuggest field with pagination, let's only support it where we need it. We shouldn't need to manage offset or pagination in any other autosuggest field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! I had to cancel the pagination in the controlled vocabs component, but the other way would feel more natural.

}
}
};
Expand Down
Loading