Skip to content

Commit

Permalink
Merge pull request #14718 from opf/feature/52119-make-renamed-attribu…
Browse files Browse the repository at this point in the history
…tes-searchable-with-old-names

[#52119] Make renamed progress attributes searchable with old names
  • Loading branch information
cbliard committed Feb 7, 2024
2 parents d96eaa6 + 8487978 commit dd4ccfe
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 2 deletions.
6 changes: 6 additions & 0 deletions config/locales/js-en.yml
Expand Up @@ -1043,13 +1043,17 @@ en:
createdAt: "Created on"
description: "Description"
date: "Date"
percentComplete: "% Complete"
percentCompleteAlternative: "Progress"
dueDate: "Finish date"
duration: "Duration"
spentTime: "Spent time"
category: "Category"
percentageDone: "Percentage done"
priority: "Priority"
projectName: "Project"
remainingWork: "Remaining work"
remainingWorkAlternative: "Remaining hours"
responsible: "Responsible"
startDate: "Start date"
status: "Status"
Expand All @@ -1060,6 +1064,8 @@ en:
updatedAt: "Updated on"
versionName: "Version"
version: "Version"
work: "Work"
workAlternative: "Estimated time"
remainingTime: "Remaining work"
default_queries:
latest_activity: "Latest activity"
Expand Down
Expand Up @@ -61,6 +61,7 @@
[closeOnSelect]="true"
[virtualScroll]="true"
[placeholder]="text.please_select"
[searchFn]="searchFunction"
(open)="onOpen()"
(change)="onFilterAdded($event)"
id="add_filter_select"
Expand Down
Expand Up @@ -49,6 +49,7 @@ import { QueryFilterResource } from 'core-app/features/hal/resources/query-filte
import { WorkPackageViewBaselineService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-baseline.service';
import { combineLatestWith } from 'rxjs';
import { repositionDropdownBugfix } from 'core-app/shared/components/autocompleter/op-autocompleter/autocompleter.helper';
import { AlternativeSearchService } from 'core-app/shared/components/work-packages/alternative-search.service';

const ADD_FILTER_SELECT_INDEX = -1;

Expand Down Expand Up @@ -94,6 +95,7 @@ export class QueryFiltersComponent extends UntilDestroyedMixin implements OnInit
readonly wpTableBaseline:WorkPackageViewBaselineService,
readonly wpFiltersService:WorkPackageFiltersService,
readonly I18n:I18nService,
readonly alternativeSearchService:AlternativeSearchService,
readonly cdRef:ChangeDetectorRef,
) {
super();
Expand Down Expand Up @@ -187,4 +189,8 @@ export class QueryFiltersComponent extends UntilDestroyedMixin implements OnInit
public onOpen() {
repositionDropdownBugfix(this.ngSelectComponent);
}

searchFunction = (term:string, currentItem:QueryFilterResource):boolean => {
return this.alternativeSearchService.searchFunction(term, currentItem);
};
}
Expand Up @@ -27,6 +27,7 @@
[placeholder]="text.placeholder"
[clearSearchOnAdd]="true"
[closeOnSelect]="true"
[searchFn]="searchFunction"
(open)="opened()"
(change)="select($event)"
></ng-select>
Expand Down
Expand Up @@ -16,6 +16,8 @@ import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destr
import { merge } from 'rxjs';
import { setBodyCursor } from 'core-app/shared/helpers/dom/set-window-cursor.helper';
import { repositionDropdownBugfix } from 'core-app/shared/components/autocompleter/op-autocompleter/autocompleter.helper';
import { QueryFilterResource } from 'core-app/features/hal/resources/query-filter-resource';
import { AlternativeSearchService } from 'core-app/shared/components/work-packages/alternative-search.service';

export interface DraggableOption {
name:string;
Expand Down Expand Up @@ -54,8 +56,11 @@ export class DraggableAutocompleteComponent extends UntilDestroyedMixin implemen
placeholder: this.I18n.t('js.label_add_columns'),
};

constructor(readonly I18n:I18nService,
readonly dragula:DragulaService) {
constructor(
readonly I18n:I18nService,
readonly dragula:DragulaService,
readonly alternativeSearchService:AlternativeSearchService,
) {
super();
}

Expand Down Expand Up @@ -137,6 +142,10 @@ export class DraggableAutocompleteComponent extends UntilDestroyedMixin implemen
repositionDropdownBugfix(this.ngSelectComponent);
}

searchFunction = (term:string, currentItem:QueryFilterResource):boolean => {
return this.alternativeSearchService.searchFunction(term, currentItem);
};

private updateAvailableOptions() {
this.availableOptions = this.options
.filter((item) => !this.selected.find((selected) => selected.id === item.id));
Expand Down
@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { QueryFilterResource } from 'core-app/features/hal/resources/query-filter-resource';

@Injectable({ providedIn: 'root' })

export class AlternativeSearchService {
constructor(
readonly I18n:I18nService,
) { }

private specialSearchStrings = {
percentComplete: this.I18n.t('js.work_packages.properties.percentComplete'),
percentCompleteAlternative: this.I18n.t('js.work_packages.properties.percentCompleteAlternative'),
work: this.I18n.t('js.work_packages.properties.work'),
workAlternative: this.I18n.t('js.work_packages.properties.workAlternative'),
remainingWork: this.I18n.t('js.work_packages.properties.remainingWork'),
remainingWorkAlternative: this.I18n.t('js.work_packages.properties.remainingWorkAlternative'),
};

private alternativeNames:{ [index:string]:string } = {
[this.specialSearchStrings.percentCompleteAlternative]: this.specialSearchStrings.percentComplete,
[this.specialSearchStrings.workAlternative]: this.specialSearchStrings.work,
[this.specialSearchStrings.remainingWorkAlternative]: this.specialSearchStrings.remainingWork,
};

public searchFunction = (term:string, currentItem:QueryFilterResource):boolean => {
const lowercaseSearchTerm = term.toLowerCase();
const lowercaseCurrentItemName = currentItem.name.toLowerCase();

const alternativeMatch = Object
.keys(this.alternativeNames)
.some((alternativeName) => {
return alternativeName.toLowerCase().includes(lowercaseSearchTerm)
&& currentItem.name === this.alternativeNames[alternativeName];
});

return lowercaseCurrentItemName.includes(lowercaseSearchTerm)
|| alternativeMatch;
};
}
@@ -0,0 +1,84 @@
# frozen_string_literal: true

# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2024 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.
# ++

require 'spec_helper'

RSpec.describe "Alternative name autocompleting",
:js,
:with_cuprite do
let!(:project) { create(:project_with_types) }
let!(:work_package) { create(:work_package, project:) }
let!(:user) { create(:admin) }

let(:work_packages_page) { Pages::WorkPackagesTable.new(project) }
let(:filters) { Components::WorkPackages::Filters.new }
let(:columns) { Components::WorkPackages::Columns.new }

before { login_as(user) }

describe 'Work package filter autocompleter' do
it 'allows discovering attributes by alternative names' do
work_packages_page.visit!
work_packages_page.expect_work_package_listed(work_package)

filters.open!

# Exact alternative match
filters.expect_alternative_available_filter('Progress', '% Complete')
# Partial alternative match
filters.expect_alternative_available_filter('ime', 'Work')
# Exact "real" match
filters.expect_alternative_available_filter('% Complete', '% Complete')
# Partial "real" match
filters.expect_alternative_available_filter('ork', 'Work')
# Indifferent to casing
filters.expect_alternative_available_filter('% comp', '% Complete')
end
end

describe 'Work package view configuration form' do
it 'allows discovering attributes by alternative names' do
work_packages_page.visit!
work_packages_page.expect_work_package_listed(work_package)

columns.open_modal
# Exact alternative match
columns.expect_alternative_available_column('Progress', '% Complete')
# Partial alternative match
columns.expect_alternative_available_column('ime', 'Work')
# Exact "real" match
columns.expect_alternative_available_column('% Complete', '% Complete')
# Partial "real" match
columns.expect_alternative_available_column('hours', 'Remaining work')
# Indifferent to casing
columns.expect_alternative_available_column('% comp', '% Complete')
end
end
end
13 changes: 13 additions & 0 deletions spec/support/components/work_packages/columns.rb
Expand Up @@ -70,6 +70,19 @@ def expect_column_available(name)
close_autocompleter
end

def expect_alternative_available_column(search_term, displayed_name)
column_autocompleter.click

autocompleter_input = column_autocompleter.find('input')

autocompleter_input.set(search_term)

expect(page)
.to have_css('.ng-dropdown-panel .ng-option-label', text: displayed_name)

autocompleter_input.set(search_term)
end

def add(name, save_changes: true)
open_modal unless modal_open?

Expand Down
15 changes: 15 additions & 0 deletions spec/support/components/work_packages/filters.rb
Expand Up @@ -48,6 +48,11 @@ def open
end
end

def open!
open
expect_open
end

def expect_filter_count(num)
expect(filter_button).to have_css('.badge', text: num, wait: 10)
end
Expand Down Expand Up @@ -96,6 +101,16 @@ def expect_available_filter(name, present: true)
input.set ""
end

def expect_alternative_available_filter(search_term, displayed_name)
input = page.find('.advanced-filters--add-filter-value input')
input.set(search_term)

expect(page)
.to have_css('.ng-dropdown-panel .ng-option-label', text: displayed_name)

input.set('')
end

def expect_loaded
SeleniumHubWaiter.wait
expect(filter_button).to have_css('.badge', wait: 2, visible: :all)
Expand Down

0 comments on commit dd4ccfe

Please sign in to comment.