Quickly filter by text within normal filters section without showing any operator (#6960)

* Add quick filter by text

* Debounce search filter input

[ci skip]
pull/6977/head
Wieland Lindenthal 6 years ago committed by Oliver Günther
parent 5d8836e30c
commit 4970471739
  1. 5
      app/assets/stylesheets/content/_advanced_filters.sass
  2. 2
      config/locales/js-en.yml
  3. 16
      frontend/src/app/components/filters/query-filters/query-filters.component.html
  4. 3
      frontend/src/app/components/filters/query-filters/query-filters.component.ts
  5. 111
      frontend/src/app/components/filters/quick-filter-by-text-input/quick-filter-by-text-input.component.ts
  6. 5
      frontend/src/app/components/filters/quick-filter-by-text-input/quick-filter-by-text-input.html
  7. 2
      frontend/src/app/components/wp-buttons/wp-filter-button/wp-filter-button.component.ts
  8. 4
      frontend/src/app/components/wp-fast-table/state/wp-table-filters.service.ts
  9. 13
      frontend/src/app/components/wp-fast-table/wp-table-filters.ts
  10. 4
      frontend/src/app/modules/hal/resources/query-filter-instance-resource.ts
  11. 2
      frontend/src/app/modules/work_packages/openproject-work-packages.module.ts
  12. 4
      modules/my_project_page/spec/features/block_editing_spec.rb
  13. 4
      spec/features/types/form_configuration_query_spec.rb
  14. 4
      spec/features/work_packages/table/configuration_modal/filter_spec.rb
  15. 4
      spec/features/wysiwyg/macros/embedded_tables_spec.rb

@ -55,6 +55,8 @@
grid-gap: 10px
align-items: center
margin-bottom: 10px
&.--without-operator
grid-template-columns: 20% 45% 50px
.advanced-filters--filter-name,
.advanced-filters--add-filter-label
@ -111,14 +113,13 @@
.advanced-filters--spacer
border-top: 1px solid $filters--border-color
height: 1px
margin: 0.75rem 1rem
margin: 0.75rem 0
.advanced-filters--spacer,
.advanced-filters--filter
&.hidden
display: none !important
fieldset#date-range p
margin: 2px 0 2px 0

@ -591,6 +591,7 @@ en:
label_enable_multi_select: "Enable multiselect"
label_disable_multi_select: "Disable multiselect"
label_filter_add: "Add filter"
label_filter_by_text: "Filter by text"
label_options: "Options"
label_column_multiselect: "Combined dropdown field: Select with arrow keys, confirm selection with enter, delete with backspace"
label_switch_to_single_select: "Switch to single select"
@ -601,6 +602,7 @@ en:
message_view_spent_time: "Show spent time for this work package"
message_work_package_read_only: "Work package is locked in this status. No attribute other than status can be altered."
no_value: "No value"
placeholder_filter_by_text: "Subject, description, comments, ..."
inline_create:
title: 'Click here to add a new work package to this list'
create:

@ -8,6 +8,22 @@
</a>
<ul class="advanced-filters--filters">
<li id="filter_search"
class="advanced-filters--filter --without-operator">
<label for="filter-by-text-input"
class="advanced-filters--filter-name"
[textContent]="text.filter_by_text"
[attr.title]="text.filter_by_text">
</label>
<div class="advanced-filters--filter-value">
<wp-filter-by-text-input>
</wp-filter-by-text-input>
</div>
</li>
<li class="advanced-filters--spacer" *ngIf="filters.anyCurrentlyVisibleFilters"></li>
<ng-container *ngFor="let filter of filters.current; trackBy: trackByName ; let index = index">
<li id="filter_{{filter.id}}"
query-filter

@ -66,7 +66,8 @@ export class QueryFiltersComponent implements OnInit, OnChanges, OnDestroy {
close_form: this.I18n.t('js.close_form_title'),
selected_filter_list: this.I18n.t('js.label_selected_filter_list'),
button_delete: this.I18n.t('js.button_delete'),
please_select: this.I18n.t('js.placeholders.selection')
please_select: this.I18n.t('js.placeholders.selection'),
filter_by_text: this.I18n.t('js.work_packages.label_filter_by_text')
};
constructor(readonly wpTableFilters:WorkPackageTableFiltersService,

@ -0,0 +1,111 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {I18nService} from "app/modules/common/i18n/i18n.service";
import {WorkPackageTableFiltersService} from "app/components/wp-fast-table/state/wp-table-filters.service";
import {QueryFilterResource} from "app/modules/hal/resources/query-filter-resource";
import {componentDestroyed, untilComponentDestroyed} from "ng2-rx-componentdestroyed";
import {TableState} from "app/components/wp-table/table-state/table-state";
import {WorkPackageCacheService} from "app/components/work-packages/work-package-cache.service";
import {Subject} from "rxjs";
import {debounceTime, distinctUntilChanged} from "rxjs/operators";
@Component({
selector: 'wp-filter-by-text-input',
templateUrl: './quick-filter-by-text-input.html'
})
export class WorkPackageFilterByTextInputComponent implements OnInit, OnDestroy {
public text = {
createWithDropdown: this.I18n.t('js.work_packages.create.button'),
createButton: this.I18n.t('js.label_work_package'),
explanation: this.I18n.t('js.label_create_work_package'),
placeholder: this.I18n.t('js.work_packages.placeholder_filter_by_text')
};
public searchTerm:string;
private searchTermChanged:Subject<string> = new Subject<string>();
private availableSearchFilter:QueryFilterResource;
constructor(readonly I18n:I18nService,
readonly tableState:TableState,
readonly wpTableFilters:WorkPackageTableFiltersService,
readonly wpCacheService:WorkPackageCacheService) {
this.searchTermChanged
.pipe(
untilComponentDestroyed(this),
debounceTime(250),
distinctUntilChanged()
)
.subscribe(term => {
this.searchTerm = term;
let currentSearchFilter = this.wpTableFilters.find('search');
if (this.searchTerm.length > 0) {
if (!currentSearchFilter) {
currentSearchFilter = this.wpTableFilters.currentState.add(this.availableSearchFilter);
}
currentSearchFilter.operator = currentSearchFilter.findOperator('**')!;
currentSearchFilter.values = [this.searchTerm];
} else if (currentSearchFilter) {
this.wpTableFilters.currentState.remove(currentSearchFilter);
}
this.wpTableFilters.replace(this.wpTableFilters.currentState);
});
}
public ngOnInit() {
let self:WorkPackageFilterByTextInputComponent = this;
this.wpTableFilters
.observeUntil(
componentDestroyed(this)
)
.subscribe(() => {
const currentSearchFilter = this.wpTableFilters.find('search');
if (currentSearchFilter) {
this.searchTerm = currentSearchFilter.values[0] as string;
} else {
this.searchTerm = '';
}
self.availableSearchFilter = _.find(self.wpTableFilters.currentState.availableFilters,
{ id: 'search' }) as QueryFilterResource;
});
}
public ngOnDestroy() {
// Nothing to do
}
public valueChange(term:string) {
this.searchTermChanged.next(term);
}
}

@ -0,0 +1,5 @@
<input id="filter-by-text-input"
type="text"
placeholder="{{text.placeholder}}"
[ngModel]="searchTerm"
(ngModelChange)="valueChange($event)">

@ -90,7 +90,7 @@ export class WorkPackageFilterButtonComponent extends AbstractWorkPackageButtonC
this.wpTableFilters
.observeUntil(componentDestroyed(this))
.subscribe(state => {
this.count = state.currentVisibleFilters.length;
this.count = state.currentlyVisibleFilters.length;
this.initialized = true;
});
}

@ -71,6 +71,10 @@ export class WorkPackageTableFiltersService extends WorkPackageTableBaseService<
);
}
public find(id:string):QueryFilterInstanceResource|undefined {
return _.find(this.currentState.current, filter => filter.id === id);
}
public applyToQuery(query:QueryResource) {
query.filters = this.current;
return true;

@ -50,6 +50,7 @@ export class WorkPackageTableFilters extends WorkPackageTableBaseState<QueryFilt
'includes',
'requires',
'required',
'search',
'subjectOrId'
];
@ -97,9 +98,15 @@ export class WorkPackageTableFilters extends WorkPackageTableBaseState<QueryFilt
return _.every(this.current, filter => filter.isCompletelyDefined());
}
public get currentVisibleFilters() {
return this.currentFilters
.filter((filter) => this.hidden.indexOf(filter.id) === -1);
public get currentlyVisibleFilters() {
const invisibleFilters = new Set(this.hidden);
invisibleFilters.delete('search');
return _.reject(this.currentFilters, (filter) => invisibleFilters.has(filter.id));
}
public get anyCurrentlyVisibleFilters():boolean {
return this.currentlyVisibleFilters.length > 0;
}
private get currentFilters() {

@ -77,4 +77,8 @@ export class QueryFilterInstanceResource extends HalResource {
public isCompletelyDefined() {
return this.values.length || (this.currentSchema && !this.currentSchema.isValueRequired());
}
public findOperator(operatorSymbol:string):QueryOperatorResource|undefined {
return _.find(this.schema.availableOperators, (operator:QueryOperatorResource) => operator.id === operatorSymbol) as QueryOperatorResource|undefined;
}
}

@ -186,6 +186,7 @@ import {WorkPackageSplitViewComponent} from "core-app/modules/work_packages/rout
import {WorkPackagesFullViewComponent} from "core-app/modules/work_packages/routing/wp-full-view/wp-full-view.component";
import {AttachmentsUploadComponent} from 'core-app/modules/attachments/attachments-upload/attachments-upload.component';
import {AttachmentListComponent} from 'core-app/modules/attachments/attachment-list/attachment-list.component';
import {WorkPackageFilterByTextInputComponent} from "core-components/filters/quick-filter-by-text-input/quick-filter-by-text-input.component";
@NgModule({
imports: [
@ -342,6 +343,7 @@ import {AttachmentListComponent} from 'core-app/modules/attachments/attachment-l
WorkPackageTimelineTableController,
WorkPackageCreateButtonComponent,
WorkPackageFilterByTextInputComponent,
// Single view
WorkPackageOverviewTabComponent,

@ -215,7 +215,7 @@ describe 'My project page editing', type: :feature, js: true do
modal.expect_open
modal.switch_to 'Filters'
filters.expect_filter_count 1
filters.expect_filter_count 2
filters.add_filter_by('Type', 'is', work_package.type.name)
modal.switch_to 'Columns'
@ -254,7 +254,7 @@ describe 'My project page editing', type: :feature, js: true do
modal.expect_open
modal.switch_to 'Filters'
filters.expect_filter_count 2
filters.expect_filter_count 3
modal.switch_to 'Columns'
columns.assume_opened
columns.expect_checked 'ID'

@ -198,7 +198,7 @@ describe 'form query configuration', type: :feature, js: true do
modal.expect_open
modal.switch_to 'Filters'
# the templated filter should be hidden in the Filters tab
filters.expect_filter_count 0
filters.expect_filter_count 1
filters.add_filter_by('Type', 'is', type_task.name)
filters.save
@ -225,7 +225,7 @@ describe 'form query configuration', type: :feature, js: true do
# Expect filter still there
modal.expect_open
modal.switch_to 'Filters'
filters.expect_filter_count 1
filters.expect_filter_count 2
filters.expect_filter_by 'Type', 'is', type_task.name
# Remove the filter again

@ -38,7 +38,7 @@ describe 'Work Package table configuration modal filters spec', js: true do
wp_table.expect_work_package_listed work_package_with_version, work_package_without_version
filters.open
filters.expect_filter_count 1
filters.expect_filter_count 2
filters.add_filter_by('Version', 'is', version.name)
filters.save
@ -48,7 +48,7 @@ describe 'Work Package table configuration modal filters spec', js: true do
wp_table.save_as('Some query name')
filters.open
filters.expect_filter_count 2
filters.expect_filter_count 3
filters.remove_filter 'version'
filters.save

@ -55,7 +55,7 @@ describe 'Wysiwyg embedded work package tables',
modal.expect_open
modal.switch_to 'Filters'
filters.expect_filter_count 1
filters.expect_filter_count 2
filters.add_filter_by('Type', 'is', work_package.type.name)
modal.switch_to 'Columns'
@ -80,7 +80,7 @@ describe 'Wysiwyg embedded work package tables',
modal.expect_open
modal.switch_to 'Filters'
filters.expect_filter_count 2
filters.expect_filter_count 3
modal.switch_to 'Columns'
columns.assume_opened
columns.expect_checked 'ID'

Loading…
Cancel
Save