mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-11-10 17:36:33 +08:00
Implement result filters [SCI-9013]
This commit is contained in:
parent
1253511020
commit
b3965e5aca
11 changed files with 295 additions and 23 deletions
|
@ -11,8 +11,13 @@ class ResultsController < ApplicationController
|
|||
respond_to do |format|
|
||||
format.json do
|
||||
# API endpoint
|
||||
@results = @my_module.results
|
||||
|
||||
apply_sort!
|
||||
apply_filters!
|
||||
|
||||
render(
|
||||
json: apply_sort(@my_module.results),
|
||||
json: @results,
|
||||
formats: :json
|
||||
)
|
||||
end
|
||||
|
@ -76,7 +81,6 @@ class ResultsController < ApplicationController
|
|||
end
|
||||
|
||||
def update_asset_view_mode
|
||||
html = ''
|
||||
ActiveRecord::Base.transaction do
|
||||
@result.assets_view_mode = params[:assets_view_mode]
|
||||
@result.save!(touch: false)
|
||||
|
@ -117,23 +121,34 @@ class ResultsController < ApplicationController
|
|||
params.require(:result).permit(:name)
|
||||
end
|
||||
|
||||
def apply_sort(results)
|
||||
def apply_sort!
|
||||
case params[:sort]
|
||||
when 'updated_at_asc'
|
||||
results.order(updated_at: :asc)
|
||||
@results = @results.order(updated_at: :asc)
|
||||
when 'updated_at_desc'
|
||||
results.order(updated_at: :desc)
|
||||
@results = @results.order(updated_at: :desc)
|
||||
when 'created_at_asc'
|
||||
results.order(created_at: :asc)
|
||||
@results = @results.order(created_at: :asc)
|
||||
when 'created_at_desc'
|
||||
results.order(created_at: :desc)
|
||||
@results = @results.order(created_at: :desc)
|
||||
when 'name_asc'
|
||||
results.order(name: :asc)
|
||||
@results = @results.order(name: :asc)
|
||||
when 'name_desc'
|
||||
results.order(name: :desc)
|
||||
@results = @results.order(name: :desc)
|
||||
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
|
||||
@my_module = MyModule.readable_by_user(current_user).find(params[:my_module_id])
|
||||
end
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
<template>
|
||||
<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">
|
||||
<Result v-for="result in results" :key="result.id" :result="result" />
|
||||
</div>
|
||||
|
@ -21,7 +28,8 @@
|
|||
data() {
|
||||
return {
|
||||
results: [],
|
||||
sort: 'created_at_desc'
|
||||
sort: 'created_at_desc',
|
||||
filters: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
@ -30,18 +38,26 @@
|
|||
methods: {
|
||||
loadResults() {
|
||||
axios.get(
|
||||
`${this.url}?sort=${this.sort}`,
|
||||
`${this.url}`,
|
||||
{
|
||||
params: {
|
||||
sort: this.sort,
|
||||
...this.filters
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
},
|
||||
).then((response) => this.results = response.data.data);
|
||||
},
|
||||
setSort(sort) {
|
||||
this.sort = sort;
|
||||
this.loadResults();
|
||||
},
|
||||
setFilters(filters) {
|
||||
this.filters = filters;
|
||||
this.loadResults();
|
||||
},
|
||||
createResult() {
|
||||
axios.post(
|
||||
`${this.url}`,
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
{{ i18n.t('my_modules.results.collapse_label') }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-light icon-btn mr-3">
|
||||
<i class="sn-icon sn-icon-filter"></i>
|
||||
</button>
|
||||
<FilterDropdown :filters="filters" @applyFilters="setFilters" />
|
||||
|
||||
<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">
|
||||
|
@ -44,17 +42,47 @@
|
|||
'name_desc'
|
||||
];
|
||||
|
||||
import FilterDropdown from '../shared/filters/filter_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'ResultsToolbar',
|
||||
props: {
|
||||
sort: { type: String, required: true }
|
||||
sort: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filters: null
|
||||
}
|
||||
},
|
||||
components: { FilterDropdown },
|
||||
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;
|
||||
},
|
||||
methods: {
|
||||
setSort(sort) {
|
||||
this.$emit('setSort', sort);
|
||||
},
|
||||
setFilters(filters) {
|
||||
this.$emit('setFilters', filters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,19 @@
|
|||
type="datetime"
|
||||
class="form-control calendar-input sci-input-field"
|
||||
:id="this.selectorId"
|
||||
placeholder='dd/mm/yyyy'
|
||||
:placeholder="placeholder || 'dd/mm/yyyy'"
|
||||
/>
|
||||
<i class="sn-icon sn-icon-calendar"></i>
|
||||
</div>
|
||||
</template>
|
||||
import '../../../../vendor/assets/javascripts/bootstrap-datetimepicker';
|
||||
|
||||
<script>
|
||||
import '../../../../vendor/assets/javascripts/bootstrap-datetimepicker';
|
||||
|
||||
export default {
|
||||
name: 'DatePicker',
|
||||
props: {
|
||||
placeholder: { type: String },
|
||||
selectorId: { type: String, required: true },
|
||||
useCurrent: { type: Boolean, default: true },
|
||||
defaultValue: { type: Date, default: null }
|
||||
|
@ -26,7 +28,7 @@ import '../../../../vendor/assets/javascripts/bootstrap-datetimepicker';
|
|||
useCurrent: this.useCurrent,
|
||||
ignoreReadonly: this.ignoreReadOnly,
|
||||
locale: this.i18n.locale,
|
||||
format: this.dateFormat,
|
||||
format: $('body').data('datetime-picker-format'),
|
||||
date: this.defaultValue
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
<template>
|
||||
<div class="date-time-picker">
|
||||
<DatePicker v-if="!timeOnly" @change="updateDate" :selectorId="`${this.selectorId}_Date`" :defaultValue="defaultValue" />
|
||||
<TimePicker v-if="!dateOnly" @change="updateTime" :selectorId="`${this.selectorId}_Time`" :defaultValue="getTime(defaultValue)" />
|
||||
<DatePicker v-if="!timeOnly"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
|
@ -15,7 +25,8 @@
|
|||
dateOnly: { type: Boolean, default: false },
|
||||
timeOnly: { type: Boolean, default: false },
|
||||
selectorId: { type: String, required: true },
|
||||
defaultValue: {type: Date, required: false }
|
||||
defaultValue: { type: Date, required: false },
|
||||
placeholder: { type: String }
|
||||
},
|
||||
data() {
|
||||
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"
|
||||
name_asc: "Name A to Z"
|
||||
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:
|
||||
comment_title: "Comments"
|
||||
no_comments: "No comments"
|
||||
|
@ -3703,6 +3711,8 @@ en:
|
|||
Added: 'Added'
|
||||
by: 'by'
|
||||
name: 'name'
|
||||
From: 'From'
|
||||
To: 'To'
|
||||
|
||||
marvinjs:
|
||||
new_sketch: "New Chemical Drawing"
|
||||
|
|
Loading…
Reference in a new issue