Merge pull request #7435 from opf/housekeeping/30453-Replace-last-occurrence-of-toggled-multi-select-with-ng-select

[30453] Replace last occurrence of toggled multi-select with ng-select

[ci skip]
pull/7438/head
Oliver Günther 5 years ago committed by GitHub
commit 8a75460489
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      app/assets/stylesheets/content/work_packages/_table_configuration_modal.sass
  2. 48
      frontend/src/app/components/wp-table/configuration-modal/tabs/highlighting-tab.component.html
  3. 88
      frontend/src/app/components/wp-table/configuration-modal/tabs/highlighting-tab.component.ts
  4. 69
      frontend/src/app/modules/common/multi-toggled-select/multi-toggled-select.component.html
  5. 143
      frontend/src/app/modules/common/multi-toggled-select/multi-toggled-select.component.ts
  6. 6
      frontend/src/app/modules/common/openproject-common.module.ts
  7. 26
      spec/support/components/work_packages/table_configuration/highlighting.rb

@ -6,6 +6,10 @@
float: left
margin-right: 20px
&.-multi-line
margin-bottom: 0
line-height: 40px
input
margin-top: 0px
@ -19,5 +23,5 @@
.ee-attribute-highlighting-upsale
margin-bottom: 1.5rem
multi-toggled-select
display: inline-block
ng-select
width: fit-content

@ -9,21 +9,28 @@
<p [textContent]="text.highlighting_mode.description"></p>
<div class="form--field -full-width">
<div class="form--field-container">
<label class="option-label">
<label class="option-label -multi-line">
<input type="radio"
[attr.disabled]="disabledValue(eeShowBanners)"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="inline"
name="highlighting_mode_switch">
{{ text.highlighting_mode.inline }}
&nbsp;
<multi-toggled-select [isDisabled]="highlightingMode !== 'inline'"
[availableOptions]="availableMappedHighlightedAttributes"
[initialSelection]="selectedAttributes"
(onValueChange)="selectedAttributes = $event">
</multi-toggled-select>
<span [textContent]="text.highlighting_mode.inline"></span>&nbsp;
</label>
<ng-select [items]="availableInlineHighlightedAttributes"
[(ngModel)]="selectedAttributes"
[multiple]="true"
[disabled]="highlightingMode !== 'inline'"
[clearable]="false"
[closeOnSelect]="false"
(change)="updateHighlightingAttributes($event)"
class="-multi-select"
bindLabel="name"
name="highlighting_attributes"
appendTo="body">
</ng-select>
</div>
</div>
<div class="form--field -full-width">
@ -36,20 +43,19 @@
[value]="true"
name="entire_row_switch">
<span [textContent]="text.highlighting_mode.entire_row_by"></span>
&ngsp;
<select (change)="updateMode($event.target.value)"
[attr.disabled]="disabledValue(eeShowBanners)"
id="selected_attribute"
name="selected_attribute"
class="form--select form--inline-select">
<option [textContent]="text.highlighting_mode.status"
[selected]="lastEntireRowAttribute === 'status'"
value="status"></option>
<option [textContent]="text.highlighting_mode.priority"
[selected]="lastEntireRowAttribute === 'priority'"
value="priority"></option>
</select>
</label>
<ng-select [items]="availableRowHighlightedAttributes"
[(ngModel)]="lastEntireRowAttribute"
[disabled]="disabledValue(eeShowBanners)"
[clearable]="false"
(change)="updateMode($event.value)"
bindLabel="name"
bindValue="value"
name="selected_attribute"
appendTo="body"
id="selected_attribute">
</ng-select>
</div>
</div>
<div class="form--field -full-width">

@ -3,10 +3,8 @@ import {TabComponent} from 'core-components/wp-table/configuration-modal/tab-por
import {WorkPackageTableHighlightingService} from 'core-components/wp-fast-table/state/wp-table-highlighting.service';
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {HighlightingMode} from "core-components/wp-fast-table/builders/highlighting/highlighting-mode.const";
import {MultiToggledSelectOption} from "core-app/modules/common/multi-toggled-select/multi-toggled-select.component";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {States} from "core-app/components/states.service";
import {WorkPackageTableHighlight} from "core-components/wp-fast-table/wp-table-highlight";
import {BannersService} from "core-app/modules/common/enterprise/banners.service";
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
@ -21,9 +19,10 @@ export class WpTableConfigurationHighlightingTab implements TabComponent {
public lastEntireRowAttribute:HighlightingMode = 'status';
public eeShowBanners:boolean = false;
public availableMappedHighlightedAttributes:MultiToggledSelectOption[] = [];
public availableInlineHighlightedAttributes:HalResource[] = [];
public selectedAttributes:any[] = [];
public selectedAttributes:MultiToggledSelectOption[] = [];
public availableRowHighlightedAttributes:{name:string; value:HighlightingMode}[] = [];
public text = {
title: this.I18n.t('js.work_packages.table_configuration.highlighting'),
@ -49,28 +48,26 @@ export class WpTableConfigurationHighlightingTab implements TabComponent {
readonly wpTableHighlight:WorkPackageTableHighlightingService) {
}
public onSave() {
let mode = this.highlightingMode;
let highlightedAttributes:HalResource[] = this.selectedAttributesAsHal();
this.wpTableHighlight.update({ mode: mode, selectedAttributes: highlightedAttributes });
}
ngOnInit() {
this.availableInlineHighlightedAttributes = this.availableHighlightedAttributes;
this.availableRowHighlightedAttributes = [
{name: this.text.highlighting_mode.status, value: 'status'},
{name: this.text.highlighting_mode.priority, value: 'priority'},
];
private selectedAttributesAsHal() {
if (this.isAllOptionSelected()) {
return [];
} else {
return this.multiToggleValuesToHal(this.selectedAttributes);
}
}
this.setSelectedValues();
this.eeShowBanners = this.Banners.eeShowBanners;
this.updateMode(this.wpTableHighlight.current.mode);
private multiToggleValuesToHal(values:MultiToggledSelectOption[]) {
return values.map(el => {
return _.find(this.availableHighlightedAttributes, (column) => column.href === el.value)!;
});
if (this.eeShowBanners) {
this.updateMode('none');
}
}
private isAllOptionSelected() {
return this.selectedAttributes.length === 1 && _.get(this.selectedAttributes[0], 'value') === 'all';
public onSave() {
let mode = this.highlightingMode;
this.wpTableHighlight.update({ mode: mode, selectedAttributes: this.selectedAttributes });
}
public updateMode(mode:HighlightingMode | 'entire-row') {
@ -80,7 +77,7 @@ export class WpTableConfigurationHighlightingTab implements TabComponent {
this.highlightingMode = mode;
}
if (['status', 'priority', 'type'].indexOf(this.highlightingMode) !== -1) {
if (['status', 'priority'].indexOf(this.highlightingMode) !== -1) {
this.lastEntireRowAttribute = this.highlightingMode;
this.entireRowMode = true;
} else {
@ -88,52 +85,25 @@ export class WpTableConfigurationHighlightingTab implements TabComponent {
}
}
public updateHighlightingAttributes(model:HalResource[]) {
this.selectedAttributes = model;
}
public disabledValue(value:boolean):string | null {
return value ? 'disabled' : null;
}
ngOnInit() {
this.availableMappedHighlightedAttributes =
[this.allAttributesOption].concat(this.getAvailableAttributes());
this.setSelectedValues();
this.eeShowBanners = this.Banners.eeShowBanners;
this.updateMode(this.wpTableHighlight.current.mode);
if (this.eeShowBanners) {
this.updateMode('none');
}
public get availableHighlightedAttributes():HalResource[] {
const schema = this.querySpace.queryForm.value!.schema;
return schema.highlightedAttributes.allowedValues;
}
private setSelectedValues() {
const currentValues = this.wpTableHighlight.current.selectedAttributes;
if (currentValues === undefined) {
this.selectedAttributes = [this.allAttributesOption];
this.selectedAttributes = this.availableInlineHighlightedAttributes;
} else {
this.selectedAttributes = this.mapAttributes(currentValues);
this.selectedAttributes = currentValues;
}
}
public get availableHighlightedAttributes():HalResource[] {
const schema = this.querySpace.queryForm.value!.schema;
return schema.highlightedAttributes.allowedValues;
}
public getAvailableAttributes():MultiToggledSelectOption[] {
return this.mapAttributes(this.availableHighlightedAttributes);
}
private mapAttributes(input:HalResource[]):MultiToggledSelectOption[] {
return input.map((el:HalResource) => ({name: el.name, value: el.$href!}));
}
private get allAttributesOption():MultiToggledSelectOption {
return {
name: this.text.highlighting_mode.inline_all_attributes,
singleOnly: true,
selectWhenEmptySelection: true,
value: 'all'
};
}
}

@ -1,69 +0,0 @@
<div class="inline-label">
<select
*ngIf="!isMultiselect"
class="focus-input wp-inline-edit--field inplace-edit--field form--select"
[(ngModel)]="selectedOption"
[attr.aria-required]="isRequired"
[required]="isRequired"
[disabled]="isDisabled"
[attr.id]="selectHtmlId || undefined"
(keydown)="onValueKeydown.emit($event)"
[compareWith]="compareByValue"
(change)="emitValueChange()">
<option
value=""
[textContent]="text.requiredPlaceholder"
[attr.label]="text.requiredPlaceholder"
*ngIf="currentValueInvalid || availableOptions.length == 0"
[selected]="currentValueInvalid || availableOptions.length == 0"
[attr.selected]="currentValueInvalid || availableOptions.length == 0 || undefined"
disabled>
</option>
<option
*ngFor="let value of availableOptions"
[ngValue]="value"
[attr.label]="value.name"
[textContent]="value.name">
</option>
</select>
<select
*ngIf="isMultiselect"
[(ngModel)]="selectedOption"
class="focus-input wp-inline-edit--field inplace-edit--textarea -animated form--select"
[attr.aria-required]="isRequired"
[required]="isRequired"
[disabled]="isDisabled"
[attr.id]="selectHtmlId || undefined"
(keydown)="onValueKeydown.emit($event)"
[compareWith]="compareByValue"
(change)="emitValueChange()"
multiple
size=5>
<option
value=""
[textContent]="text.requiredPlaceholder"
*ngIf="currentValueInvalid || availableOptions.length == 0"
[attr.label]="text.requiredPlaceholder"
[selected]="currentValueInvalid || availableOptions.length == 0"
disabled>
</option>
<option
*ngFor="let value of availableMultiOptions"
[ngValue]="value"
[attr.label]="value.name"
[textContent]="value.name">
</option>
</select>
<a href
class="wp-inline-edit--toggle-multiselect form-label no-decoration-on-hover -transparent"
(click)="toggleMultiselect()">
<op-icon *ngIf="isMultiselect"
icon-classes="icon-minus2 icon4"
[icon-title]="text.switch_to_single_select"></op-icon>
<op-icon *ngIf="!isMultiselect"
icon-classes="icon-add icon4"
[icon-title]="text.switch_to_multi_select"></op-icon>
</a>
</div>

@ -1,143 +0,0 @@
// -- 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, EventEmitter, Input, OnInit, Output} from "@angular/core";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {AngularTrackingHelpers} from "core-components/angular/tracking-functions";
export interface MultiToggledSelectOption {
name:string;
singleOnly?:true;
selectWhenEmptySelection?:true;
value:any;
}
@Component({
selector: 'multi-toggled-select',
templateUrl: './multi-toggled-select.component.html'
})
export class MultiToggledSelectComponent<T extends MultiToggledSelectOption> implements OnInit {
@Input() availableOptions:T[];
@Input() initialSelection:T[]|undefined;
@Input() selectHtmlId:string|undefined;
@Input() isRequired:boolean = false;
@Input() isDisabled:boolean = false;
@Input() currentValueInvalid:boolean = false;
@Output() onValueChange = new EventEmitter<T[]|T|undefined>();
@Output() onMultiToggle = new EventEmitter<boolean>();
@Output() onValueKeydown = new EventEmitter<KeyboardEvent>();
public text = {
requiredPlaceholder: this.I18n.t('js.placeholders.selection'),
placeholder: this.I18n.t('js.placeholders.default'),
switch_to_single_select: this.I18n.t('js.work_packages.label_switch_to_single_select'),
switch_to_multi_select: this.I18n.t('js.work_packages.label_switch_to_multi_select'),
};
/** Whether we're currently multi-selecting */
public isMultiselect = false;
/** Comparer function for values */
public compareByValue = AngularTrackingHelpers.compareByAttribute('value');
/** Current selected option */
private _selectedOption:T|T[]|undefined;
constructor(protected readonly I18n:I18nService) {
}
ngOnInit() {
this.ensureSingleInitialSelectionIsNotArray();
this.isMultiselect = this.hasMultipleSelectedOptions();
}
public hasMultipleSelectedOptions() {
return (this.selectedOption instanceof Array) && this.selectedOption.length > 1;
}
public emitValueChange() {
this.onValueChange.emit(_.castArray(this.selectedOption || []));
}
public toggleMultiselect() {
this.isMultiselect = !this.isMultiselect;
if (this.isMultiselect ) {
/** Switching to multi select.
* Ensure selectedOption is either an empty Array or the selectedOption,
* Preventing cases such as `[undefined]`. **/
this._selectedOption = _.castArray(this.selectedOption || []);
} else {
/** Switching to single select. **/
if (Array.isArray(this.selectedOption)) {
if (this.selectedOption.length === 0) {
this.onEmptySelection();
} else {
this._selectedOption = (this.selectedOption as T[])[0];
}
this.emitValueChange();
}
}
}
public get availableMultiOptions() {
return this.availableOptions.filter(el => el.singleOnly !== true);
}
public get selectedOption():T|T[]|undefined {
return this._selectedOption;
}
public set selectedOption(val:T|T[]|undefined) {
this._selectedOption = val;
}
public get nullOption():T {
return { name: this.text.placeholder, value: '' } as T;
}
/** Ensure that the initialSelection becomes an Array.
* `undefined` becomes an empty Array. **/
private ensureSingleInitialSelectionIsNotArray():void {
if (Array.isArray(this.initialSelection)) {
if (this.initialSelection.length === 1) {
this.selectedOption = this.initialSelection[0];
} else {
this.selectedOption = this.initialSelection;
}
}
}
private onEmptySelection():void {
const newSelection = _.find(this.availableOptions, option => option.selectWhenEmptySelection === true);
if (newSelection) {
this.selectedOption = newSelection;
}
}
}

@ -57,7 +57,6 @@ import {highlightColBootstrap} from "./highlight-col/highlight-col.directive";
import {HookService} from "../plugins/hook-service";
import {HTMLSanitizeService} from "./html-sanitize/html-sanitize.service";
import {ColorsAutocompleter} from "core-app/modules/common/colors/colors-autocompleter.component";
import {MultiToggledSelectComponent} from "core-app/modules/common/multi-toggled-select/multi-toggled-select.component";
import {BannersService} from "core-app/modules/common/enterprise/banners.service";
import {ResizerComponent} from "core-app/modules/common/resizer/resizer.component";
import {TablePaginationComponent} from 'core-components/table-pagination/table-pagination.component';
@ -150,9 +149,6 @@ export function bootstrapModule(injector:Injector) {
// Table highlight
HighlightColDirective,
// Multi select component
MultiToggledSelectComponent,
ResizerComponent,
TablePaginationComponent,
@ -213,8 +209,6 @@ export function bootstrapModule(injector:Injector) {
CopyToClipboardDirective,
ColorsAutocompleter,
MultiToggledSelectComponent,
ResizerComponent,
TablePaginationComponent,

@ -44,23 +44,31 @@ module Components
def switch_entire_row_highlight(label)
modal_open? or open_modal
choose "Entire row by"
page.all(".form--field")[1].select label
# Open select field
within(page.all(".form--field")[1]) do
page.find('.ng-input input').click
end
page.find('.ng-dropdown-panel .ng-option', text: label).click
apply
end
def switch_inline_attribute_highlight(*labels)
modal_open? or open_modal
choose "Highlighted attribute(s)"
# Open select field
within(page.all(".form--field")[0]) do
if labels.size == 1
select labels.first
elsif labels.size > 1
find('[class*="--toggle-multiselect"]').click
labels.each do |label|
select label
end
end
page.find('.ng-input input').click
end
# Delete all previously selected options
page.all('.ng-dropdown-panel .ng-option-selected').each { |option| option.click }
labels.each do |label|
page.find('.ng-dropdown-panel .ng-option', text: label).click
end
apply
end

Loading…
Cancel
Save