diff --git a/app/models/queries/base_query.rb b/app/models/queries/base_query.rb index 7e1401ca82..c0356cac90 100644 --- a/app/models/queries/base_query.rb +++ b/app/models/queries/base_query.rb @@ -163,7 +163,7 @@ class Queries::BaseQuery end def context - nil + self end def apply_filters(scope) diff --git a/app/models/queries/filters/available_filters.rb b/app/models/queries/filters/available_filters.rb index 68c17db1cc..b4bc663efb 100644 --- a/app/models/queries/filters/available_filters.rb +++ b/app/models/queries/filters/available_filters.rb @@ -76,7 +76,12 @@ module Queries private def non_existing_filter(key) - ::Queries::Filters::NotExistingFilter.create!(name: key) + case key.to_sym + when :typeahead + ::Queries::Filters::EmptyFilter.create!(name: key) + else + ::Queries::Filters::NotExistingFilter.create!(name: key) + end end def get_initialized_filter(key, no_memoization) diff --git a/app/models/queries/filters/empty_filter.rb b/app/models/queries/filters/empty_filter.rb new file mode 100644 index 0000000000..3887788359 --- /dev/null +++ b/app/models/queries/filters/empty_filter.rb @@ -0,0 +1,62 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 the OpenProject GmbH +# +# 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 COPYRIGHT and LICENSE files for more details. +#++ + +module Queries + module Filters + class EmptyFilter < Base + def available? + true + end + + def type + :inexistent + end + + def self.key + :empty + end + + def human_name + name.to_s.presence || type + end + + # deactivating superclass validation + def validate_inclusion_of_operator; end + + def scope + context.default_scope + end + + def attributes_hash + nil + end + end + end +end diff --git a/app/models/queries/filters/shared/any_user_name_attribute_filter.rb b/app/models/queries/filters/shared/any_user_name_attribute_filter.rb index 40092397ea..b80d83b4b1 100644 --- a/app/models/queries/filters/shared/any_user_name_attribute_filter.rb +++ b/app/models/queries/filters/shared/any_user_name_attribute_filter.rb @@ -41,6 +41,7 @@ module Queries::Filters::Shared::AnyUserNameAttributeFilter def available_operators [Queries::Operators::Contains, + Queries::Operators::Everywhere, Queries::Operators::NotContains] end diff --git a/app/models/queries/filters/shared/user_name_filter.rb b/app/models/queries/filters/shared/user_name_filter.rb index 05875916db..111891fe34 100644 --- a/app/models/queries/filters/shared/user_name_filter.rb +++ b/app/models/queries/filters/shared/user_name_filter.rb @@ -49,7 +49,7 @@ module Queries::Filters::Shared::UserNameFilter ["#{sql_concat_name} IN (?)", sql_value] when '!' ["#{sql_concat_name} NOT IN (?)", sql_value] - when '~' + when '~', '**' ["#{sql_concat_name} LIKE ?", "%#{sql_value}%"] when '!~' ["#{sql_concat_name} NOT LIKE ?", "%#{sql_value}%"] @@ -62,7 +62,7 @@ module Queries::Filters::Shared::UserNameFilter case operator when '=', '!' values.map { |val| self.class.connection.quote_string(val.downcase) }.join(',') - when '~', '!~' + when '**', '~', '!~' values.first.downcase end end diff --git a/app/models/queries/principals.rb b/app/models/queries/principals.rb index 5323df6ea0..830ef614ca 100644 --- a/app/models/queries/principals.rb +++ b/app/models/queries/principals.rb @@ -39,6 +39,7 @@ module Queries::Principals register.filter query, filters::StatusFilter register.filter query, filters::NameFilter register.filter query, filters::AnyNameAttributeFilter + register.filter query, filters::TypeaheadFilter register.filter query, filters::IdFilter register.order query, orders::NameOrder diff --git a/app/models/queries/principals/filters/typeahead_filter.rb b/app/models/queries/principals/filters/typeahead_filter.rb new file mode 100644 index 0000000000..e8eba86b0c --- /dev/null +++ b/app/models/queries/principals/filters/typeahead_filter.rb @@ -0,0 +1,43 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 the OpenProject GmbH +# +# 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 COPYRIGHT and LICENSE files for more details. +#++ + +class Queries::Principals::Filters::TypeaheadFilter < Queries::Principals::Filters::AnyNameAttributeFilter + def self.key + :typeahead + end + + def type + :search + end + + def human_name + I18n.t('label_search') + end +end diff --git a/app/models/queries/projects.rb b/app/models/queries/projects.rb index 6e3bde4f5b..7682fb849b 100644 --- a/app/models/queries/projects.rb +++ b/app/models/queries/projects.rb @@ -40,6 +40,7 @@ module Queries::Projects filter query, filters::TemplatedFilter filter query, filters::PublicFilter filter query, filters::NameAndIdentifierFilter + filter query, filters::TypeaheadFilter filter query, filters::CustomFieldFilter filter query, filters::CreatedAtFilter filter query, filters::LatestActivityAtFilter diff --git a/app/models/queries/projects/filters/name_and_identifier_filter.rb b/app/models/queries/projects/filters/name_and_identifier_filter.rb index ff4ca55641..11e948eacd 100644 --- a/app/models/queries/projects/filters/name_and_identifier_filter.rb +++ b/app/models/queries/projects/filters/name_and_identifier_filter.rb @@ -39,7 +39,7 @@ class Queries::Projects::Filters::NameAndIdentifierFilter < Queries::Projects::F where_equal when '!' where_not_equal - when '~' + when '~', '**' where_contains when '!~' where_not_contains diff --git a/app/models/queries/projects/filters/typeahead_filter.rb b/app/models/queries/projects/filters/typeahead_filter.rb new file mode 100644 index 0000000000..8b6f7993f5 --- /dev/null +++ b/app/models/queries/projects/filters/typeahead_filter.rb @@ -0,0 +1,43 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2021 the OpenProject GmbH +# +# 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 COPYRIGHT and LICENSE files for more details. +#++ + +class Queries::Projects::Filters::TypeaheadFilter < Queries::Projects::Filters::NameAndIdentifierFilter + def self.key + :typeahead + end + + def type + :search + end + + def human_name + I18n.t('label_search') + end +end diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 84ba517218..1be55787ad 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -279,6 +279,7 @@ en: enumeration_doc_categories: "Document categories" enumeration_work_package_priorities: "Work package priorities" filter: + more_values_not_shown: "There are %{total} more results, search to filter results." description: text_open_filter: "Open this filter with 'ALT' and arrow keys." text_close_filter: "To select an entry leave the focus for example by pressing enter. To leave without filter select the first (empty) entry." diff --git a/frontend/src/app/core/apiv3/paths/apiv3-resource.ts b/frontend/src/app/core/apiv3/paths/apiv3-resource.ts index a0d36fd8c9..479caab428 100644 --- a/frontend/src/app/core/apiv3/paths/apiv3-resource.ts +++ b/frontend/src/app/core/apiv3/paths/apiv3-resource.ts @@ -1,10 +1,15 @@ +/* eslint-disable max-classes-per-file */ + import { Constructor } from '@angular/cdk/table'; import { SimpleResource, SimpleResourceCollection } from 'core-app/core/apiv3/paths/path-resources'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service'; import { Observable } from 'rxjs'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; -import { ApiV3FilterBuilder } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder'; +import { + ApiV3Filter, + ApiV3FilterBuilder, +} from 'core-app/shared/helpers/api-v3/api-v3-filter-builder'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; @@ -98,7 +103,24 @@ export class ApiV3ResourceCollection> exte * @param params additional URL params to append */ public filtered>>(filters:ApiV3FilterBuilder, params:{ [key:string]:string } = {}, resourceClass?:Constructor):R { - return this.subResource(`?${filters.toParams(params)}`, resourceClass); + const url = new URL(this.path, window.location.origin); + + if (url.searchParams.has('filters')) { + const existingFilters = JSON.parse(url.searchParams.get('filters') as string) as ApiV3Filter[]; + url.searchParams.set('filters', JSON.stringify(existingFilters.concat(filters.filters))); + } else { + url.searchParams.set('filters', filters.toJson()); + } + + Object + .keys(params) + .forEach((key) => { + url.searchParams.set(key, params[key]); + }); + + const cls = resourceClass || ApiV3GettableResource; + // eslint-disable-next-line new-cap + return new cls(this.apiRoot, url.pathname, url.search, this) as R; } /** @@ -107,6 +129,7 @@ export class ApiV3ResourceCollection> exte * @param segment Additional segment to add to the current path */ protected subResource>(segment:string, cls:Constructor = ApiV3GettableResource as any):R { + // eslint-disable-next-line new-cap return new cls(this.apiRoot, this.path, segment, this); } } diff --git a/frontend/src/app/core/global_search/input/global-search-input.component.html b/frontend/src/app/core/global_search/input/global-search-input.component.html index 0dd24a4e60..232ac73239 100644 --- a/frontend/src/app/core/global_search/input/global-search-input.component.html +++ b/frontend/src/app/core/global_search/input/global-search-input.component.html @@ -23,7 +23,6 @@ [focusDirectly]="isFocusedDirectly" [model]="selectedItem" [searchFn]="customSearchFn" - [loading]="isLoading" (focus)="onFocus()" (blur)="onFocusOut()" (search)="search($event)" diff --git a/frontend/src/app/core/global_search/input/global-search-input.component.ts b/frontend/src/app/core/global_search/input/global-search-input.component.ts index ec1a2e571b..f30b5b9fd4 100644 --- a/frontend/src/app/core/global_search/input/global-search-input.component.ts +++ b/frontend/src/app/core/global_search/input/global-search-input.component.ts @@ -93,8 +93,6 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy { public markable = false; - public isLoading = false; - getAutocompleterData = (query:string):Observable => this.autocompleteWorkPackages(query); public autocompleterOptions = { @@ -278,14 +276,12 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy { const hashFreeQuery = this.queryWithoutHash(query); - this.isLoading = true; return this .fetchSearchResults(hashFreeQuery, hashFreeQuery !== query) .get() .pipe( map((collection) => this.searchResultsToOptions(collection.elements, hashFreeQuery)), tap(() => { - this.isLoading = false; this.setMarkedOption(); }), ); diff --git a/frontend/src/app/features/boards/board/board-filter/board-filter.component.html b/frontend/src/app/features/boards/board/board-filter/board-filter.component.html index d51da825e3..d582622de0 100644 --- a/frontend/src/app/features/boards/board/board-filter/board-filter.component.html +++ b/frontend/src/app/features/boards/board/board-filter/board-filter.component.html @@ -1 +1 @@ - + diff --git a/frontend/src/app/features/boards/board/inline-add/board-inline-add-autocompleter.component.ts b/frontend/src/app/features/boards/board/inline-add/board-inline-add-autocompleter.component.ts index b83fd014c5..ba3f4f4abc 100644 --- a/frontend/src/app/features/boards/board/inline-add/board-inline-add-autocompleter.component.ts +++ b/frontend/src/app/features/boards/board/inline-add/board-inline-add-autocompleter.component.ts @@ -66,13 +66,9 @@ export class BoardInlineAddAutocompleterComponent implements AfterViewInit { placeholder: this.I18n.t('js.relations_autocomplete.placeholder'), }; - // Whether we're currently loading - public isLoading = false; - getAutocompleterData = (searchString:string):Observable => { // Return when the search string is empty if (searchString.length === 0) { - this.isLoading = false; return of([]); } @@ -103,7 +99,6 @@ export class BoardInlineAddAutocompleterComponent implements AfterViewInit { this.notificationService.handleRawError(error); return of([]); }), - tap(() => this.isLoading = false), ); }; diff --git a/frontend/src/app/features/boards/board/inline-add/board-inline-add-autocompleter.html b/frontend/src/app/features/boards/board/inline-add/board-inline-add-autocompleter.html index 25b4775a66..3dfa5bdf1f 100644 --- a/frontend/src/app/features/boards/board/inline-add/board-inline-add-autocompleter.html +++ b/frontend/src/app/features/boards/board/inline-add/board-inline-add-autocompleter.html @@ -3,7 +3,6 @@ classes="wp-inline-create--reference-autocompleter" [closeOnSelect]="false" [placeholder]="text.placeholder" - [loading]="isLoading" [resource]="autocompleterOptions.resource" [getOptionsFn]="autocompleterOptions.getOptionsFn" (close)="cancel()" diff --git a/frontend/src/app/features/hal/resources/query-filter-instance-schema-resource.ts b/frontend/src/app/features/hal/resources/query-filter-instance-schema-resource.ts index edc157b7ff..0de0c6fec7 100644 --- a/frontend/src/app/features/hal/resources/query-filter-instance-schema-resource.ts +++ b/frontend/src/app/features/hal/resources/query-filter-instance-schema-resource.ts @@ -102,6 +102,10 @@ export class QueryFilterInstanceSchemaResource extends SchemaResource { return !!(this.values && this.values.allowedValues); } + public loadedAllowedValues():boolean { + return Array.isArray(this.values?.allowedValues); + } + public resultingSchema(operator:QueryOperatorResource):QueryFilterInstanceSchemaResource { const staticSchema = this.$source; const dependentSchema = this.dependency.forValue(operator.href!.toString()); diff --git a/frontend/src/app/features/team-planner/team-planner/assignee/add-assignee.component.ts b/frontend/src/app/features/team-planner/team-planner/assignee/add-assignee.component.ts index 3d83249921..9cc7ef6d34 100644 --- a/frontend/src/app/features/team-planner/team-planner/assignee/add-assignee.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/assignee/add-assignee.component.ts @@ -75,7 +75,7 @@ export class AddAssigneeComponent { filters.add('member', '=', [this.currentProjectService.id || '']); if (term) { - filters.add('name_and_identifier', '~', [term]); + filters.add('typeahead', '**', [term]); } return this diff --git a/frontend/src/app/features/work-packages/components/filters/filter-boolean-value/filter-boolean-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-boolean-value/filter-boolean-value.component.ts index 900ad38988..217f207bb1 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-boolean-value/filter-boolean-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-boolean-value/filter-boolean-value.component.ts @@ -34,7 +34,7 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; @Component({ - selector: 'filter-boolean-value', + selector: 'op-filter-boolean-value', templateUrl: './filter-boolean-value.component.html', }) export class FilterBooleanValueComponent { diff --git a/frontend/src/app/features/work-packages/components/filters/filter-container/filter-container.directive.ts b/frontend/src/app/features/work-packages/components/filters/filter-container/filter-container.directive.ts index 09daef4d42..4db3bf220b 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-container/filter-container.directive.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-container/filter-container.directive.ts @@ -47,7 +47,7 @@ import { WorkPackageFiltersService } from 'core-app/features/work-packages/compo @Component({ templateUrl: './filter-container.directive.html', changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'filter-container', + selector: 'op-filter-container', }) export class WorkPackageFilterContainerComponent extends UntilDestroyedMixin implements OnInit, OnDestroy { @Input('showFilterButton') showFilterButton = false; diff --git a/frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts index e64da1dab8..2bd8e0b9b6 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-date-time-value/filter-date-time-value.component.ts @@ -39,7 +39,7 @@ import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/que import { AbstractDateTimeValueController } from '../abstract-filter-date-time-value/abstract-filter-date-time-value.controller'; @Component({ - selector: 'filter-date-time-value', + selector: 'op-filter-date-time-value', templateUrl: './filter-date-time-value.component.html', }) export class FilterDateTimeValueComponent extends AbstractDateTimeValueController implements OnInit { diff --git a/frontend/src/app/features/work-packages/components/filters/filter-date-times-value/filter-date-times-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-date-times-value/filter-date-times-value.component.ts index 44b7a4e710..03b7b1887e 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-date-times-value/filter-date-times-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-date-times-value/filter-date-times-value.component.ts @@ -39,7 +39,7 @@ import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/que import { AbstractDateTimeValueController } from '../abstract-filter-date-time-value/abstract-filter-date-time-value.controller'; @Component({ - selector: 'filter-date-times-value', + selector: 'op-filter-date-times-value', templateUrl: './filter-date-times-value.component.html', }) export class FilterDateTimesValueComponent extends AbstractDateTimeValueController implements OnInit { diff --git a/frontend/src/app/features/work-packages/components/filters/filter-date-value/filter-date-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-date-value/filter-date-value.component.ts index cdb10b1818..ebd583b14c 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-date-value/filter-date-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-date-value/filter-date-value.component.ts @@ -36,7 +36,7 @@ import { TimezoneService } from 'core-app/core/datetime/timezone.service'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; @Component({ - selector: 'filter-date-value', + selector: 'op-filter-date-value', templateUrl: './filter-date-value.component.html', }) export class FilterDateValueComponent extends UntilDestroyedMixin { diff --git a/frontend/src/app/features/work-packages/components/filters/filter-dates-value/filter-dates-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-dates-value/filter-dates-value.component.ts index fa8d3829f9..5236f93832 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-dates-value/filter-dates-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-dates-value/filter-dates-value.component.ts @@ -37,7 +37,7 @@ import { TimezoneService } from 'core-app/core/datetime/timezone.service'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; @Component({ - selector: 'filter-dates-value', + selector: 'op-filter-dates-value', templateUrl: './filter-dates-value.component.html', }) export class FilterDatesValueComponent extends UntilDestroyedMixin { diff --git a/frontend/src/app/features/work-packages/components/filters/filter-integer-value/filter-integer-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-integer-value/filter-integer-value.component.ts index 245e792a58..72e5262efe 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-integer-value/filter-integer-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-integer-value/filter-integer-value.component.ts @@ -36,7 +36,7 @@ import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/que import { QueryFilterResource } from 'core-app/features/hal/resources/query-filter-resource'; @Component({ - selector: 'filter-integer-value', + selector: 'op-filter-integer-value', templateUrl: './filter-integer-value.component.html', }) export class FilterIntegerValueComponent extends UntilDestroyedMixin { diff --git a/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.html b/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.html index 214509e5e7..56ebc06e90 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.html +++ b/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.html @@ -1,25 +1,20 @@
- - +
diff --git a/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.ts index 8d246c5c62..a690482dae 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.ts @@ -1,127 +1,104 @@ import { NgSelectComponent } from '@ng-select/ng-select'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; -import { DebouncedRequestSwitchmap, errorNotificationHandler } from 'core-app/shared/helpers/rxjs/debounced-input-switchmap'; -import { Observable } from 'rxjs'; +import { + Observable, + of, +} from 'rxjs'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; import { ApiV3FilterBuilder } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder'; -import { map } from 'rxjs/operators'; +import { + map, + switchMap, + withLatestFrom, +} from 'rxjs/operators'; import { ApiV3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { ApiV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service'; -import { HalResourceSortingService } from 'core-app/features/hal/services/hal-resource-sorting.service'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; import { - AfterViewInit, ChangeDetectionStrategy, + ChangeDetectionStrategy, ChangeDetectorRef, - Component, EventEmitter, Input, NgZone, OnInit, Output, ViewChild, + Component, + EventEmitter, + Input, + NgZone, + Output, + ViewChild, } from '@angular/core'; -import { compareByHrefOrString } from 'core-app/shared/helpers/angular/tracking-functions'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { I18nService } from 'core-app/core/i18n/i18n.service'; +import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; +import { take } from 'rxjs/internal/operators/take'; @Component({ - selector: 'filter-searchable-multiselect-value', + selector: 'op-filter-searchable-multiselect-value', changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './filter-searchable-multiselect-value.component.html', }) -export class FilterSearchableMultiselectValueComponent extends UntilDestroyedMixin implements OnInit, AfterViewInit { +export class FilterSearchableMultiselectValueComponent extends UntilDestroyedMixin { @Input() public filter:QueryFilterInstanceResource; @Input() public shouldFocus = false; @Output() public filterChanged = new EventEmitter(); - private _isEmpty:boolean; - - public _availableOptions:HalResource[] = []; - - public compareByHrefOrString = compareByHrefOrString; - - public active:Set; - - public requests = new DebouncedRequestSwitchmap( - (searchTerm:string) => this.loadAvailable(searchTerm), - errorNotificationHandler(this.halNotification), - true, + private meValue = this.halResourceService.createHalResource( + { + _links: { + self: { + href: this.apiV3Service.users.me.path, + title: this.I18n.t('js.label_me'), + }, + }, + }, true, ); + autocompleterFn = (searchTerm:string):Observable => this.loadAvailable(searchTerm); + readonly text = { placeholder: this.I18n.t('js.placeholders.selection'), }; - public get value() { + public get value():string[]|HalResource[] { return this.filter.values; } - public get availableOptions() { - return this._availableOptions; - } - - public set availableOptions(val:HalResource[]) { - this._availableOptions = this.halSorting.sort(val); - } - - public get isEmpty():boolean { - return this._isEmpty = this.value.length === 0; - } - @ViewChild('ngSelectInstance', { static: true }) ngSelectInstance:NgSelectComponent; constructor(readonly halResourceService:HalResourceService, - readonly halSorting:HalResourceSortingService, readonly apiV3Service:ApiV3Service, readonly cdRef:ChangeDetectorRef, readonly I18n:I18nService, protected currentProject:CurrentProjectService, + protected currentUser:CurrentUserService, readonly halNotification:HalResourceNotificationService, readonly ngZone:NgZone) { super(); } - ngOnInit() { - this.initialization(); - // Request an empty value to load warning early on - this.requests.input$.next(''); - } - - ngAfterViewInit():void { - if (this.ngSelectInstance && this.shouldFocus) { - this.ngSelectInstance.focus(); - } - } - - initialization() { - this - .requests - .output$.pipe( - this.untilDestroyed(), - ) - .subscribe((values:HalResource[]) => { - this.availableOptions = values; - this.cdRef.detectChanges(); - }); - } - public loadAvailable(matching:string):Observable { const filters:ApiV3FilterBuilder = this.createFilters(matching); - const { href } = this.filter.currentSchema!.values!.allowedValues as any; + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + const { href } = this.filter.currentSchema!.values!.allowedValues as { href:string }; const filteredData = (this.apiV3Service.collectionFromString(href) as ApiV3ResourceCollection) - .filtered(filters) + .filtered(filters, { pageSize: '-1' }) .get() - .pipe(map((collection) => collection.elements)); + .pipe( + switchMap((collection) => this.withMeValue(matching, collection.elements)), + ); return filteredData; } - protected createFilters(matching:string) { + protected createFilters(matching:string):ApiV3FilterBuilder { const filters = new ApiV3FilterBuilder(); if (matching) { - filters.add('subjectOrId', '**', [matching]); + filters.add('typeahead', '**', [matching]); } return filters; @@ -130,20 +107,32 @@ export class FilterSearchableMultiselectValueComponent extends UntilDestroyedMix public setValues(val:any) { this.filter.values = val.length > 0 ? (Array.isArray(val) ? val : [val]) : [] as HalResource[]; this.filterChanged.emit(this.filter); - this.requests.input$.next(''); this.cdRef.detectChanges(); } - public repositionDropdown() { - if (this.ngSelectInstance) { - const component = (this.ngSelectInstance) as any; - if (component && component.dropdownPanel) { - this.ngZone.runOutsideAngular(() => { - setTimeout(() => { - component.dropdownPanel._updatePosition(); - }, 25); - }); - } + private withMeValue(matching:string, elements:HalResource[]):Observable { + if (!this.isUserResource || (!!matching && matching !== 'me')) { + return of(elements); } + + return this + .currentUser + .isLoggedIn$ + .pipe( + take(1), + withLatestFrom(this.currentUser.user$), + map(([logged, user]) => { + if (logged && user) { + return [this.meValue].concat(elements); + } + + return elements; + }), + ); + } + + private get isUserResource() { + const type = _.get(this.filter.currentSchema, 'values.type', null) as string; + return type && type.indexOf('User') > 0; } } diff --git a/frontend/src/app/features/work-packages/components/filters/filter-string-value/filter-string-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-string-value/filter-string-value.component.ts index 82b1f28c65..a9d3fd014e 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-string-value/filter-string-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-string-value/filter-string-value.component.ts @@ -35,7 +35,7 @@ import { componentDestroyed } from '@w11k/ngx-componentdestroyed'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; @Component({ - selector: 'filter-string-value', + selector: 'op-filter-string-value', templateUrl: './filter-string-value.component.html', }) export class FilterStringValueComponent extends UntilDestroyedMixin { diff --git a/frontend/src/app/features/work-packages/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.html b/frontend/src/app/features/work-packages/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.html index 7cf41ef463..311c4e8b30 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.html +++ b/frontend/src/app/features/work-packages/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.html @@ -1,24 +1,18 @@
- - +
diff --git a/frontend/src/app/features/work-packages/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts index bc70d1e799..a48dbaba95 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts @@ -27,7 +27,6 @@ //++ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; -import { UserResource } from 'core-app/features/hal/resources/user-resource'; import { AfterViewInit, ChangeDetectionStrategy, @@ -40,19 +39,16 @@ import { ViewChild, } from '@angular/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { compareByHrefOrString } from 'core-app/shared/helpers/angular/tracking-functions'; import { HalResourceSortingService } from 'core-app/features/hal/services/hal-resource-sorting.service'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { NgSelectComponent } from '@ng-select/ng-select'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; -import { RootResource } from 'core-app/features/hal/resources/root-resource'; -import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service'; @Component({ - selector: 'filter-toggled-multiselect-value', + selector: 'op-filter-toggled-multiselect-value', changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './filter-toggled-multiselect-value.component.html', }) @@ -65,11 +61,7 @@ export class FilterToggledMultiselectValueComponent implements OnInit, AfterView @ViewChild('ngSelectInstance', { static: true }) ngSelectInstance:NgSelectComponent; - public _availableOptions:HalResource[] = []; - - public compareByHrefOrString = compareByHrefOrString; - - private _isEmpty:boolean; + public availableOptions:HalResource[] = []; readonly text = { placeholder: this.I18n.t('js.placeholders.selection'), @@ -84,8 +76,10 @@ export class FilterToggledMultiselectValueComponent implements OnInit, AfterView readonly I18n:I18nService) { } - ngOnInit() { - this.fetchAllowedValues(); + ngOnInit():void { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + const values = (this.filter.currentSchema!.values!.allowedValues as HalResource[]); + this.availableOptions = this.halSorting.sort(values); } ngAfterViewInit():void { @@ -94,93 +88,13 @@ export class FilterToggledMultiselectValueComponent implements OnInit, AfterView } } - public get value() { + public get value():unknown[] { return this.filter.values; } - public setValues(val:any) { - this.filter.values = _.castArray(val); + public setValues(val:HalResource[]|string[]|string|HalResource):void { + this.filter.values = _.castArray(val) as HalResource[]|string[]; this.filterChanged.emit(this.filter); this.cdRef.detectChanges(); } - - public get availableOptions() { - return this._availableOptions; - } - - public set availableOptions(val:HalResource[]) { - this._availableOptions = this.halSorting.sort(val); - } - - public get isEmpty():boolean { - return this._isEmpty = this.value.length === 0; - } - - public repositionDropdown() { - if (this.ngSelectInstance) { - setTimeout(() => { - const component = (this.ngSelectInstance) as any; - if (component && component.dropdownPanel) { - component.dropdownPanel._updatePosition(); - } - }, 25); - } - } - - private get isUserResource() { - const type = _.get(this.filter.currentSchema, 'values.type', null); - return type && type.indexOf('User') > 0; - } - - private fetchAllowedValues() { - if ((this.filter.currentSchema!.values!.allowedValues as CollectionResource).$load) { - this.loadAllowedValues(); - } else { - this.availableOptions = (this.filter.currentSchema!.values!.allowedValues as HalResource[]); - } - } - - private loadAllowedValues() { - const valuesSchema = this.filter.currentSchema!.values!; - const loadingPromises = [(valuesSchema.allowedValues as any).$load()]; - - // If it is a User resource, we want to have the 'me' option. - // We therefore fetch the current user from the api and copy - // the current user's value from the set of allowedValues. The - // copy will have it's name altered to 'me' and will then be - // prepended to the list. - if (this.isUserResource) { - loadingPromises.push(this.apiV3Service.root.get().toPromise()); - } - - Promise.all(loadingPromises) - .then(((resources:Array) => { - const options = (resources[0] as CollectionResource).elements; - - this.availableOptions = options; - - if (this.isUserResource && this.filter.filter.id !== 'memberOfGroup') { - this.addMeValue((resources[1] as RootResource).user); - } - })); - } - - private addMeValue(currentUser:UserResource) { - if (!(currentUser && currentUser.href)) { - return; - } - - const me:HalResource = this.halResourceService.createHalResource( - { - _links: { - self: { - href: this.apiV3Service.users.me.path, - title: this.I18n.t('js.label_me'), - }, - }, - }, true, - ); - - this._availableOptions.unshift(me); - } } diff --git a/frontend/src/app/features/work-packages/components/filters/query-filter/query-filter.component.html b/frontend/src/app/features/work-packages/components/filters/query-filter/query-filter.component.html index c6c2d30069..f55d1e5afd 100644 --- a/frontend/src/app/features/work-packages/components/filters/query-filter/query-filter.component.html +++ b/frontend/src/app/features/work-packages/components/filters/query-filter/query-filter.component.html @@ -38,78 +38,79 @@ - + >
- + > - + > - + > - + > - + > - + > - + > - + - - + + > +
diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts index cacb87bbad..730808314b 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts @@ -84,13 +84,9 @@ export class WorkPackageRelationsAutocompleteComponent { @Output() onEmptySelected = new EventEmitter(); - // Whether we're currently loading - public isLoading = false; - getAutocompleterData = (query:string|null):Observable => { // Return when the search string is empty if (query === null || query.length === 0) { - this.isLoading = false; return of([]); } @@ -107,7 +103,6 @@ export class WorkPackageRelationsAutocompleteComponent { this.notificationService.handleRawError(error); return of([]); }), - tap(() => this.isLoading = false), ); }; diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.html b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.html index 1a9482f03d..6d521be32a 100644 --- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.html +++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.html @@ -4,7 +4,6 @@ [appendTo]="appendToContainer" [placeholder]="inputPlaceholder" [typeToSearchText]="inputPlaceholder" - [loading]="isLoading" [resource]="autocompleterOptions.resource" [defaultData]="false" (open)="onOpen()" diff --git a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.html b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.html index 541970eb0c..5b50e97c52 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.html +++ b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.html @@ -7,11 +7,11 @@ - - + @@ -174,19 +175,22 @@ - + {{ item.name }} - - + + {{item.type?.name }} #{{ item.id }} {{ item.subject }} - - {{ item.name }} + + × + {{item.name}} \ No newline at end of file diff --git a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts index 63065b0d30..1dda18576e 100644 --- a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts +++ b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts @@ -1,28 +1,40 @@ +/* We just forward the ng-select outputs without renaming */ +/* eslint-disable @angular-eslint/no-output-native */ import { AfterViewInit, - OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, EventEmitter, + HostBinding, Input, NgZone, + OnChanges, Output, + SimpleChanges, TemplateRef, ViewChild, - SimpleChanges, - HostBinding, } from '@angular/core'; -import { DropdownPosition, NgSelectComponent } from '@ng-select/ng-select'; import { - Observable, + DropdownPosition, + NgSelectComponent, +} from '@ng-select/ng-select'; +import { + BehaviorSubject, + merge, NEVER, + Observable, of, Subject, - merge, } from 'rxjs'; -import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; +import { + debounceTime, + distinctUntilChanged, + filter, + switchMap, + tap, +} from 'rxjs/operators'; import { GroupValueFn } from '@ng-select/ng-select/lib/ng-select.component'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; @@ -85,7 +97,7 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft @Input() public items?:IOPAutocompleterOption[]|HalResource[]; - private items$ = new Subject(); + private items$ = new BehaviorSubject(null); @Input() public clearSearchOnAdd?:boolean = true; @@ -101,15 +113,15 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft @Input() public markFirst ? = true; - @Input() public placeholder?:string = this.I18n.t('js.autocompleter.placeholder'); + @Input() public placeholder:string = this.I18n.t('js.autocompleter.placeholder'); - @Input() public notFoundText?:string = this.I18n.t('js.autocompleter.notFoundText'); + @Input() public notFoundText:string = this.I18n.t('js.autocompleter.notFoundText'); - @Input() public typeToSearchText?:string = this.I18n.t('js.autocompleter.typeToSearchText'); + @Input() public typeToSearchText:string = this.I18n.t('js.autocompleter.typeToSearchText'); @Input() public addTagText?:string; - @Input() public loadingText?:string; + @Input() public loadingText:string = this.I18n.t('js.ajax.loading'); @Input() public clearAllText?:string; @@ -119,8 +131,6 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft @Input() public appendTo?:string; - @Input() public loading?:boolean = false; - @Input() public closeOnSelect?:boolean = true; @Input() public hideSelected?:boolean = false; @@ -131,7 +141,7 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft @Input() public maxSelectedItems?:number; - @Input() public groupBy?:string | Function; + @Input() public groupBy?:string|(() => string); @Input() public groupValue?:GroupValueFn; @@ -163,28 +173,28 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft @Input() public editableSearchTerm?:boolean = false; - @Input() public keyDownFn ? = (_:KeyboardEvent) => true; + @Input() public keyDownFn ? = ():boolean => true; - @Input() public typeahead:Subject = new Subject(); + @Input() public typeahead:BehaviorSubject = new BehaviorSubject(null); // a function for setting the options of ng-select - @Input() public getOptionsFn:(searchTerm:string) => any; + @Input() public getOptionsFn:(searchTerm:string) => Observable; - @Output() public open = new EventEmitter(); + @Output() public open = new EventEmitter(); - @Output() public close = new EventEmitter(); + @Output() public close = new EventEmitter(); - @Output() public change = new EventEmitter(); + @Output() public change = new EventEmitter(); - @Output() public focus = new EventEmitter(); + @Output() public focus = new EventEmitter(); - @Output() public blur = new EventEmitter(); + @Output() public blur = new EventEmitter(); - @Output() public search = new EventEmitter<{ term:string, items:any[] }>(); + @Output() public search = new EventEmitter<{ term:string, items:unknown[] }>(); - @Output() public keydown = new EventEmitter(); + @Output() public keydown = new EventEmitter(); - @Output() public clear = new EventEmitter(); + @Output() public clear = new EventEmitter(); @Output() public add = new EventEmitter(); @@ -198,9 +208,9 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft public active:Set; - public results$:any; + public results$:Observable; - public isLoading = false; + public loading$ = new Subject(); @ViewChild('ngSelectInstance') ngSelectInstance:NgSelectComponent; @@ -225,7 +235,7 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft super(); } - ngOnChanges(changes:SimpleChanges) { + ngOnChanges(changes:SimpleChanges):void { if (changes.items) { this.items$.next(changes.items.currentValue); } @@ -239,24 +249,35 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft this.ngZone.runOutsideAngular(() => { setTimeout(() => { this.results$ = merge( - (this.items$ || new Subject()), + this.items$, this.typeahead.pipe( + filter(() => !!(this.defaultData || this.getOptionsFn)), + filter((val) => val !== null), distinctUntilChanged(), debounceTime(250), - (this.defaultData - ? switchMap((queryString) => this.opAutocompleterService.loadData(queryString, this.resource, this.filters, this.searchKey)) - : this.getOptionsFn - ? switchMap((queryString) => this.getOptionsFn(queryString)) - : switchMap(() => NEVER) + tap(() => this.loading$.next(true)), + switchMap((queryString:string) => { + if (this.defaultData) { + return this.opAutocompleterService.loadData(queryString, this.resource, this.filters, this.searchKey); + } + + if (this.getOptionsFn) { + return this.getOptionsFn(queryString); + } + + return NEVER; + }), + tap( + () => this.loading$.next(false), + () => this.loading$.next(false), ), ), ); if (this.fetchDataDirectly) { - this.results$ = this.defaultData - ? (this.opAutocompleterService.loadData('', this.resource, this.filters, this.searchKey)) - : (this.getOptionsFn('')); + this.typeahead.next(''); } + if (this.openDirectly) { this.ngSelectInstance.open(); this.ngSelectInstance.focus(); diff --git a/frontend/src/app/shared/components/autocompleter/te-work-package-autocompleter/te-work-package-autocompleter.component.html b/frontend/src/app/shared/components/autocompleter/te-work-package-autocompleter/te-work-package-autocompleter.component.html index 26a20361cd..c7d7e48d57 100644 --- a/frontend/src/app/shared/components/autocompleter/te-work-package-autocompleter/te-work-package-autocompleter.component.html +++ b/frontend/src/app/shared/components/autocompleter/te-work-package-autocompleter/te-work-package-autocompleter.component.html @@ -12,7 +12,6 @@ [typeahead]="typeahead" [appendTo]="appendTo" [hideSelected]="hideSelected" - [loading]="finishedLoading | async" [id]="id" (change)="changeModel($event)" (open)="opened()" diff --git a/frontend/src/app/shared/components/autocompleter/te-work-package-autocompleter/te-work-package-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/te-work-package-autocompleter/te-work-package-autocompleter.component.ts index dc16cd7d13..ae4e138f60 100644 --- a/frontend/src/app/shared/components/autocompleter/te-work-package-autocompleter/te-work-package-autocompleter.component.ts +++ b/frontend/src/app/shared/components/autocompleter/te-work-package-autocompleter/te-work-package-autocompleter.component.ts @@ -27,10 +27,13 @@ //++ import { - AfterViewInit, Component, EventEmitter, Injector, Output, ViewEncapsulation, + AfterViewInit, + Component, + EventEmitter, + Injector, + Output, + ViewEncapsulation, } from '@angular/core'; -import { of } from 'rxjs'; -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { WorkPackageAutocompleterComponent } from 'core-app/shared/components/autocompleter/work-package-autocompleter/wp-autocompleter.component'; export type TimeEntryWorkPackageAutocompleterMode = 'all'|'recent'; @@ -55,8 +58,6 @@ export class TimeEntryWorkPackageAutocompleterComponent extends WorkPackageAutoc this.text.recent = this.I18n.t('js.label_recent'); } - public loading = false; - public mode:TimeEntryWorkPackageAutocompleterMode = 'all'; public setMode(value:TimeEntryWorkPackageAutocompleterMode) { diff --git a/frontend/src/global_styles/content/_advanced_filters.sass b/frontend/src/global_styles/content/_advanced_filters.sass index e230391636..1965dafaa5 100644 --- a/frontend/src/global_styles/content/_advanced_filters.sass +++ b/frontend/src/global_styles/content/_advanced_filters.sass @@ -96,6 +96,9 @@ $advanced-filters--grid-gap: 10px .advanced-filters--select @extend .form--select +.advanced-filters--ng-select + width: 100% + .advanced-filters--affix @extend .form--field-affix @include form--field-affix-mixin--transparent diff --git a/frontend/src/global_styles/content/_autocomplete.sass b/frontend/src/global_styles/content/_autocomplete.sass index 9db10bc825..ff477a967b 100644 --- a/frontend/src/global_styles/content/_autocomplete.sass +++ b/frontend/src/global_styles/content/_autocomplete.sass @@ -183,6 +183,12 @@ mark.ui-autocomplete-match .ng-dropdown-panel z-index: 9500 !important + // Overrides for the ng-footer-tmp option + .ng-footer-text + font-size: 0.9rem + padding: 5px + font-style: italic + .ng-option-label vertical-align: top @@ -190,7 +196,7 @@ mark.ui-autocomplete-match font-size: 14px line-height: 22px - .op-avatar + .op-avatar margin-right: 8px .work-package-table--container .ng-dropdown-panel diff --git a/lib/api/decorators/offset_paginated_collection.rb b/lib/api/decorators/offset_paginated_collection.rb index ead34ed1cf..499320a575 100644 --- a/lib/api/decorators/offset_paginated_collection.rb +++ b/lib/api/decorators/offset_paginated_collection.rb @@ -41,7 +41,8 @@ module API @self_link_base = self_link @query = query @page = page.to_i > 0 ? page.to_i : 1 - @per_page = resulting_page_size(per_page, models) + resolved_page_size = resolve_page_size(per_page) + @per_page = resulting_page_size(resolved_page_size, models) full_self_link = make_page_link(page: @page, page_size: @per_page) paged = paged_models(models) diff --git a/modules/dashboards/spec/features/work_package_table_spec.rb b/modules/dashboards/spec/features/work_package_table_spec.rb index 0a0cc724bf..6c589dab30 100644 --- a/modules/dashboards/spec/features/work_package_table_spec.rb +++ b/modules/dashboards/spec/features/work_package_table_spec.rb @@ -102,7 +102,7 @@ describe 'Arbitrary WorkPackage query table widget dashboard', type: :feature, j # At the beginning, the default query is displayed expect(filter_area.area) - .to have_selector('.subject', text: type_work_package.subject) + .to have_selector('.subject', text: type_work_package.subject, wait: 10) expect(filter_area.area) .to have_selector('.subject', text: other_type_work_package.subject) diff --git a/spec/features/work_packages/table/queries/me_filter_spec.rb b/spec/features/work_packages/table/queries/me_filter_spec.rb index 2d331b038c..649df15714 100644 --- a/spec/features/work_packages/table/queries/me_filter_spec.rb +++ b/spec/features/work_packages/table/queries/me_filter_spec.rb @@ -114,6 +114,10 @@ describe 'filter me value', js: true do # Expect new work packages receive assignee split_screen = wp_table.create_wp_by_button wp_user.type + + # Wait a bit for the page to load + sleep 2 + subject = split_screen.edit_field :subject subject.set_value 'foobar' subject.submit_by_enter diff --git a/spec/models/queries/users/filters/any_name_attribute_filter_spec.rb b/spec/models/queries/users/filters/any_name_attribute_filter_spec.rb index 79ff2fd07a..eef564d6de 100644 --- a/spec/models/queries/users/filters/any_name_attribute_filter_spec.rb +++ b/spec/models/queries/users/filters/any_name_attribute_filter_spec.rb @@ -50,7 +50,9 @@ describe Queries::Users::Filters::AnyNameAttributeFilter, type: :model do describe '#available_operators' do it 'supports = and !' do expect(instance.available_operators) - .to eql [Queries::Operators::Contains, Queries::Operators::NotContains] + .to contain_exactly Queries::Operators::Contains, + Queries::Operators::NotContains, + Queries::Operators::Everywhere end end end diff --git a/spec/support/components/work_packages/filters.rb b/spec/support/components/work_packages/filters.rb index 22945dc7b4..3958150fae 100644 --- a/spec/support/components/work_packages/filters.rb +++ b/spec/support/components/work_packages/filters.rb @@ -170,7 +170,7 @@ module Components Array(value).each do |val| select_autocomplete page.find("#filter_#{id}"), query: val, - results_selector: '.advanced-filters--ng-select .ng-dropdown-panel-items' + results_selector: '.ng-dropdown-panel-items' end else within_values(id) do