mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-01-04 14:44:26 +08:00
Merge pull request #6049 from artoscinote/ma_SCI_9013
Implement result filters [SCI-9013]
This commit is contained in:
commit
abd6e794e9
11 changed files with 294 additions and 22 deletions
|
@ -9,8 +9,13 @@ class ResultsController < ApplicationController
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json do
|
format.json do
|
||||||
# API endpoint
|
# API endpoint
|
||||||
|
@results = @my_module.results
|
||||||
|
|
||||||
|
apply_sort!
|
||||||
|
apply_filters!
|
||||||
|
|
||||||
render(
|
render(
|
||||||
json: apply_sort(@my_module.results),
|
json: @results,
|
||||||
formats: :json
|
formats: :json
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -74,7 +79,6 @@ class ResultsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_asset_view_mode
|
def update_asset_view_mode
|
||||||
html = ''
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@result.assets_view_mode = params[:assets_view_mode]
|
@result.assets_view_mode = params[:assets_view_mode]
|
||||||
@result.save!(touch: false)
|
@result.save!(touch: false)
|
||||||
|
@ -115,23 +119,34 @@ class ResultsController < ApplicationController
|
||||||
params.require(:result).permit(:name)
|
params.require(:result).permit(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_sort(results)
|
def apply_sort!
|
||||||
case params[:sort]
|
case params[:sort]
|
||||||
when 'updated_at_asc'
|
when 'updated_at_asc'
|
||||||
results.order(updated_at: :asc)
|
@results = @results.order(updated_at: :asc)
|
||||||
when 'updated_at_desc'
|
when 'updated_at_desc'
|
||||||
results.order(updated_at: :desc)
|
@results = @results.order(updated_at: :desc)
|
||||||
when 'created_at_asc'
|
when 'created_at_asc'
|
||||||
results.order(created_at: :asc)
|
@results = @results.order(created_at: :asc)
|
||||||
when 'created_at_desc'
|
when 'created_at_desc'
|
||||||
results.order(created_at: :desc)
|
@results = @results.order(created_at: :desc)
|
||||||
when 'name_asc'
|
when 'name_asc'
|
||||||
results.order(name: :asc)
|
@results = @results.order(name: :asc)
|
||||||
when 'name_desc'
|
when 'name_desc'
|
||||||
results.order(name: :desc)
|
@results = @results.order(name: :desc)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def apply_filters!
|
||||||
|
if params[:query].present?
|
||||||
|
@results = @results.search(current_user, params[:archived] == 'true', params[:query], params[:page] || 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
@results = @results.where('created_at >= ?', params[:created_at_from]) if params[:created_at_from]
|
||||||
|
@results = @results.where('created_at <= ?', params[:created_at_to]) if params[:created_at_to]
|
||||||
|
@results = @results.where('updated_at >= ?', params[:updated_at_from]) if params[:updated_at_from]
|
||||||
|
@results = @results.where('updated_at <= ?', params[:updated_at_to]) if params[:updated_at_to]
|
||||||
|
end
|
||||||
|
|
||||||
def load_my_module
|
def load_my_module
|
||||||
@my_module = MyModule.readable_by_user(current_user).find(params[:my_module_id])
|
@my_module = MyModule.readable_by_user(current_user).find(params[:my_module_id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="results-wrapper">
|
<div class="results-wrapper">
|
||||||
<ResultsToolbar :sort="sort" @setSort="setSort" @newResult="createResult" @expandAll="expandAll" @collapseAll="collapseAll" class="mb-3" />
|
<ResultsToolbar :sort="sort"
|
||||||
|
@setSort="setSort"
|
||||||
|
@setFilters="setFilters"
|
||||||
|
@newResult="createResult"
|
||||||
|
@expandAll="expandAll"
|
||||||
|
@collapseAll="collapseAll"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
<div class="results-list">
|
<div class="results-list">
|
||||||
<Result v-for="result in results" :key="result.id"
|
<Result v-for="result in results" :key="result.id"
|
||||||
:result="result"
|
:result="result"
|
||||||
|
@ -27,6 +34,7 @@
|
||||||
return {
|
return {
|
||||||
results: [],
|
results: [],
|
||||||
sort: 'created_at_desc',
|
sort: 'created_at_desc',
|
||||||
|
filters: {},
|
||||||
resultToReload: null
|
resultToReload: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -39,18 +47,26 @@
|
||||||
},
|
},
|
||||||
loadResults() {
|
loadResults() {
|
||||||
axios.get(
|
axios.get(
|
||||||
`${this.url}?sort=${this.sort}`,
|
`${this.url}`,
|
||||||
{
|
{
|
||||||
|
params: {
|
||||||
|
sort: this.sort,
|
||||||
|
...this.filters
|
||||||
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
).then((response) => this.results = response.data.data);
|
).then((response) => this.results = response.data.data);
|
||||||
},
|
},
|
||||||
setSort(sort) {
|
setSort(sort) {
|
||||||
this.sort = sort;
|
this.sort = sort;
|
||||||
this.loadResults();
|
this.loadResults();
|
||||||
},
|
},
|
||||||
|
setFilters(filters) {
|
||||||
|
this.filters = filters;
|
||||||
|
this.loadResults();
|
||||||
|
},
|
||||||
createResult() {
|
createResult() {
|
||||||
axios.post(
|
axios.post(
|
||||||
`${this.url}`,
|
`${this.url}`,
|
||||||
|
|
|
@ -14,9 +14,7 @@
|
||||||
{{ i18n.t('my_modules.results.collapse_label') }}
|
{{ i18n.t('my_modules.results.collapse_label') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-light icon-btn mr-3">
|
<FilterDropdown :filters="filters" @applyFilters="setFilters" />
|
||||||
<i class="sn-icon sn-icon-filter"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="dropdown-toggle btn btn-light icon-btn mr-3" id="sortDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
<button class="dropdown-toggle btn btn-light icon-btn mr-3" id="sortDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||||
|
@ -44,17 +42,47 @@
|
||||||
'name_desc'
|
'name_desc'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
import FilterDropdown from '../shared/filters/filter_dropdown.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ResultsToolbar',
|
name: 'ResultsToolbar',
|
||||||
props: {
|
props: {
|
||||||
sort: { type: String, required: true }
|
sort: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
filters: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { FilterDropdown },
|
||||||
created() {
|
created() {
|
||||||
|
this.filters = [
|
||||||
|
{
|
||||||
|
key: 'query',
|
||||||
|
type: 'Text',
|
||||||
|
label: this.i18n.t('my_modules.results.filters.query.label'),
|
||||||
|
placeholder: this.i18n.t('my_modules.results.filters.query.placeholder')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'created_at',
|
||||||
|
type: 'DateRange',
|
||||||
|
label: this.i18n.t('my_modules.results.filters.created_at.label')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'updated_at',
|
||||||
|
type: 'DateRange',
|
||||||
|
label: this.i18n.t('my_modules.results.filters.updated_at.label')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
this.sorts = SORTS;
|
this.sorts = SORTS;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setSort(sort) {
|
setSort(sort) {
|
||||||
this.$emit('setSort', sort);
|
this.$emit('setSort', sort);
|
||||||
|
},
|
||||||
|
setFilters(filters) {
|
||||||
|
this.$emit('setFilters', filters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,17 +5,19 @@
|
||||||
type="datetime"
|
type="datetime"
|
||||||
class="form-control calendar-input sci-input-field"
|
class="form-control calendar-input sci-input-field"
|
||||||
:id="this.selectorId"
|
:id="this.selectorId"
|
||||||
placeholder='dd/mm/yyyy'
|
:placeholder="placeholder || 'dd/mm/yyyy'"
|
||||||
/>
|
/>
|
||||||
<i class="sn-icon sn-icon-calendar"></i>
|
<i class="sn-icon sn-icon-calendar"></i>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
import '../../../../vendor/assets/javascripts/bootstrap-datetimepicker';
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import '../../../../vendor/assets/javascripts/bootstrap-datetimepicker';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DatePicker',
|
name: 'DatePicker',
|
||||||
props: {
|
props: {
|
||||||
|
placeholder: { type: String },
|
||||||
selectorId: { type: String, required: true },
|
selectorId: { type: String, required: true },
|
||||||
useCurrent: { type: Boolean, default: true },
|
useCurrent: { type: Boolean, default: true },
|
||||||
defaultValue: { type: Date, default: null }
|
defaultValue: { type: Date, default: null }
|
||||||
|
@ -26,7 +28,7 @@ import '../../../../vendor/assets/javascripts/bootstrap-datetimepicker';
|
||||||
useCurrent: this.useCurrent,
|
useCurrent: this.useCurrent,
|
||||||
ignoreReadonly: this.ignoreReadOnly,
|
ignoreReadonly: this.ignoreReadOnly,
|
||||||
locale: this.i18n.locale,
|
locale: this.i18n.locale,
|
||||||
format: this.dateFormat,
|
format: $('body').data('datetime-picker-format'),
|
||||||
date: this.defaultValue
|
date: this.defaultValue
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="date-time-picker">
|
<div class="date-time-picker">
|
||||||
<DatePicker v-if="!timeOnly" @change="updateDate" :selectorId="`${this.selectorId}_Date`" :defaultValue="defaultValue" />
|
<DatePicker v-if="!timeOnly"
|
||||||
<TimePicker v-if="!dateOnly" @change="updateTime" :selectorId="`${this.selectorId}_Time`" :defaultValue="getTime(defaultValue)" />
|
@change="updateDate"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:selectorId="`${this.selectorId}_Date`"
|
||||||
|
:defaultValue="defaultValue"
|
||||||
|
/>
|
||||||
|
<TimePicker v-if="!dateOnly"
|
||||||
|
@change="updateTime"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:selectorId="`${this.selectorId}_Time`"
|
||||||
|
:defaultValue="getTime(defaultValue)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -15,7 +25,8 @@
|
||||||
dateOnly: { type: Boolean, default: false },
|
dateOnly: { type: Boolean, default: false },
|
||||||
timeOnly: { type: Boolean, default: false },
|
timeOnly: { type: Boolean, default: false },
|
||||||
selectorId: { type: String, required: true },
|
selectorId: { type: String, required: true },
|
||||||
defaultValue: {type: Date, required: false }
|
defaultValue: { type: Date, required: false },
|
||||||
|
placeholder: { type: String }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
81
app/javascript/vue/shared/filters/filter_dropdown.vue
Normal file
81
app/javascript/vue/shared/filters/filter_dropdown.vue
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div ref="dropdown" class="filter-container dropdown" :class="{ 'filters-applied': appliedDotIsShown }">
|
||||||
|
<button class="btn btn-light icon-btn filter-button" :title="i18n.t('filters_modal.title')" data-toggle="dropdown"><i class="sn-icon sn-icon-filter"></i></button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right filter-dropdown" @click.stop.self>
|
||||||
|
<div class="header !-mb-2">
|
||||||
|
<div class="title">{{ i18n.t('filters_modal.title') }}</div>
|
||||||
|
<button @click="toggleDropdown" type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="body p-3">
|
||||||
|
<div v-for="filter in filters" :key="filter.key + resetFilters" class="mt-4">
|
||||||
|
<Component :is="`${filter.type}Filter`" :filter="filter" :value="filterValues[filter.key]" @update="updateFilter" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<div class="buttons">
|
||||||
|
<div @click.prevent="clearFilters" class="btn btn-secondary">
|
||||||
|
{{ i18n.t('filters_modal.clear_btn') }}
|
||||||
|
</div>
|
||||||
|
<div @click.prevent="applyFilters" class="btn btn-primary">
|
||||||
|
{{ i18n.t('filters_modal.show_btn.one') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import TextFilter from './inputs/text_filter.vue'
|
||||||
|
import SelectFilter from './inputs/select_filter.vue';
|
||||||
|
import DateRangeFilter from './inputs/date_range_filter.vue';
|
||||||
|
import CheckboxFilter from './inputs/checkbox_filter.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FilterDropdown',
|
||||||
|
props: {
|
||||||
|
filters: { type: Array, required: true }
|
||||||
|
},
|
||||||
|
components: { TextFilter, SelectFilter, DateRangeFilter, CheckboxFilter },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
filterValues: {},
|
||||||
|
filtersApplied: false,
|
||||||
|
resetFilters: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
appliedDotIsShown() {
|
||||||
|
return this.filtersApplied && Object.keys(this.filterValues).length !== 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateFilter(params) {
|
||||||
|
if (params.value !== '' && params.value !== undefined && params.value !== null) {
|
||||||
|
this.$set(this.filterValues, params.key, params.value);
|
||||||
|
} else {
|
||||||
|
this.$delete(this.filterValues, params.key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyFilters() {
|
||||||
|
this.filtersApplied = true;
|
||||||
|
this.$emit('applyFilters', this.filterValues);
|
||||||
|
this.toggleDropdown();
|
||||||
|
},
|
||||||
|
clearFilters() {
|
||||||
|
this.filterValues = {};
|
||||||
|
this.filtersApplied = false;
|
||||||
|
|
||||||
|
// This changes filter keys in the v-for, so they get fully reloaded,
|
||||||
|
// which prevents perserved state issues with datepickers
|
||||||
|
this.resetFilters = !this.resetFilters;
|
||||||
|
this.$emit('applyFilters', this.filterValues);
|
||||||
|
this.toggleDropdown();
|
||||||
|
},
|
||||||
|
toggleDropdown() {
|
||||||
|
this.$refs.dropdown.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
14
app/javascript/vue/shared/filters/inputs/checkbox_filter.vue
Normal file
14
app/javascript/vue/shared/filters/inputs/checkbox_filter.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<div>TODO</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CheckboxFilter',
|
||||||
|
props: {
|
||||||
|
filter: { type: Object, required: true }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<label>{{ filter.label }}</label>
|
||||||
|
<div class="flex center justify-between">
|
||||||
|
<DateTimePicker
|
||||||
|
@change="updateDateFrom"
|
||||||
|
:placeholder="i18n.t('From')"
|
||||||
|
:dateOnly="true"
|
||||||
|
:selectorId="`DatePicker${filter.key}`"
|
||||||
|
/>
|
||||||
|
<div class="px-2 mt-2"> — </div>
|
||||||
|
<DateTimePicker
|
||||||
|
@change="updateDateTo"
|
||||||
|
:placeholder="i18n.t('To')"
|
||||||
|
:dateOnly="true"
|
||||||
|
:selectorId="`DatePickerTo${filter.key}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DateTimePicker from '../../date_time_picker.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DateRangeFilter',
|
||||||
|
props: {
|
||||||
|
filter: { type: Object, required: true }
|
||||||
|
},
|
||||||
|
components: { DateTimePicker },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dateFrom: null,
|
||||||
|
dateTo: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateDateFrom(value) {
|
||||||
|
this.dateFrom = value;
|
||||||
|
this.updateFilter();
|
||||||
|
},
|
||||||
|
updateDateTo(value) {
|
||||||
|
this.dateTo = value;
|
||||||
|
this.updateFilter();
|
||||||
|
},
|
||||||
|
updateFilter() {
|
||||||
|
this.$emit('update', { key: `${this.filter.key}_from`, value: this.dateFrom });
|
||||||
|
this.$emit('update', { key: `${this.filter.key}_to`, value: this.dateTo });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
15
app/javascript/vue/shared/filters/inputs/select_filter.vue
Normal file
15
app/javascript/vue/shared/filters/inputs/select_filter.vue
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<div>TODO</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Select from '../../select.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SelectFilter',
|
||||||
|
props: {
|
||||||
|
filter: { type: Object, required: true }
|
||||||
|
},
|
||||||
|
components: { Select }
|
||||||
|
}
|
||||||
|
</script>
|
28
app/javascript/vue/shared/filters/inputs/text_filter.vue
Normal file
28
app/javascript/vue/shared/filters/inputs/text_filter.vue
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<label>{{ filter.label }}</label>
|
||||||
|
<div class="sci-input-container">
|
||||||
|
<input class="sci-input-field"
|
||||||
|
type="text"
|
||||||
|
:id="filter.key"
|
||||||
|
:placeholder="filter.placeholder"
|
||||||
|
:value="value"
|
||||||
|
@input="update($event.target.value)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TextFilter',
|
||||||
|
props: {
|
||||||
|
filter: { type: Object, required: true },
|
||||||
|
value: { type: String }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
update(value) {
|
||||||
|
this.$emit('update', { key: this.filter.key, value: value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1265,6 +1265,14 @@ en:
|
||||||
created_at_desc: "Created last"
|
created_at_desc: "Created last"
|
||||||
name_asc: "Name A to Z"
|
name_asc: "Name A to Z"
|
||||||
name_desc: "Name Z to A"
|
name_desc: "Name Z to A"
|
||||||
|
filters:
|
||||||
|
query:
|
||||||
|
label: 'Contains text'
|
||||||
|
placeholder: 'Enter search terms'
|
||||||
|
created_at:
|
||||||
|
label: 'Created date'
|
||||||
|
updated_at:
|
||||||
|
label: 'Updated date'
|
||||||
options:
|
options:
|
||||||
comment_title: "Comments"
|
comment_title: "Comments"
|
||||||
no_comments: "No comments"
|
no_comments: "No comments"
|
||||||
|
@ -3710,6 +3718,8 @@ en:
|
||||||
Added: 'Added'
|
Added: 'Added'
|
||||||
by: 'by'
|
by: 'by'
|
||||||
name: 'name'
|
name: 'name'
|
||||||
|
From: 'From'
|
||||||
|
To: 'To'
|
||||||
|
|
||||||
marvinjs:
|
marvinjs:
|
||||||
new_sketch: "New Chemical Drawing"
|
new_sketch: "New Chemical Drawing"
|
||||||
|
|
Loading…
Reference in a new issue