pull/6263/head
commit
14e3e88255
@ -0,0 +1 @@ |
||||
app/assets/vendor/* binary |
@ -0,0 +1,24 @@ |
||||
import {Subject} from 'rxjs'; |
||||
import {Observable} from 'rxjs/Observable'; |
||||
import {EventEmitter} from '@angular/core'; |
||||
|
||||
export class DebouncedEventEmitter<T> { |
||||
private emitter = new EventEmitter<T>(); |
||||
private debouncer:Subject<T>; |
||||
|
||||
constructor(takeUntil:Observable<true>, debounceTimeInMs:number = 250) { |
||||
this.debouncer = new Subject<T>(); |
||||
this.debouncer |
||||
.debounceTime(debounceTimeInMs) |
||||
.takeUntil(takeUntil) |
||||
.subscribe((val) => this.emitter.emit(val)); |
||||
} |
||||
|
||||
public emit(value?:T):void { |
||||
this.debouncer.next(value); |
||||
} |
||||
|
||||
public subscribe(generatorOrNext?:any, error?:any, complete?:any):any { |
||||
return this.emitter.subscribe(generatorOrNext, error, complete); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; |
||||
|
||||
export namespace AngularTrackingHelpers { |
||||
export function halHref<T extends HalResource>(_index:number, item:T):string|null { |
||||
return item.$href; |
||||
} |
||||
|
||||
export function compareByHref<T extends HalResource>(a:T|undefined|null, b:T|undefined|null):boolean { |
||||
const bothNil = !a && !b; |
||||
return bothNil || (!!a && !!b && a.$href === b.$href); |
||||
} |
||||
|
||||
export function compareByHrefOrString<T extends HalResource>(a:T|string|undefined|null, b:T|string|undefined|null):boolean { |
||||
if (a instanceof HalResource && b instanceof HalResource) { |
||||
return compareByHref(a as HalResource, b as HalResource); |
||||
} |
||||
|
||||
const bothNil = !a && !b; |
||||
return bothNil || a === b; |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
import {opUiComponentsModule} from 'core-app/angular-modules'; |
||||
|
||||
function opIcon() { |
||||
return { |
||||
restrict: 'EA', |
||||
scope: { |
||||
iconClasses: '@', |
||||
iconTitle: '@' |
||||
}, |
||||
link: (_scope:ng.IScope, element:ng.IAugmentedJQuery) => { |
||||
element.addClass('op-icon--wrapper'); |
||||
}, |
||||
template: ` |
||||
<i class="{{iconClasses}}" aria-hidden="true"></i> |
||||
<span class="hidden-for-sighted" ng-bind="iconTitle" ng-if="iconTitle"></span> |
||||
` |
||||
}; |
||||
} |
||||
|
||||
opUiComponentsModule.directive('opIcon', opIcon); |
@ -1,66 +0,0 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
describe('NotificationBoxDirective', function() { |
||||
var $compile; |
||||
var $rootScope; |
||||
|
||||
beforeEach(angular.mock.module('openproject.uiComponents', 'openproject.templates')); |
||||
beforeEach(angular.mock.module('openproject.services', function($provide) { |
||||
var configurationService = {}; |
||||
|
||||
configurationService.accessibilityModeEnabled = sinon.stub().returns(false); |
||||
$provide.constant('ConfigurationService', configurationService); |
||||
})); |
||||
|
||||
beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_) { |
||||
$compile = _$compile_; |
||||
$rootScope = _$rootScope_; |
||||
})); |
||||
|
||||
it('should need a content to properly work', function() { |
||||
expect(function() { |
||||
$compile('<notification-box></notification-box>')($rootScope); |
||||
$rootScope.$digest(); |
||||
}).to.throw; |
||||
}); |
||||
|
||||
it('should render with content set', function() { |
||||
$rootScope.warning = { message: 'warning!' }; |
||||
var element = $compile('<notification-box content="warning"></notification-box>')($rootScope); |
||||
$rootScope.$digest(); |
||||
expect(element.html()).to.contain('warning!'); |
||||
}); |
||||
|
||||
it('should render with the appropiate type', function() { |
||||
$rootScope.error = { message: 'error!', type: 'error' }; |
||||
var element = $compile('<notification-box content="error"></notification-box>')($rootScope); |
||||
$rootScope.$digest(); |
||||
expect(element.html()).to.contain('-error'); |
||||
}); |
||||
}); |
@ -0,0 +1,15 @@ |
||||
<div id="div-values-{{filter.name}}"> |
||||
<select [(ngModel)]="value" |
||||
class="advanced-filters--select-field" |
||||
id="values-{{filter.name}}" |
||||
name="v[{{filter.name}}]" > |
||||
<option value="" |
||||
disabled |
||||
[textContent]="text.placeholder" |
||||
*ngIf="hasNoValue"> |
||||
</option> |
||||
<option *ngFor="let value of availableOptions" |
||||
[textContent]="text[value]" |
||||
[ngValue]="value"></option> |
||||
</select> |
||||
</div> |
@ -1,10 +0,0 @@ |
||||
<div id="div-values-{{$ctrl.filter.name}}"> |
||||
|
||||
<select ng-model="$ctrl.value" |
||||
ng-options="$ctrl.text[value] for value in $ctrl.availableOptions" |
||||
class="advanced-filters--select-field" |
||||
id="values-{{$ctrl.filter.name}}" |
||||
name="v[{{$ctrl.filter.name}}]" > |
||||
<option value="" disabled ng-bind="::$ctrl.text.placeholder" ng-if="$ctrl.hasNoValue"></option> |
||||
</select> |
||||
</div> |
@ -1,5 +1,8 @@ |
||||
<div class="work-packages--filters-optional-container" [hidden]="!wpFiltersService.visible"> |
||||
<div id="query_form_content" class="hide-when-print"> |
||||
<ng1-query-filters-wrapper></ng1-query-filters-wrapper> |
||||
<query-filters *ngIf="!!filters" |
||||
[filters]="filters" |
||||
[showCloseFilter]="true" |
||||
(filtersChanged)="replaceIfComplete($event)"></query-filters> |
||||
</div> |
||||
</div> |
||||
|
@ -0,0 +1,18 @@ |
||||
<div class="inline-label" id="div-values-{{filter.id}}"> |
||||
<op-date-picker (onChange)="value = isoDateParser($event)"> |
||||
<input [ngModel]="isoDateFormatter(value)" |
||||
(ngModelChange)="value = isoDateParser($event)" |
||||
required |
||||
id="values-{{filter.id}}" |
||||
name="v[{{filter.id}}]" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
transform-date-utc |
||||
type="text"/> |
||||
</op-date-picker> |
||||
<span class="advanced-filters--tooltip-trigger -multiline" |
||||
[attr.data-tooltip]="timeZoneText" |
||||
*ngIf="isTimeZoneDifferent"> |
||||
<op-icon icon-classes="icon icon-warning"></op-icon> |
||||
</span> |
||||
</div> |
@ -1,18 +0,0 @@ |
||||
<div class="inline-label" id="div-values-{{$ctrl.filter.id}}"> |
||||
<op-date-picker> |
||||
<input ng-model="$ctrl.value" |
||||
ng-model-options="::$ctrl.filterDateModelOptions" |
||||
ng-required="true" |
||||
id="values-{{$ctrl.filter.id}}" |
||||
name="v[{{$ctrl.filter.id}}]" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
transform-date-utc |
||||
type="text"/> |
||||
</op-date-picker> |
||||
<span class="advanced-filters--tooltip-trigger -multiline" |
||||
data-tooltip="{{$ctrl.timeZoneText}}" |
||||
ng-if="$ctrl.isTimeZoneDifferent"> |
||||
<op-icon icon-classes="icon icon-warning"></op-icon> |
||||
</span> |
||||
</div> |
@ -0,0 +1,31 @@ |
||||
<div id="div-values-{{filter.id}}" class="inline-label"> |
||||
<op-date-picker (onChange)="begin = isoDateParser($event)"> |
||||
<input [ngModel]="isoDateFormatter(begin)" |
||||
(ngModelChange)="begin = isoDateParser($event)" |
||||
[disabled]="isLoading" |
||||
required |
||||
id="values-{{filter.id}}-begin" |
||||
name="v[{{filter.id}}]-begin" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
type="text"/> |
||||
</op-date-picker> |
||||
|
||||
<span class="advanced-filters--affix" [textContent]="text.spacer"> |
||||
</span> |
||||
|
||||
<op-date-picker (onChange)="end = isoDateParser($event)"> |
||||
<input [ngModel]="isoDateFormatter(end)" |
||||
(ngModelChange)="end = isoDateParser($event)" |
||||
[disabled]="isLoading" |
||||
id="values-{{filter.id}}-end" |
||||
name="v[{{filter.id}}]-end" |
||||
class="advanced-filters--date-field" |
||||
size="10"> |
||||
</op-date-picker> |
||||
<span class="advanced-filters--tooltip-trigger -multiline" |
||||
*ngIf="isTimeZoneDifferent" |
||||
[attr.data-tooltip]="timeZoneText"> |
||||
<op-icon icon-classes="icon icon-warning"></op-icon> |
||||
</span> |
||||
</div> |
@ -1,36 +0,0 @@ |
||||
<div id="div-values-{{$ctrl.filter.id}}" class="inline-label"> |
||||
<op-date-picker> |
||||
<input ng-model="$ctrl.begin" |
||||
ng-model-options="::$ctrl.filterDateModelOptions" |
||||
ng-required="true" |
||||
ng-disabled="isLoading" |
||||
id="values-{{$ctrl.filter.id}}-begin" |
||||
name="v[{{$ctrl.filter.id}}]-begin" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
transform-date-utc |
||||
type="text"/> |
||||
</op-date-picker> |
||||
|
||||
<span class="advanced-filters--affix"> |
||||
{{ ::$ctrl.I18n.t('js.filter.value_spacer') }} |
||||
</span> |
||||
|
||||
<op-date-picker> |
||||
<input ng-model="$ctrl.end" |
||||
ng-model-options="::$ctrl.filterDateModelOptions" |
||||
ng-required="true" |
||||
ng-disabled="isLoading" |
||||
id="values-{{$ctrl.filter.id}}-end" |
||||
name="v[{{$ctrl.filter.id}}]-end" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
transform-date-utc |
||||
type="text"/> |
||||
</op-date-picker> |
||||
<span class="advanced-filters--tooltip-trigger -multiline" |
||||
ng-if="$ctrl.isTimeZoneDifferent" |
||||
data-tooltip="{{$ctrl.timeZoneText}}"> |
||||
<op-icon icon-classes="icon icon-warning"></op-icon> |
||||
</span> |
||||
</div> |
@ -0,0 +1,12 @@ |
||||
<div id="div-values-{{filter.id}}"> |
||||
<op-date-picker (onChange)="value = parser($event)"> |
||||
<input [ngModel]="formatter(value)" |
||||
(ngModelChange)="value = parser($event)" |
||||
required |
||||
id="values-{{filter.id}}" |
||||
name="v[{{filter.id}}]" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
type="text"/> |
||||
</op-date-picker> |
||||
</div> |
@ -1,13 +0,0 @@ |
||||
<div id="div-values-{{$ctrl.filter.id}}"> |
||||
<op-date-picker> |
||||
<input ng-model="$ctrl.value" |
||||
ng-model-options="::$ctrl.filterDateModelOptions" |
||||
ng-required="true" |
||||
id="values-{{$ctrl.filter.id}}" |
||||
name="v[{{$ctrl.filter.id}}]" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
transform-date-value |
||||
type="text"/> |
||||
</op-date-picker> |
||||
</div> |
@ -0,0 +1,29 @@ |
||||
<div id="div-values-{{filter.id}}" |
||||
class="inline-label"> |
||||
<op-date-picker (onChange)="begin = parser($event)"> |
||||
<input [ngModel]="formatter(begin)" |
||||
(ngModelChange)="begin = parser($event)" |
||||
[disabled]="isLoading" |
||||
required |
||||
id="values-{{filter.id}}-begin" |
||||
name="v[{{filter.id}}]-begin" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
type="text"/> |
||||
</op-date-picker> |
||||
|
||||
<span class="advanced-filters--affix" [textContent]="text.spacer"> |
||||
</span> |
||||
|
||||
<op-date-picker (onChange)="end = parser($event)"> |
||||
<input [ngModel]="formatter(end)" |
||||
(ngModelChange)="end = parser($event)" |
||||
[disabled]="isLoading" |
||||
required |
||||
id="values-{{filter.id}}-end" |
||||
name="v[{{filter.id}}]-end" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
type="text"/> |
||||
</op-date-picker> |
||||
</div> |
@ -1,32 +0,0 @@ |
||||
<div id="div-values-{{$ctrl.filter.id}}" |
||||
class="inline-label"> |
||||
<op-date-picker> |
||||
<input ng-model="$ctrl.begin" |
||||
ng-model-options="::$ctrl.filterDateModelOptions" |
||||
ng-required="true" |
||||
ng-disabled="isLoading" |
||||
id="values-{{$ctrl.filter.id}}-begin" |
||||
name="v[{{$ctrl.filter.id}}]-begin" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
transform-date-value |
||||
type="text"/> |
||||
</op-date-picker> |
||||
|
||||
<span class="advanced-filters--affix"> |
||||
{{ ::$ctrl.I18n.t('js.filter.value_spacer') }} |
||||
</span> |
||||
|
||||
<op-date-picker> |
||||
<input ng-model="$ctrl.end" |
||||
ng-model-options="::$ctrl.filterDateModelOptions" |
||||
ng-required="true" |
||||
ng-disabled="isLoading" |
||||
id="values-{{$ctrl.filter.id}}-end" |
||||
name="v[{{$ctrl.filter.id}}]-end" |
||||
class="advanced-filters--date-field" |
||||
size="10" |
||||
transform-date-value |
||||
type="text"/> |
||||
</op-date-picker> |
||||
</div> |
@ -0,0 +1,14 @@ |
||||
<div id="div-values-{{filter.id}}" class="inline-label"> |
||||
<input [(ngModel)]="value" |
||||
[disabled]="isLoading" |
||||
required |
||||
class="advanced-filters--number-field" |
||||
id="values-{{filter.id}}" |
||||
name="v[{{filter.id}}]" |
||||
min="0" |
||||
type="number" /> |
||||
<label for="values-{{filter.id}}" |
||||
[textContent]="unit" |
||||
class="advanced-filters--affix"> |
||||
</label> |
||||
</div> |
@ -1,18 +0,0 @@ |
||||
<div id="div-values-{{$ctrl.filter.id}}" class="inline-label"> |
||||
|
||||
<input ng-model="$ctrl.value" |
||||
ng-model-options="::$ctrl.filterModelOptions" |
||||
ng-required="true" |
||||
class="advanced-filters--number-field" |
||||
id="values-{{$ctrl.filter.id}}" |
||||
name="v[{{$ctrl.filter.id}}]" |
||||
min="0" |
||||
type="number" |
||||
value="" |
||||
ng-disabled="isLoading"/> |
||||
<label for="values-{{$ctrl.filter.id}}" |
||||
class="advanced-filters--affix"> |
||||
{{ ::$ctrl.unit }} |
||||
</label> |
||||
|
||||
</div> |
@ -0,0 +1,12 @@ |
||||
<div id="div-values-{{filter.name}}"> |
||||
<input [(ngModel)]="value" |
||||
requried="true" |
||||
class="advanced-filters--text-field" |
||||
id="values-{{filter.name}}" |
||||
name="v[{{filter.name}}]" |
||||
type="text" /> |
||||
<label for="values-{{filter.name}}" |
||||
[textContent]="text.enter_text" |
||||
class="hidden-for-sighted"> |
||||
</label> |
||||
</div> |
@ -1,15 +0,0 @@ |
||||
<div id="div-values-{{$ctrl.filter.name}}"> |
||||
|
||||
<input ng-model="$ctrl.value" |
||||
ng-model-options="::$ctrl.filterModelOptions" |
||||
ng-required="true" |
||||
class="advanced-filters--text-field" |
||||
id="values-{{$ctrl.filter.name}}" |
||||
name="v[{{$ctrl.filter.name}}]" |
||||
type="text" /> |
||||
|
||||
<label for="values-{{$ctrl.filter.name}}" |
||||
class="hidden-for-sighted"> |
||||
{{ ::$ctrl.I18n.t('js.work_packages.description_enter_text') }} |
||||
</label> |
||||
</div> |
@ -0,0 +1,49 @@ |
||||
<div class="inline-label" |
||||
id="div-values-{{filter.id}}"> |
||||
|
||||
<select filter-value-select |
||||
*ngIf="!isMultiselect" |
||||
[(ngModel)]="value" |
||||
[compareWith]="compareByHrefOrString" |
||||
[disabled]="disabled" |
||||
id="values-{{filter.id}}" |
||||
name="v[{{filter.id}}][]" |
||||
class="advanced-filters--select"> |
||||
<option [textContent]="text.placeholder" |
||||
*ngIf="hasNoValue" |
||||
[ngValue]="null" |
||||
disabled></option> |
||||
<option *ngFor="let value of availableOptions" |
||||
[textContent]="value.name" |
||||
[ngValue]="value"></option> |
||||
</select> |
||||
|
||||
<select multiple |
||||
filter-value-select |
||||
*ngIf="isMultiselect" |
||||
[(ngModel)]="value" |
||||
[compareWith]="compareByHrefOrString" |
||||
[disabled]="disabled" |
||||
id="values-{{filter.id}}" |
||||
name="v[{{filter.id}}][]" |
||||
class="advanced-filters--select"> |
||||
<option [ngValue]="null" |
||||
[textContent]="text.placeholder" |
||||
disabled |
||||
selected |
||||
*ngIf="hasNoValue"></option> |
||||
<option *ngFor="let value of availableOptions" |
||||
[textContent]="value.name" |
||||
[ngValue]="value"></option> |
||||
</select> |
||||
|
||||
<a class="form-label filter-toggled-multiselect--toggler no-decoration-on-hover -transparent" |
||||
(click)="toggleMultiselect()"> |
||||
<op-icon *ngIf="isMultiselect" |
||||
icon-classes="icon4 icon-minus2" |
||||
[icon-title]="text.disableMulti"></op-icon> |
||||
<op-icon *ngIf="!isMultiselect" |
||||
icon-classes="icon4 icon-add" |
||||
[icon-title]="text.enableMulti"></op-icon> |
||||
</a> |
||||
</div> |
@ -0,0 +1,246 @@ |
||||
//-- 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 { |
||||
$httpToken, |
||||
$qToken, |
||||
halResourceFactoryToken, |
||||
I18nToken, |
||||
PathHelperToken |
||||
} from 'core-app/angular4-transition-utils'; |
||||
import {async, fakeAsync, TestBed, tick} from '@angular/core/testing'; |
||||
import {ComponentFixture} from '@angular/core/testing/src/component_fixture'; |
||||
import {FilterToggledMultiselectValueComponent} from './filter-toggled-multiselect-value.component'; |
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; |
||||
import {FormsModule} from '@angular/forms'; |
||||
import {OpIcon} from 'core-components/common/icon/op-icon'; |
||||
import {DebugElement} from '@angular/core'; |
||||
import {By} from '@angular/platform-browser'; |
||||
import {RootDmService} from 'core-app/modules/dm-services/root-dm.service'; |
||||
|
||||
require('core-app/angular4-test-setup'); |
||||
|
||||
describe('FilterToggledMultiselectValueComponent', () => { |
||||
const I18nStub = { |
||||
t: sinon.stub() |
||||
.withArgs('js.placeholders.selection') |
||||
.returns('PLACEHOLDER') |
||||
}; |
||||
|
||||
let app:FilterToggledMultiselectValueComponent; |
||||
let fixture:ComponentFixture<FilterToggledMultiselectValueComponent> |
||||
let element:JQuery; |
||||
let debugElement:DebugElement; |
||||
|
||||
const allowedValues = [ |
||||
{ |
||||
name: 'New York', |
||||
$href: 'api/new_york' |
||||
}, |
||||
{ |
||||
name: 'California', |
||||
$href: 'api/california' |
||||
} |
||||
]; |
||||
|
||||
beforeEach(async(() => { |
||||
TestBed.configureTestingModule({ |
||||
imports: [ |
||||
FormsModule |
||||
], |
||||
declarations: [ |
||||
OpIcon, |
||||
FilterToggledMultiselectValueComponent |
||||
], |
||||
providers: [ |
||||
{ provide: I18nToken, useValue: I18nStub }, |
||||
{ provide: PathHelperToken, useValue: {} }, |
||||
{ provide: RootDmService, useValue: {} }, |
||||
{ provide: $qToken, useValue: {} }, |
||||
{ provide: $httpToken, useValue: {} }, |
||||
{ provide: halResourceFactoryToken, useValue: {} }, |
||||
] |
||||
}) |
||||
.compileComponents() |
||||
.then(() => { |
||||
fixture = TestBed.createComponent(FilterToggledMultiselectValueComponent); |
||||
app = fixture.debugElement.componentInstance; |
||||
debugElement = fixture.debugElement; |
||||
element = jQuery(debugElement.nativeElement); |
||||
}); |
||||
})); |
||||
|
||||
describe('with values', function () { |
||||
beforeEach(function () { |
||||
app.filter = { |
||||
name: "BO' SELECTA", |
||||
values: allowedValues, |
||||
currentSchema: { |
||||
values: { |
||||
allowedValues: allowedValues |
||||
} |
||||
}, |
||||
$embedded: {} as any, |
||||
$links: {} as any |
||||
} as any; |
||||
fixture.detectChanges(); |
||||
}); |
||||
|
||||
describe('app.isValueMulti()', function () { |
||||
it('is true', () => { |
||||
expect(app.isValueMulti()).to.be.true; |
||||
}); |
||||
}); |
||||
|
||||
describe('app.value', function () { |
||||
it('is no array', function () { |
||||
expect(Array.isArray(app.value)).to.be.true; |
||||
}); |
||||
|
||||
it('is the filter value', function () { |
||||
let value = app.value as HalResource[]; |
||||
|
||||
expect(value.length).to.eq(2); |
||||
expect(value[0]).to.eq(allowedValues[0]); |
||||
expect(value[1]).to.eq(allowedValues[1]); |
||||
}); |
||||
}); |
||||
|
||||
describe('element', function () { |
||||
it('should render a div', function () { |
||||
expect(element.prop('tagName')).to.equal('DIV'); |
||||
}); |
||||
|
||||
it('should render only one select', function () { |
||||
expect(element.find('select').length).to.equal(1); |
||||
expect(element.find('select.ng-hide').length).to.equal(0); |
||||
}); |
||||
|
||||
it('should render two OPTIONs SELECT', function () { |
||||
var select = element.find('select:not(.ng-hide)').first(); |
||||
var options = select.find('option').toArray() as HTMLOptionElement[]; |
||||
|
||||
expect(options.length).to.equal(2); |
||||
|
||||
expect(options[0].textContent).to.equal(allowedValues[0].name); |
||||
expect(options[1].textContent).to.equal(allowedValues[1].name); |
||||
}); |
||||
|
||||
it('should render a link that toggles multi-select', fakeAsync(function () { |
||||
expect(app.isMultiselect, 'Component is multiselect').to.be.true; |
||||
var a = debugElement.query(By.css('.filter-toggled-multiselect--toggler')); |
||||
expect(element.find('select').length, 'has select').to.equal(1); |
||||
expect(element.find('select[multiple]').length, 'has multiple select').to.equal(1); |
||||
a.triggerEventHandler('click', null); |
||||
fixture.detectChanges(); |
||||
|
||||
expect(app.isMultiselect, 'Component is no longer multiselect').to.be.false; |
||||
expect(element.find('select').length, 'has select').to.equal(1); |
||||
expect(element.find('select[multiple]').length, 'has no multiple select').to.equal(0); |
||||
})); |
||||
}); |
||||
}); |
||||
|
||||
describe('w/o values and options', function () { |
||||
beforeEach(function () { |
||||
app.filter = { |
||||
name: "BO' SELECTA", |
||||
values: [], |
||||
currentSchema: { |
||||
values: { |
||||
allowedValues: [] |
||||
} |
||||
} |
||||
} as any; |
||||
|
||||
fixture.detectChanges(); |
||||
}); |
||||
|
||||
describe('app.isValueMulti()', function () { |
||||
it('is false', () => { |
||||
expect(app.isValueMulti()).to.be.false; |
||||
}); |
||||
}); |
||||
|
||||
describe('app.value', function () { |
||||
it('is no array', function () { |
||||
expect(Array.isArray(app.value)).to.be.false; |
||||
}); |
||||
|
||||
it('is null', function () { |
||||
expect(app.value).to.be.null; |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('w/o value', function () { |
||||
beforeEach(function () { |
||||
app.filter = { |
||||
name: "BO' SELECTA", |
||||
values: [], |
||||
currentSchema: { |
||||
values: { |
||||
allowedValues: allowedValues |
||||
} |
||||
} |
||||
} as any; |
||||
|
||||
fixture.detectChanges(); |
||||
}); |
||||
|
||||
describe('app.isValueMulti()', function () { |
||||
it('is false', () => { |
||||
expect(app.isValueMulti()).to.be.false; |
||||
}); |
||||
}); |
||||
|
||||
describe('app.value', function () { |
||||
it('is no array', function () { |
||||
expect(Array.isArray(app.value)).to.be.false; |
||||
}); |
||||
|
||||
it('is null', function () { |
||||
expect(app.value).to.be.null; |
||||
}); |
||||
}); |
||||
|
||||
describe('element', function () { |
||||
it('should render two OPTIONs SELECT + Placeholder', function () { |
||||
var select = element.find('select:not(.ng-hide)').first(); |
||||
var options = select.find('option').toArray() as HTMLOptionElement[]; |
||||
|
||||
expect(options.length).to.equal(3); |
||||
expect(options[0].textContent).to.equal('PLACEHOLDER'); |
||||
|
||||
expect(options[1].textContent).to.equal(allowedValues[0].name); |
||||
expect(options[2].textContent).to.equal(allowedValues[1].name); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
@ -1,37 +0,0 @@ |
||||
<div class="inline-label" |
||||
id="div-values-{{$ctrl.filter.id}}"> |
||||
|
||||
<select filter-value-select |
||||
ng-if="!$ctrl.isMultiselect" |
||||
name="v[{{$ctrl.filter.id}}][]" |
||||
ng-model="$ctrl.value" |
||||
ng-model-options="{ getterSetter: true }" |
||||
ng-disabled="$ctrl.disabled" |
||||
ng-attr-id="values-{{$ctrl.filter.id}}" |
||||
class="advanced-filters--select" |
||||
ng-options="value.name for value in $ctrl.availableOptions track by value.$href"> |
||||
<option value="" disabled ng-bind="::$ctrl.text.placeholder" ng-if="$ctrl.hasNoValue"></option> |
||||
</select> |
||||
|
||||
<select multiple |
||||
filter-value-select |
||||
ng-if="$ctrl.isMultiselect" |
||||
name="v[{{$ctrl.filter.id}}][]" |
||||
ng-model="$ctrl.value" |
||||
ng-model-options="{ getterSetter: true }" |
||||
ng-disabled="$ctrl.disabled" |
||||
ng-attr-id="values-{{$ctrl.filter.id}}" |
||||
class="advanced-filters--select" |
||||
ng-options="value.name for value in $ctrl.availableOptions track by value.$href"> |
||||
<option value="" disabled ng-bind="::$ctrl.text.placeholder" ng-if="$ctrl.hasNoValue"></option> |
||||
</select> |
||||
|
||||
<a href class="form-label no-decoration-on-hover -transparent" ng-click="$ctrl.toggleMultiselect()"> |
||||
<op-icon ng-if="$ctrl.isMultiselect" |
||||
icon-classes="icon4 icon-minus2" |
||||
icon-title="{{ ::$ctrl.text.disableMulti }}"></op-icon> |
||||
<op-icon ng-if="!$ctrl.isMultiselect" |
||||
icon-classes="icon4 icon-add" |
||||
icon-title="{{ ::$ctrl.text.enableMulti }}"></op-icon> |
||||
</a> |
||||
</div> |
@ -1,227 +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.
|
||||
//++
|
||||
|
||||
/*jshint expr: true*/ |
||||
|
||||
import {ToggledMultiselectController} from './filter-toggled-multiselect-value.directive' |
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; |
||||
|
||||
describe('toggledMultiselect Directive', function() { |
||||
var compile:any, element:any, rootScope:any, scope:any, I18n:any; |
||||
var controller:ToggledMultiselectController; |
||||
var allowedValues:any; |
||||
|
||||
beforeEach(angular.mock.module('openproject.filters', |
||||
'openproject.templates', |
||||
'openproject.services')); |
||||
|
||||
beforeEach(inject(function($rootScope:any, $compile:any, $injector:any) { |
||||
var html = '<filter-toggled-multiselect-value icon-name="cool-icon.png" filter="filter"></filter-toggled-multiselect-value>'; |
||||
|
||||
element = angular.element(html); |
||||
rootScope = $rootScope; |
||||
scope = $rootScope.$new(); |
||||
(window as any).ngInjector = $injector; |
||||
|
||||
allowedValues = [ |
||||
{ |
||||
name: 'New York', |
||||
$href: 'api/new_york' |
||||
}, |
||||
{ |
||||
name: 'California', |
||||
$href: 'api/california' |
||||
} |
||||
] |
||||
|
||||
compile = function() { |
||||
$compile(element)(scope); |
||||
angular.element(document.body).append(element); |
||||
scope.$apply(); |
||||
|
||||
controller = element.controller('filterToggledMultiselectValue'); |
||||
}; |
||||
})); |
||||
|
||||
beforeEach(angular.mock.inject((_I18n_:any) => { |
||||
I18n = _I18n_; |
||||
sinon.stub(I18n, 't').withArgs('js.placeholders.selection').returns('PLACEHOLDER'); |
||||
})); |
||||
afterEach(angular.mock.inject(() => { |
||||
I18n.t.restore(); |
||||
element.remove(); |
||||
})); |
||||
|
||||
describe('with values', function() { |
||||
beforeEach(function() { |
||||
scope.filter = { |
||||
name: "BO' SELECTA", |
||||
values: allowedValues, |
||||
currentSchema: { |
||||
values: { |
||||
allowedValues: allowedValues |
||||
} |
||||
} |
||||
}; |
||||
|
||||
compile(); |
||||
}); |
||||
|
||||
describe('controller.isValueMulti()', function() { |
||||
it('is true', () => { |
||||
expect(controller.isValueMulti()).to.be.true; |
||||
}); |
||||
}); |
||||
|
||||
describe('controller.value', function() { |
||||
it('is no array', function() { |
||||
expect(Array.isArray(controller.value)).to.be.true; |
||||
}); |
||||
|
||||
it('is the filter value', function() { |
||||
let value = controller.value as HalResource[]; |
||||
|
||||
expect(value.length).to.eq(2); |
||||
expect(value[0]).to.eq(allowedValues[0]); |
||||
expect(value[1]).to.eq(allowedValues[1]); |
||||
}); |
||||
}); |
||||
|
||||
describe('element', function() { |
||||
it('should render a div', function() { |
||||
expect(element.prop('tagName')).to.equal('DIV'); |
||||
}); |
||||
|
||||
it('should render only one select', function() { |
||||
expect(element.find('select').length).to.equal(1); |
||||
expect(element.find('select.ng-hide').length).to.equal(0); |
||||
}); |
||||
|
||||
it('should render two OPTIONs SELECT', function() { |
||||
var select = element.find('select:not(.ng-hide)').first(); |
||||
var options = select.find('option'); |
||||
|
||||
expect(options.length).to.equal(2); |
||||
|
||||
expect(options[0].value).to.equal(allowedValues[0].$href); |
||||
expect(options[0].textContent).to.equal(allowedValues[0].name); |
||||
|
||||
expect(options[1].value).to.equal(allowedValues[1].$href); |
||||
expect(options[1].textContent).to.equal(allowedValues[1].name); |
||||
}); |
||||
|
||||
xit('should render a link that toggles multi-select', function() { |
||||
var a = element.find('a'); |
||||
expect(element.find('select.ng-hide').length).to.equal(1); |
||||
a.click(); |
||||
scope.$apply(); |
||||
expect(element.find('select.ng-hide').length).to.equal(1); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('w/o values and options', function() { |
||||
beforeEach(function() { |
||||
scope.filter = { |
||||
name: "BO' SELECTA", |
||||
values: [], |
||||
currentSchema: { |
||||
values: { |
||||
allowedValues: [] |
||||
} |
||||
} |
||||
} |
||||
|
||||
compile(); |
||||
}); |
||||
|
||||
describe('controller.isValueMulti()', function() { |
||||
it('is false', () => { |
||||
expect(controller.isValueMulti()).to.be.false; |
||||
}); |
||||
}); |
||||
|
||||
describe('controller.value', function() { |
||||
it('is no array', function() { |
||||
expect(Array.isArray(controller.value)).to.be.false; |
||||
}); |
||||
|
||||
it('is undefined', function() { |
||||
expect(controller.value).to.be.undefined; |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('w/o value', function() { |
||||
beforeEach(function() { |
||||
scope.filter = { |
||||
name: "BO' SELECTA", |
||||
values: [], |
||||
currentSchema: { |
||||
values: { |
||||
allowedValues: allowedValues |
||||
} |
||||
} |
||||
} |
||||
|
||||
compile(); |
||||
}); |
||||
|
||||
describe('controller.isValueMulti()', function() { |
||||
it('is false', () => { |
||||
expect(controller.isValueMulti()).to.be.false; |
||||
}); |
||||
}); |
||||
|
||||
describe('controller.value', function() { |
||||
it('is no array', function() { |
||||
expect(Array.isArray(controller.value)).to.be.false; |
||||
}); |
||||
|
||||
it('is undefined', function() { |
||||
expect(controller.value).to.be.undefined; |
||||
}); |
||||
}); |
||||
|
||||
describe('element', function() { |
||||
it('should render two OPTIONs SELECT + Placeholder', function() { |
||||
var select = element.find('select:not(.ng-hide)').first(); |
||||
var options = select.find('option'); |
||||
|
||||
expect(options.length).to.equal(3); |
||||
expect(options[0].textContent).to.equal('PLACEHOLDER'); |
||||
|
||||
expect(options[1].value).to.equal(allowedValues[0].$href); |
||||
expect(options[1].textContent).to.equal(allowedValues[0].name); |
||||
|
||||
expect(options[2].value).to.equal(allowedValues[1].$href); |
||||
expect(options[2].textContent).to.equal(allowedValues[1].name); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -1,32 +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 {filtersModule} from '../../angular-modules'; |
||||
|
||||
filtersModule |
||||
.constant('ADD_FILTER_SELECT_INDEX', -1) |
@ -0,0 +1,83 @@ |
||||
<ng-container> |
||||
<!-- Name --> |
||||
<label for="operators-{{filter.id}}" |
||||
class="advanced-filters--filter-name" |
||||
[textContent]="filter.name" |
||||
[attr.title]="filter.name"> |
||||
</label> |
||||
|
||||
<!-- Operator --> |
||||
<div class="advanced-filters--filter-operator"> |
||||
|
||||
<label for="operators-{{filter.id}}" |
||||
class="hidden-for-sighted"> |
||||
{{ filter.name }} |
||||
{{ text.open_filter }} |
||||
</label> |
||||
|
||||
<select required |
||||
class="advanced-filters--select" |
||||
id="operators-{{filter.id}}" |
||||
name="op[{{filter.id}}]" |
||||
[(ngModel)]="filter.operator" |
||||
(ngModelChange)="onFilterUpdated(filter)" |
||||
[compareWith]="compareByHref" |
||||
style="vertical-align: top;" |
||||
[disabled]="isLoading"> |
||||
<option *ngFor="let operator of availableOperators" |
||||
[textContent]="operator.name" |
||||
[ngValue]="operator"> |
||||
</option> |
||||
</select> |
||||
</div> |
||||
|
||||
<!-- Values --> |
||||
<ng-container *ngIf="showValuesInput"> |
||||
<div class="advanced-filters--filter-value" [ngSwitch]="filter.currentSchema.values.type"> |
||||
<filter-integer-value *ngSwitchCase="'[1]Integer'" |
||||
(filterChanged)="onFilterUpdated($event)" |
||||
[filter]="filter"></filter-integer-value> |
||||
|
||||
<filter-date-value *ngSwitchCase="'[1]Date'" |
||||
(filterChanged)="onFilterUpdated($event)" |
||||
[filter]="filter"></filter-date-value> |
||||
|
||||
<filter-dates-value *ngSwitchCase="'[2]Date'" |
||||
(filterChanged)="onFilterUpdated($event)" |
||||
[filter]="filter"></filter-dates-value> |
||||
|
||||
<filter-date-time-value *ngSwitchCase="'[1]DateTime'" |
||||
(filterChanged)="onFilterUpdated($event)" |
||||
[filter]="filter"></filter-date-time-value> |
||||
|
||||
<filter-date-times-value *ngSwitchCase="'[2]DateTime'" |
||||
(filterChanged)="onFilterUpdated($event)" [filter]="filter"></filter-date-times-value> |
||||
|
||||
<filter-string-value *ngSwitchCase="'[1]String'" |
||||
(filterChanged)="onFilterUpdated($event)" |
||||
[filter]="filter"></filter-string-value> |
||||
|
||||
<filter-string-value *ngSwitchCase="'[1]Float'" |
||||
(filterChanged)="onFilterUpdated($event)" |
||||
[filter]="filter"></filter-string-value> |
||||
|
||||
<filter-boolean-value *ngSwitchCase="'[1]Boolean'" |
||||
(filterChanged)="onFilterUpdated($event)" |
||||
[filter]="filter"></filter-boolean-value> |
||||
|
||||
<filter-toggled-multiselect-value *ngSwitchDefault |
||||
(filterChanged)="onFilterUpdated($event)" |
||||
[filter]="filter"></filter-toggled-multiselect-value> |
||||
</div> |
||||
</ng-container> |
||||
|
||||
<div class="advanced-filters--filter-value" *ngIf="!showValuesInput"> |
||||
</div> |
||||
|
||||
<div class="advanced-filters--remove-filter"> |
||||
<accessible-by-keyboard (execute)="removeThisFilter()"> |
||||
<op-icon icon-classes="icon-close advanced-filters--remove-filter-icon" |
||||
[icon-title]="text.button_delete"></op-icon> |
||||
</accessible-by-keyboard> |
||||
</div> |
||||
</ng-container> |
@ -0,0 +1,85 @@ |
||||
// -- 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 {WorkPackageTableFiltersService} from '../../wp-fast-table/state/wp-table-filters.service'; |
||||
import {Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output} from '@angular/core'; |
||||
import {I18nToken} from 'core-app/angular4-transition-utils'; |
||||
import WorkPackageFiltersService from 'core-components/filters/wp-filters/wp-filters.service'; |
||||
import {QueryFilterResource} from 'core-app/modules/hal/resources/query-filter-resource'; |
||||
import {AngularTrackingHelpers} from 'core-components/angular/tracking-functions'; |
||||
|
||||
@Component({ |
||||
selector: '[query-filter]', |
||||
template: require('!!raw-loader!./query-filter.component.html') |
||||
}) |
||||
export class QueryFilterComponent implements OnInit, OnDestroy { |
||||
@Input() public filter:QueryFilterResource; |
||||
@Output() public filterChanged = new EventEmitter<QueryFilterResource>(); |
||||
@Output() public deactivateFilter = new EventEmitter<QueryFilterResource>(); |
||||
|
||||
public availableOperators:any; |
||||
public showValuesInput:boolean = false; |
||||
public eeShowBanners:boolean = false; |
||||
public trackByHref = AngularTrackingHelpers.halHref; |
||||
public compareByHref = AngularTrackingHelpers.compareByHref; |
||||
|
||||
public text = { |
||||
open_filter: this.I18n.t('js.filter.description.text_open_filter'), |
||||
close_filter: this.I18n.t('js.filter.description.text_close_filter'), |
||||
label_filter_add: this.I18n.t('js.work_packages.label_filter_add'), |
||||
upsale_for_more: this.I18n.t('js.filter.upsale_for_more'), |
||||
upsale_link: this.I18n.t('js.filter.upsale_link'), |
||||
button_delete: this.I18n.t('js.button_delete'), |
||||
}; |
||||
|
||||
constructor(readonly wpTableFilters:WorkPackageTableFiltersService, |
||||
readonly wpFiltersService:WorkPackageFiltersService, |
||||
@Inject(I18nToken) readonly I18n:op.I18n) { |
||||
} |
||||
|
||||
public onFilterUpdated(filter:QueryFilterResource) { |
||||
this.filter = filter; |
||||
this.showValuesInput = this.filter.currentSchema.isValueRequired(); |
||||
this.filterChanged.emit(this.filter); |
||||
} |
||||
|
||||
public removeThisFilter() { |
||||
this.deactivateFilter.emit(this.filter); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.eeShowBanners = angular.element('body').hasClass('ee-banners-visible'); |
||||
this.availableOperators = this.filter.schema.availableOperators; |
||||
this.showValuesInput = this.filter.currentSchema.isValueRequired(); |
||||
} |
||||
|
||||
ngOnDestroy() { |
||||
// Nothing to do
|
||||
} |
||||
} |
||||
|
@ -1,67 +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 {filtersModule} from '../../../angular-modules'; |
||||
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource'; |
||||
import {QueryFilterInstanceSchemaResource} from 'core-app/modules/hal/resources/query-filter-instance-schema-resource'; |
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; |
||||
import {QueryOperatorResource} from 'core-app/modules/hal/resources/query-operator-resource'; |
||||
import {WorkPackageTableFiltersService} from '../../wp-fast-table/state/wp-table-filters.service'; |
||||
|
||||
function queryFilterDirective($animate:any, |
||||
wpTableFilters:WorkPackageTableFiltersService) { |
||||
return { |
||||
restrict: 'A', |
||||
scope: true, |
||||
link: function (scope:any, element:ng.IAugmentedJQuery) { |
||||
$animate.enabled(false, element); |
||||
|
||||
scope.$watchCollection('filter.values', function (values: any, oldValues: any) { |
||||
if (!_.isEqual(values, oldValues)) { |
||||
putStateIfComplete(); |
||||
} |
||||
}); |
||||
|
||||
scope.availableOperators = scope.filter.schema.availableOperators; |
||||
|
||||
scope.$watchCollection('filter.operator', function(operator:QueryOperatorResource, oldOperator:QueryOperatorResource) { |
||||
scope.showValuesInput = scope.filter.currentSchema.isValueRequired(); |
||||
|
||||
if (!_.isEqual(operator, oldOperator)) { |
||||
putStateIfComplete(); |
||||
} |
||||
}); |
||||
|
||||
function putStateIfComplete() { |
||||
wpTableFilters.replaceIfComplete(scope.filters); |
||||
} |
||||
} |
||||
}; |
||||
} |
||||
|
||||
filtersModule.directive('queryFilter', queryFilterDirective); |
@ -1,54 +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.
|
||||
//++
|
||||
|
||||
// This Angular directive will act as an interface to the "upgraded" AngularJS component
|
||||
// query-filters
|
||||
import { |
||||
Directive, DoCheck, ElementRef, Inject, Injector, OnChanges, OnDestroy, |
||||
OnInit, SimpleChanges |
||||
} from '@angular/core'; |
||||
import {UpgradeComponent} from '@angular/upgrade/static'; |
||||
|
||||
@Directive({selector: 'ng1-query-filters-wrapper'}) |
||||
export class Ng1QueryFiltersComponentWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { |
||||
|
||||
constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) { |
||||
// We must pass the name of the directive as used by AngularJS to the super
|
||||
super('queryFilters', elementRef, injector); |
||||
} |
||||
|
||||
// For this class to work when compiled with AoT, we must implement these lifecycle hooks
|
||||
// because the AoT compiler will not realise that the super class implements them
|
||||
ngOnInit() { super.ngOnInit(); } |
||||
|
||||
ngOnChanges(changes:SimpleChanges) { super.ngOnChanges(changes); } |
||||
|
||||
ngDoCheck() { super.ngDoCheck(); } |
||||
|
||||
ngOnDestroy() { super.ngOnDestroy(); } |
||||
} |
@ -0,0 +1,61 @@ |
||||
<fieldset id="filters" *ngIf="filters && filters.current" class="advanced-filters--container"> |
||||
<legend [textContent]="text.selected_filter_list"></legend> |
||||
|
||||
<a *ngIf="showCloseFilter" |
||||
[attr.title]="text.close_form" |
||||
class="advanced-filters--close icon-context icon-close" |
||||
(click)="closeFilter()"> |
||||
</a> |
||||
|
||||
<ul class="advanced-filters--filters"> |
||||
<ng-container *ngFor="let filter of filters.current; let index = index"> |
||||
<li id="filter_{{filter.id}}" |
||||
query-filter |
||||
[filter]="filter" |
||||
(deactivateFilter)="deactivateFilter($event)" |
||||
(filterChanged)="filtersChanged.emit(filters)" |
||||
class="advanced-filters--filter"> |
||||
</li> |
||||
</ng-container> |
||||
|
||||
<li class="advanced-filters--spacer" *ngIf="filters.current.length > 0"></li> |
||||
|
||||
<li class="advanced-filters--add-filter"> |
||||
<!-- Add filters --> |
||||
<label for="add_filter_select" aria-hidden="true" class="advanced-filters--add-filter-label"> |
||||
<op-icon icon-classes="icon-add icon4"></op-icon> |
||||
{{ text.label_filter_add }}: |
||||
</label> |
||||
<label for="add_filter_select" class="hidden-for-sighted"> |
||||
{{ text.label_filter_add }} |
||||
{{ text.open_filter }} |
||||
{{ text.close_filter }} |
||||
</label> |
||||
|
||||
<div class="advanced-filters--add-filter-value"> |
||||
<select class="advanced-filters--select" |
||||
id="add_filter_select" |
||||
[ngModel]="filterToBeAdded" |
||||
(ngModelChange)="onFilterAdded($event)"> |
||||
<option [textContent]="text.please_select" value="" disabled></option> |
||||
<option *ngFor="let filter of remainingFilters" |
||||
[textContent]="filter.name" |
||||
[ngValue]="filter"> |
||||
</option> |
||||
</select> |
||||
</div> |
||||
|
||||
<div class="advanced-filters--add-filter-info" |
||||
*ngIf="eeShowBanners"> |
||||
<div class="notification-box"> |
||||
<div class="notification-box--content"> |
||||
{{ text.upsale_for_more }} |
||||
<a href="https://www.openproject.org/enterprise-edition/?op_edtion=community-edition&op_referrer=wp-filter#filters" |
||||
target='blank' |
||||
[textContent]="text.upsale_link"></a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
</fieldset> |
@ -0,0 +1,139 @@ |
||||
//-- 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 {WorkPackageTableFiltersService} from '../../wp-fast-table/state/wp-table-filters.service'; |
||||
import WorkPackageFiltersService from "../../filters/wp-filters/wp-filters.service"; |
||||
import {Component, Inject, Input, OnChanges, OnDestroy, OnInit, Output} from '@angular/core'; |
||||
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource'; |
||||
import {I18nToken} from 'core-app/angular4-transition-utils'; |
||||
import {componentDestroyed} from 'ng2-rx-componentdestroyed'; |
||||
import {WorkPackageTableFilters} from 'core-components/wp-fast-table/wp-table-filters'; |
||||
import {QueryFilterResource} from 'core-app/modules/hal/resources/query-filter-resource'; |
||||
import {DebouncedEventEmitter} from 'core-components/angular/debounced-event-emitter'; |
||||
|
||||
const ADD_FILTER_SELECT_INDEX = -1; |
||||
|
||||
|
||||
@Component({ |
||||
selector: 'query-filters', |
||||
template: require('!!raw-loader!./query-filters.component.html') |
||||
}) |
||||
export class QueryFiltersComponent implements OnInit, OnChanges, OnDestroy { |
||||
@Input() public filters:WorkPackageTableFilters; |
||||
@Input() public showCloseFilter:boolean = false; |
||||
@Output() public filtersChanged = new DebouncedEventEmitter<WorkPackageTableFilters>(componentDestroyed(this)); |
||||
|
||||
|
||||
public filterToBeAdded:QueryFilterResource|undefined; |
||||
public remainingFilters:any[] = []; |
||||
public eeShowBanners:boolean = false; |
||||
public focusElementIndex:number = 0; |
||||
|
||||
public text = { |
||||
open_filter: this.I18n.t('js.filter.description.text_open_filter'), |
||||
label_filter_add: this.I18n.t('js.work_packages.label_filter_add'), |
||||
close_filter: this.I18n.t('js.filter.description.text_close_filter'), |
||||
upsale_for_more: this.I18n.t('js.filter.upsale_for_more'), |
||||
upsale_link: this.I18n.t('js.filter.upsale_link'), |
||||
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') |
||||
}; |
||||
|
||||
constructor(readonly wpTableFilters:WorkPackageTableFiltersService, |
||||
readonly wpFiltersService:WorkPackageFiltersService, |
||||
@Inject(I18nToken) readonly I18n:op.I18n) { |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.eeShowBanners = angular.element('body').hasClass('ee-banners-visible'); |
||||
} |
||||
|
||||
ngOnDestroy() { |
||||
// Nothing to do.
|
||||
} |
||||
|
||||
ngOnChanges() { |
||||
this.updateRemainingFilters(); |
||||
} |
||||
|
||||
public onFilterAdded(filterToBeAdded:QueryFilterResource) { |
||||
if (filterToBeAdded) { |
||||
let newFilter = this.filters.add(filterToBeAdded); |
||||
this.filterToBeAdded = undefined; |
||||
|
||||
const index = this.currentFilterLength(); |
||||
this.updateFilterFocus(index); |
||||
this.updateRemainingFilters(); |
||||
|
||||
this.filtersChanged.emit(this.filters); |
||||
} |
||||
} |
||||
|
||||
public closeFilter() { |
||||
this.wpFiltersService.toggleVisibility(); |
||||
} |
||||
|
||||
public deactivateFilter(removedFilter:QueryFilterInstanceResource) { |
||||
let index = this.filters.current.indexOf(removedFilter); |
||||
|
||||
this.filters.remove(removedFilter); |
||||
if (removedFilter.isCompletelyDefined()) { |
||||
this.filtersChanged.emit(this.filters); |
||||
} |
||||
|
||||
this.updateFilterFocus(index); |
||||
this.updateRemainingFilters(); |
||||
} |
||||
|
||||
private updateRemainingFilters() { |
||||
this.remainingFilters = _.sortBy(this.filters.remainingFilters, 'name'); |
||||
} |
||||
|
||||
private updateFilterFocus(index:number) { |
||||
var activeFilterCount = this.currentFilterLength(); |
||||
|
||||
if (activeFilterCount === 0) { |
||||
this.focusElementIndex = ADD_FILTER_SELECT_INDEX; |
||||
} else { |
||||
const filterIndex = (index < activeFilterCount) ? index : activeFilterCount - 1; |
||||
const filter = this.currentFilterAt(filterIndex); |
||||
this.focusElementIndex = this.filters.current.indexOf(filter); |
||||
} |
||||
} |
||||
|
||||
public currentFilterLength() { |
||||
return this.filters.current.length; |
||||
} |
||||
|
||||
public currentFilterAt(index:number) { |
||||
return this.filters.current[index]; |
||||
} |
||||
|
||||
} |
@ -1,125 +0,0 @@ |
||||
<fieldset id="filters" class="advanced-filters--container"> |
||||
<legend ng-bind="I18n.t('js.label_selected_filter_list')"></legend> |
||||
|
||||
<a title="{{ ::I18n.t('js.close_form_title') }}" class="advanced-filters--close icon-context icon-close" ng-click="closeFilter()"></a> |
||||
<ul class="advanced-filters--filters"> |
||||
<li query-filter |
||||
ng-repeat="filter in filters.current" |
||||
id="filter_{{filter.id}}" |
||||
class="advanced-filters--filter"> |
||||
|
||||
<!-- Name --> |
||||
<label for="operators-{{filter.id}}" |
||||
class="advanced-filters--filter-name" |
||||
title="{{::filter.name}}"> |
||||
{{ ::filter.name }} |
||||
</label> |
||||
|
||||
<!-- Operator --> |
||||
<div class="advanced-filters--filter-operator"> |
||||
|
||||
<label for="operators-{{filter.id}}" class="hidden-for-sighted"> |
||||
{{ ::filter.name }} |
||||
{{ ::I18n.t('js.filter.description.text_open_filter') }} |
||||
</label> |
||||
|
||||
<select require |
||||
focus="{{$index == focusElementIndex}}" |
||||
class="advanced-filters--select" |
||||
id="operators-{{filter.id}}" |
||||
name="op[{{filter.id}}]" |
||||
ng-model="filter.operator" |
||||
style="vertical-align: top;" |
||||
ng-disabled="isLoading" |
||||
ng-options="operator.name for operator in availableOperators track by operator.href"> |
||||
</select> |
||||
</div> |
||||
|
||||
<!-- Values --> |
||||
<div class="advanced-filters--filter-value" |
||||
ng-if="showValuesInput" |
||||
ng-switch="filter.currentSchema.values.type"> |
||||
|
||||
<filter-integer-value ng-switch-when="[1]Integer" |
||||
filter="filter"> |
||||
</filter-integer-value> |
||||
|
||||
<filter-date-value ng-switch-when="[1]Date" |
||||
filter="filter"> |
||||
</filter-date-value> |
||||
|
||||
<filter-dates-value ng-switch-when="[2]Date" |
||||
filter="filter"> |
||||
</filter-dates-value> |
||||
|
||||
<filter-date-time-value ng-switch-when="[1]DateTime" |
||||
filter="filter"> |
||||
</filter-date-time-value> |
||||
|
||||
<filter-date-times-value ng-switch-when="[2]DateTime" |
||||
filter="filter"> |
||||
</filter-date-times-value> |
||||
|
||||
<filter-string-value ng-switch-when="[1]String" |
||||
filter="filter"> |
||||
</filter-string-value> |
||||
|
||||
<filter-string-value ng-switch-when="[1]Float" |
||||
filter="filter"> |
||||
</filter-string-value> |
||||
|
||||
<filter-boolean-value ng-switch-when="[1]Boolean" |
||||
filter="filter"> |
||||
</filter-boolean-value> |
||||
|
||||
<filter-toggled-multiselect-value ng-switch-default |
||||
filter="filter"> |
||||
</filter-toggled-multiselect-value> |
||||
</div> |
||||
|
||||
<div class="advanced-filters--filter-value" |
||||
ng-if="!showValuesInput"> |
||||
</div> |
||||
|
||||
<div class="advanced-filters--remove-filter"> |
||||
<accessible-by-keyboard execute="deactivateFilter(filter)"> |
||||
<op-icon icon-classes="icon-close advanced-filters--remove-filter-icon" icon-title="{{I18n.t('js.button_delete')}}"></op-icon> |
||||
</accessible-by-keyboard> |
||||
</div> |
||||
|
||||
</li> |
||||
|
||||
<li class="advanced-filters--spacer" ng-if="filters.current.length > 0"></li> |
||||
|
||||
<li class="advanced-filters--add-filter"> |
||||
<!-- Add filters --> |
||||
<label for="add_filter_select" aria-hidden="true" class="advanced-filters--add-filter-label"> |
||||
<op-icon icon-classes="icon-add icon4"></op-icon> |
||||
{{ I18n.t('js.work_packages.label_filter_add') }}: |
||||
</label> |
||||
<label for="add_filter_select" class="hidden-for-sighted"> |
||||
{{ I18n.t('js.work_packages.label_filter_add') }} |
||||
{{ I18n.t('js.filter.description.text_open_filter') }} |
||||
{{ I18n.t('js.filter.description.text_close_filter') }} |
||||
</label> |
||||
|
||||
<div class="advanced-filters--add-filter-value"> |
||||
<select class="advanced-filters--select" |
||||
id="add_filter_select" |
||||
focus="{{focusElementIndex == -1}}" |
||||
ng-model="filterToBeAdded" |
||||
ng-options="filter.name for filter in remainingFilters | orderBy: 'name'" /> |
||||
</select> |
||||
</div> |
||||
|
||||
<div class="advanced-filters--add-filter-info" ng-if="eeShowBanners"> |
||||
<div class="notification-box"> |
||||
<div class="notification-box--content"> |
||||
{{ I18n.t('js.filter.upsale_for_more') }} |
||||
<a href="https://www.openproject.org/enterprise-edition/?op_edtion=community-edition&op_referrer=wp-filter#filters" target='blank' ng-bind="I18n.t('js.filter.upsale_link')"></a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
</fieldset> |
@ -1,127 +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 {filtersModule} from '../../../angular-modules'; |
||||
import {QueryFilterInstanceSchemaResource} from 'core-app/modules/hal/resources/query-filter-instance-schema-resource' |
||||
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource' |
||||
import {QueryFilterResource} from 'core-app/modules/hal/resources/query-filter-resource' |
||||
import {QueryResource} from 'core-app/modules/hal/resources/query-resource' |
||||
import {FormResource} from 'core-app/modules/hal/resources/form-resource' |
||||
import {WorkPackageTableFiltersService} from '../../wp-fast-table/state/wp-table-filters.service'; |
||||
import WorkPackageFiltersService from "../../filters/wp-filters/wp-filters.service"; |
||||
|
||||
function queryFiltersDirective($timeout:ng.ITimeoutService, |
||||
I18n:op.I18n, |
||||
wpTableFilters:WorkPackageTableFiltersService, |
||||
wpFiltersService:WorkPackageFiltersService, |
||||
ADD_FILTER_SELECT_INDEX:any) { |
||||
|
||||
return { |
||||
restrict: 'E', |
||||
scope: {}, |
||||
templateUrl: '/components/filters/query-filters/query-filters.directive.html', |
||||
|
||||
link: function (scope:any) { |
||||
scope.I18n = I18n; |
||||
scope.focusElementIndex; |
||||
scope.remainingFilters = []; |
||||
|
||||
scope.filters; |
||||
|
||||
scope.eeShowBanners = angular.element('body').hasClass('ee-banners-visible'); |
||||
|
||||
wpTableFilters.observeOnScope(scope).subscribe(initialize); |
||||
|
||||
scope.$watch('filterToBeAdded', function (filter:any) { |
||||
if (filter) { |
||||
scope.filterToBeAdded = undefined; |
||||
let newFilter = scope.filters.add(filter); |
||||
var index = currentFilterLength(); |
||||
updateFilterFocus(index); |
||||
updateRemainingFilters(); |
||||
|
||||
wpTableFilters.replaceIfComplete(scope.filters); |
||||
} |
||||
}); |
||||
|
||||
scope.closeFilter = function () { |
||||
wpFiltersService.toggleVisibility(); |
||||
} |
||||
|
||||
scope.deactivateFilter = function (removedFilter:QueryFilterInstanceResource) { |
||||
let index = scope.filters.current.indexOf(removedFilter); |
||||
|
||||
if (removedFilter.isCompletelyDefined()) { |
||||
wpTableFilters.remove(removedFilter); |
||||
} else { |
||||
scope.filters.remove(removedFilter); |
||||
} |
||||
|
||||
updateFilterFocus(index); |
||||
|
||||
updateRemainingFilters(); |
||||
}; |
||||
|
||||
function initialize() { |
||||
scope.filters = wpTableFilters.currentState; |
||||
|
||||
updateRemainingFilters(); |
||||
} |
||||
|
||||
function updateRemainingFilters() { |
||||
scope.remainingFilters = scope.filters.remainingFilters; |
||||
} |
||||
|
||||
function updateFilterFocus(index:number) { |
||||
var activeFilterCount = currentFilterLength(); |
||||
|
||||
if (activeFilterCount == 0) { |
||||
scope.focusElementIndex = ADD_FILTER_SELECT_INDEX; |
||||
} else { |
||||
var filterIndex = (index < activeFilterCount) ? index : activeFilterCount - 1; |
||||
var filter = currentFilterAt(filterIndex); |
||||
scope.focusElementIndex = scope.filters.current.indexOf(filter); |
||||
} |
||||
|
||||
$timeout(function () { |
||||
scope.$broadcast('updateFocus'); |
||||
}, 300); |
||||
} |
||||
|
||||
function currentFilterLength() { |
||||
return scope.filters.current.length; |
||||
} |
||||
|
||||
function currentFilterAt(index:number) { |
||||
return scope.filters.current[index]; |
||||
} |
||||
} |
||||
}; |
||||
} |
||||
|
||||
filtersModule.directive('queryFilters', queryFiltersDirective); |
@ -1,115 +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 {wpControllersModule} from '../../../angular-modules'; |
||||
import {WorkPackageTableColumnsService} from '../../wp-fast-table/state/wp-table-columns.service'; |
||||
import {QueryColumn} from '../../wp-query/query-column'; |
||||
|
||||
function ColumnsModalController(this:any, |
||||
$scope:any, |
||||
$timeout:ng.ITimeoutService, |
||||
I18n:op.I18n, |
||||
columnsModal:any, |
||||
wpTableColumns:WorkPackageTableColumnsService, |
||||
ConfigurationService:any) { |
||||
var vm = this; |
||||
|
||||
vm.name = 'Columns'; |
||||
vm.closeMe = columnsModal.deactivate; |
||||
|
||||
vm.selectedColumns = []; |
||||
vm.availableColumns = []; |
||||
vm.unusedColumns = []; |
||||
|
||||
vm.text = { |
||||
closePopup: I18n.t('js.close_popup_title'), |
||||
columnsLabel: I18n.t('js.label_columns'), |
||||
selectedColumns: I18n.t('js.description_selected_columns'), |
||||
multiSelectLabel: I18n.t('js.work_packages.label_column_multiselect'), |
||||
applyButton: I18n.t('js.modals.button_apply'), |
||||
cancelButton: I18n.t('js.modals.button_cancel'), |
||||
upsaleRelationColumns: I18n.t('js.modals.upsale_relation_columns'), |
||||
upsaleRelationColumnsLink: I18n.t('js.modals.upsale_relation_columns_link') |
||||
}; |
||||
|
||||
vm.availableColumns = wpTableColumns.all; |
||||
vm.unusedColumns = wpTableColumns.unused; |
||||
vm.selectedColumns = angular.copy(wpTableColumns.getColumns()); |
||||
|
||||
vm.impaired = ConfigurationService.accessibilityModeEnabled(); |
||||
vm.selectedColumnMap = {}; |
||||
|
||||
vm.eeShowBanners = angular.element('body').hasClass('ee-banners-visible'); |
||||
|
||||
if (vm.impaired) { |
||||
vm.selectedColumns.forEach((column:QueryColumn) => { |
||||
vm.selectedColumnMap[column.id] = true; |
||||
}); |
||||
} |
||||
|
||||
vm.updateSelectedColumns = () => { |
||||
wpTableColumns.setColumns(vm.selectedColumns); |
||||
|
||||
columnsModal.deactivate(); |
||||
}; |
||||
|
||||
/** |
||||
* When a column is removed from the selection it becomes unused and hence available for |
||||
* selection again. When a column is added to the selection it becomes used and is |
||||
* therefore unavailable for selection. |
||||
* |
||||
* This function updates the unused columns according to the currently selected columns. |
||||
* |
||||
* @param selectedColumns Columns currently selected through the multi select box. |
||||
*/ |
||||
vm.updateUnusedColumns = (selectedColumns:QueryColumn[]) => { |
||||
vm.unusedColumns = _.differenceBy(vm.availableColumns, selectedColumns, '$href'); |
||||
}; |
||||
|
||||
vm.setSelectedColumn = (column:QueryColumn) => { |
||||
if (vm.selectedColumnMap[column.id]) { |
||||
vm.selectedColumns.push(column); |
||||
} |
||||
else { |
||||
_.remove(vm.selectedColumns, (c: any) => c.id === column.id); |
||||
} |
||||
}; |
||||
|
||||
//hack to prevent dragging of close icons
|
||||
$timeout(() => { |
||||
angular.element('.columns-modal-content .ui-select-match-close').on('dragstart', event => { |
||||
event.preventDefault(); |
||||
}); |
||||
}); |
||||
|
||||
$scope.$on('uiSelectSort:change', (event:any, args:any) => { |
||||
vm.selectedColumns = args.array; |
||||
}); |
||||
} |
||||
|
||||
wpControllersModule.controller('ColumnsModalController', ColumnsModalController); |
@ -1,89 +0,0 @@ |
||||
<div class="ng-modal-window columns-modal loading-indicator--location" |
||||
data-indicator-name="modal"> |
||||
<div class="ng-modal-inner" tabindex="0"> |
||||
<div class="modal-header"> |
||||
<a> |
||||
<i |
||||
class="icon-close" |
||||
ng-click="$ctrl.closeMe()" |
||||
title="{{ ::$ctrl.text.closePopup }}"> |
||||
</i> |
||||
</a> |
||||
</div> |
||||
|
||||
<h3>{{ ::$ctrl.text.columnsLabel }}</h3> |
||||
|
||||
<div class="columns-modal-content select2-modal-content" |
||||
ng-if="!$ctrl.impaired"> |
||||
|
||||
<label |
||||
for="selected_columns" |
||||
class="hidden-for-sighted"> |
||||
{{ ::$ctrl.text.selectedColumns }} |
||||
</label> |
||||
|
||||
<ui-select |
||||
sortable="true" |
||||
ng-model="$ctrl.selectedColumns" |
||||
theme="select2" |
||||
id="selected_columns" |
||||
focus |
||||
multiple |
||||
aria-labelledby="column_multiselect_description" |
||||
title="{{ ::$ctrl.text.columnsLabel }}"> |
||||
<ui-select-match>{{ $item.name }}</ui-select-match> |
||||
<ui-select-choices |
||||
repeat="column in $ctrl.unusedColumns | filter: { name: $select.search } | orderBy:'name'" |
||||
refresh="$ctrl.updateUnusedColumns($select.selected)" |
||||
refresh-delay="0"> |
||||
<div ng-bind-html="column.name | highlight: $select.search"></div> |
||||
</ui-select-choices> |
||||
</ui-select> |
||||
|
||||
<span class="tooltip--right -multiline" tabindex="0" title |
||||
data-tooltip="{{ ::$ctrl.text.multiSelectLabel }}" |
||||
aria-labelledby="column_multiselect_description"> |
||||
<op-icon icon-classes="icon icon-help1"></op-icon> |
||||
</span> |
||||
<div class="hidden-for-sighted" id="column_multiselect_description"> |
||||
{{ ::$ctrl.text.multiSelectLabel }} |
||||
</div> |
||||
</div> |
||||
|
||||
<div |
||||
class="columns-modal-content select2-modal-content" |
||||
ng-if="$ctrl.impaired"> |
||||
<label |
||||
for="selected_columns" |
||||
class="hidden-for-sighted"> |
||||
{{ ::$ctrl.text.selectedColumns }} |
||||
</label> |
||||
|
||||
<div ng-repeat="column in $ctrl.availableColumns | orderBy:'name'"> |
||||
<label class="form--label-with-check-box" for="column-{{column.id}}"> |
||||
<div class="form--check-box-container"> |
||||
<input id="column-{{column.id}}" |
||||
type="checkbox" |
||||
title="{{ column.name }}" |
||||
ng-model="$ctrl.selectedColumnMap[column.id]" |
||||
ng-change="$ctrl.setSelectedColumn(column)" |
||||
focus="$first" /> |
||||
</div> |
||||
{{column.name}} |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<div ng-if="$ctrl.eeShowBanners" class="ee-relation-columns-upsale"> |
||||
{{$ctrl.text.upsaleRelationColumns}} |
||||
<a href="https://www.openproject.org/enterprise-edition/?op_edtion=community-edition&op_referrer=wp-list-columns#relations" target='blank' ng-bind="$ctrl.text.upsaleRelationColumnsLink"></a> |
||||
</div> |
||||
<div> |
||||
<button class="button -highlight" ng-click="$ctrl.updateSelectedColumns()"> |
||||
{{ ::$ctrl.text.applyButton }} |
||||
</button> |
||||
<button class="button" ng-click="$ctrl.closeMe()"> |
||||
{{ ::$ctrl.text.cancelButton }} |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -1,54 +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 {wpControllersModule} from '../../../angular-modules'; |
||||
import {WorkPackageTableGroupByService} from '../../wp-fast-table/state/wp-table-group-by.service'; |
||||
|
||||
function GroupingModalController(this:any, |
||||
$scope:any, |
||||
groupingModal:any, |
||||
wpTableGroupBy:WorkPackageTableGroupByService, |
||||
I18n:op.I18n) { |
||||
this.name = 'GroupBy'; |
||||
this.closeMe = groupingModal.deactivate; |
||||
let emptyOption = {title: I18n.t('js.inplace.clear_value_label')}; |
||||
|
||||
$scope.vm = {}; |
||||
|
||||
wpTableGroupBy.onReady($scope).then(() => { |
||||
$scope.vm.available = wpTableGroupBy.available; |
||||
$scope.vm.current = wpTableGroupBy.current; |
||||
}); |
||||
|
||||
$scope.updateGroupBy = () => { |
||||
wpTableGroupBy.set($scope.vm.current); |
||||
groupingModal.deactivate(); |
||||
}; |
||||
} |
||||
|
||||
wpControllersModule.controller('GroupingModalController', GroupingModalController); |
@ -1,47 +0,0 @@ |
||||
<div class="ng-modal-window"> |
||||
<div class="ng-modal-inner" tabindex="0"> |
||||
<div class="modal-header"> |
||||
<a> |
||||
<i |
||||
class="icon-close" |
||||
ng-click="$ctrl.closeMe()" |
||||
title="{{ ::I18n.t('js.close_popup_title') }}"> |
||||
</i> |
||||
</a> |
||||
</div> |
||||
|
||||
<h3>{{ ::I18n.t('js.label_group_by') }}</h3> |
||||
|
||||
<form> |
||||
<div class="form--field -full-width"> |
||||
<label |
||||
for="selected_columns_new" |
||||
class="form--label hidden-for-sighted"> |
||||
{{ ::I18n.t('js.label_group_by') }} |
||||
</label> |
||||
<div class="form--field-container"> |
||||
<div class="form--select-container"> |
||||
<select |
||||
ng-model="vm.current" |
||||
focus="true" |
||||
title="{{ fieldController.editTitle }}" |
||||
id="selected_columns_new" |
||||
class="form--select" |
||||
ng-options="groupBy.name for groupBy in vm.available | orderBy: 'name' track by groupBy.$href"> |
||||
<option value="">{{::I18n.t('js.placeholders.default')}}</option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div> |
||||
<button class="button -highlight" ng-click="updateGroupBy()"> |
||||
{{ ::I18n.t('js.modals.button_apply') }} |
||||
</button> |
||||
<button class="button" ng-click="$ctrl.closeMe()"> |
||||
{{ ::I18n.t('js.modals.button_cancel') }} |
||||
</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</div> |
@ -1,40 +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 {wpControllersModule} from '../../../angular-modules'; |
||||
|
||||
function groupingModalService(btfModal:any) { |
||||
return btfModal({ |
||||
controller: 'GroupingModalController', |
||||
controllerAs: '$ctrl', |
||||
afterFocusOn: '#work-packages-settings-button', |
||||
templateUrl: '/components/modals/grouping-modal/grouping-modal.service.html' |
||||
}); |
||||
} |
||||
|
||||
wpControllersModule.factory('groupingModal', groupingModalService); |
@ -1,107 +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 {wpControllersModule} from '../../../angular-modules'; |
||||
import {WorkPackageTableSortByService} from '../../wp-fast-table/state/wp-table-sort-by.service'; |
||||
import { |
||||
QUERY_SORT_BY_ASC, |
||||
QUERY_SORT_BY_DESC, |
||||
QuerySortByResource |
||||
} from 'core-app/modules/hal/resources/query-sort-by-resource'; |
||||
import {QueryColumn} from '../../wp-query/query-column'; |
||||
|
||||
class SortModalObject { |
||||
constructor(public column: QueryColumn|null, |
||||
public direction: string) { |
||||
} |
||||
} |
||||
|
||||
function SortingModalController(this:any, |
||||
sortingModal:any, |
||||
$scope:any, |
||||
wpTableSortBy:WorkPackageTableSortByService, |
||||
I18n:op.I18n) { |
||||
this.name = 'Sorting'; |
||||
this.closeMe = sortingModal.deactivate; |
||||
|
||||
$scope.currentSortation = []; |
||||
$scope.availableColumns = []; |
||||
$scope.allColumns = []; |
||||
$scope.sortationObjects = []; |
||||
|
||||
wpTableSortBy.onReady($scope).then(() => { |
||||
$scope.currentSortation = wpTableSortBy.currentSortBys; |
||||
let availableSortation = wpTableSortBy.available; |
||||
let allColumns:QueryColumn[] = _.map(availableSortation, sort => sort.column); |
||||
$scope.allColumns = _.uniqBy(allColumns, '$href'); |
||||
|
||||
_.each($scope.currentSortation, sort => { |
||||
$scope.sortationObjects.push(new SortModalObject(sort.column, |
||||
sort.direction.$href)); |
||||
}); |
||||
|
||||
fillUpSortElements(); |
||||
}); |
||||
|
||||
function fillUpSortElements() { |
||||
while ($scope.sortationObjects.length < 3) { |
||||
$scope.sortationObjects.push(new SortModalObject(null, QUERY_SORT_BY_ASC)); |
||||
} |
||||
} |
||||
|
||||
$scope.$watchCollection('sortationObjects', () => $scope.updatedSelection()); |
||||
|
||||
$scope.updatedSelection = () => { |
||||
let usedColumns = _.map($scope.sortationObjects, (object:SortModalObject) => object.column); |
||||
$scope.availableColumns = _.differenceBy($scope.allColumns, usedColumns, '$href'); |
||||
}; |
||||
|
||||
$scope.availableColumnsAndCurrent = (column:SortModalObject) => { |
||||
return _.uniqBy(_.concat($scope.availableColumns, _.compact([column])), '$href'); |
||||
}; |
||||
|
||||
$scope.updateSortation = () => { |
||||
let sortElements = ($scope.sortationObjects as SortModalObject[]) |
||||
.filter(object => object.column) |
||||
.map(object => _.find(wpTableSortBy.available, availableSort => |
||||
availableSort.column.$href === object.column!.$href && |
||||
availableSort.direction.$href === object.direction |
||||
)); |
||||
|
||||
wpTableSortBy.set(_.compact(sortElements)); |
||||
|
||||
sortingModal.deactivate(); |
||||
}; |
||||
|
||||
$scope.availableDirections = [ |
||||
{$href: QUERY_SORT_BY_ASC, name: I18n.t('js.label_ascending')}, |
||||
{$href: QUERY_SORT_BY_DESC, name: I18n.t('js.label_descending')} |
||||
]; |
||||
} |
||||
|
||||
wpControllersModule.controller('SortingModalController', SortingModalController); |
@ -1,56 +0,0 @@ |
||||
<div class="ng-modal-window"> |
||||
<div class="ng-modal-inner modal-content" tabindex="0"> |
||||
<div class="modal-header"> |
||||
<a><op-icon icon-classes="icon-close" ng-click="$ctrl.closeMe()" title="{{ ::I18n.t('js.close_popup_title') }}"></op-icon></a></div> |
||||
|
||||
<h3>{{ ::I18n.t('js.label_sorting') }}</h3> |
||||
|
||||
<form name="modalSortingForm"> |
||||
<div id="modal-sorting" |
||||
class="modal-content-container loading-indicator--location" |
||||
data-indicator-name="sorting-modal"> |
||||
<div class="form--row modal-sorting-row-{{$index}}" ng-repeat="sort in sortationObjects"> |
||||
<div class="form--field -full-width"> |
||||
<label |
||||
for="modal-sorting-attribute-{{$index}}" |
||||
class="form--label hidden-for-sighted"> |
||||
{{ I18n.t('js.filter.sorting.criteria.' + { 1: 'one', 2: 'two', 3: 'three'}[$index + 1]) }} |
||||
</label> |
||||
<div class="form--field-container"> |
||||
<div class="form--select-container"> |
||||
<select |
||||
id="modal-sorting-attribute-{{$index}}" |
||||
ng-model="sort.column" |
||||
ng-change="updatedSelection()" |
||||
focus="!$index" |
||||
class="form--select" |
||||
ng-options="column.name for column in availableColumnsAndCurrent(sort.column) | orderBy:'name' track by column.$href"> |
||||
<option value="">{{::I18n.t('js.placeholders.default')}}</option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="form--field -full-width"> |
||||
<div class="form--field-container"> |
||||
<label class="option-label" ng-repeat="availableDirection in availableDirections"> |
||||
<input type="radio" |
||||
ng-model="sort.direction" |
||||
ng-value="availableDirection.$href" |
||||
name="modal-sorting-attribute-{{sort.column.name}}--sort-direction"> |
||||
{{availableDirection.name}} |
||||
</label> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<button class="button -highlight" |
||||
ng-disabled="modalSortingForm.$invalid" |
||||
ng-click="updateSortation()"> |
||||
{{ ::I18n.t('js.modals.button_apply') }} |
||||
</button> |
||||
<button class="button" ng-click="$ctrl.closeMe()"> |
||||
{{ ::I18n.t('js.modals.button_cancel') }} |
||||
</button> |
||||
</form> |
||||
</div> |
||||
</div> |
@ -1,40 +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 {wpControllersModule} from '../../../angular-modules'; |
||||
|
||||
function sortingModalService(btfModal:any) { |
||||
return btfModal({ |
||||
controller: 'SortingModalController', |
||||
controllerAs: '$ctrl', |
||||
afterFocusOn: '#work-packages-settings-button', |
||||
templateUrl: '/components/modals/sorting-modal/sorting-modal.service.html' |
||||
}); |
||||
} |
||||
|
||||
wpControllersModule.factory('sortingModal', sortingModalService); |
@ -1,75 +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 {wpControllersModule} from '../../../angular-modules'; |
||||
import {WorkPackageTableTimelineService} from '../../wp-fast-table/state/wp-table-timeline.service'; |
||||
import {WorkPackageTableColumnsService} from '../../wp-fast-table/state/wp-table-columns.service'; |
||||
|
||||
function TimelinesModalController(this:any, |
||||
timelinesModal:any, |
||||
$scope:any, |
||||
wpTableTimeline:WorkPackageTableTimelineService, |
||||
wpTableColumns:WorkPackageTableColumnsService, |
||||
I18n:op.I18n) { |
||||
this.name = 'Timelines'; |
||||
this.closeMe = timelinesModal.deactivate; |
||||
|
||||
$scope.text = { |
||||
apply: I18n.t('js.modals.button_apply'), |
||||
cancel: I18n.t('js.modals.button_cancel'), |
||||
close: I18n.t('js.close_popup_title'), |
||||
title: I18n.t('js.timelines.gantt_chart'), |
||||
labels: { |
||||
description: I18n.t('js.timelines.labels.description'), |
||||
bar: I18n.t('js.timelines.labels.bar'), |
||||
none: I18n.t('js.timelines.filter.noneSelection'), |
||||
left: I18n.t('js.timelines.labels.left'), |
||||
right: I18n.t('js.timelines.labels.right'), |
||||
farRight: I18n.t('js.timelines.labels.farRight') |
||||
} |
||||
}; |
||||
|
||||
// Current label models
|
||||
const labels = wpTableTimeline.labels; |
||||
$scope.labels = _.clone(labels); |
||||
|
||||
// Available labels
|
||||
const availableColumns = wpTableColumns |
||||
.allPropertyColumns |
||||
.sort((a, b) => a.name.localeCompare(b.name)); |
||||
|
||||
$scope.availableAttributes = [{ id: '', name: $scope.text.labels.none }].concat(availableColumns); |
||||
|
||||
// Save
|
||||
$scope.updateLabels = () => { |
||||
wpTableTimeline.updateLabels($scope.labels); |
||||
timelinesModal.deactivate(); |
||||
}; |
||||
} |
||||
|
||||
wpControllersModule.controller('TimelinesModalController', TimelinesModalController); |
@ -1,45 +0,0 @@ |
||||
<div class="ng-modal-window"> |
||||
<div class="ng-modal-inner modal-content"> |
||||
<div class="modal-header"> |
||||
<a><op-icon icon-classes="icon-close" ng-click="$ctrl.closeMe()" title="{{ ::text.close }}"></op-icon></a></div> |
||||
|
||||
<h3>{{ ::text.title }}</h3> |
||||
|
||||
<form name="modalTimelinesForm"> |
||||
<div id="modal-timelines" |
||||
class="modal-content-container"> |
||||
<p ng-bind="::text.labels.description"></p> |
||||
<section class="form--section"> |
||||
<div class="form--row" ng-repeat="(key, value) in labels"> |
||||
<div class="form--field"> |
||||
<label |
||||
for="modal-timelines-label-{{key}}" |
||||
class="form--label"> |
||||
{{ text.labels[key] }} |
||||
</label> |
||||
<div class="form--field-container"> |
||||
<div class="form--select-container"> |
||||
<select |
||||
id="modal-timelines-label-{{key}}" |
||||
ng-model="labels[key]" |
||||
focus="$first" |
||||
class="form--select" |
||||
ng-options="c.id as c.name for c in availableAttributes"> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</section> |
||||
</div> |
||||
<button class="button -highlight" |
||||
ng-bind="::text.apply" |
||||
ng-click="updateLabels()"> |
||||
</button> |
||||
<button class="button" |
||||
ng-bind="::text.cancel" |
||||
ng-click="$ctrl.closeMe()"> |
||||
</button> |
||||
</form> |
||||
</div> |
||||
</div> |
@ -1,40 +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 {wpControllersModule} from '../../../angular-modules'; |
||||
|
||||
function timelinesModalService(btfModal:any) { |
||||
return btfModal({ |
||||
controller: 'TimelinesModalController', |
||||
controllerAs: '$ctrl', |
||||
afterFocusOn: '#work-packages-settings-button', |
||||
templateUrl: '/components/modals/timelines-modal/timelines-modal.service.html' |
||||
}); |
||||
} |
||||
|
||||
wpControllersModule.factory('timelinesModal', timelinesModalService); |
@ -0,0 +1,45 @@ |
||||
import {ElementRef, OnInit} from '@angular/core'; |
||||
import {OpModalLocalsMap} from 'core-components/op-modals/op-modal.types'; |
||||
import {OpModalService} from 'core-components/op-modals/op-modal.service'; |
||||
|
||||
export abstract class OpModalComponent implements OnInit { |
||||
|
||||
/* Close on escape? */ |
||||
public closeOnEscape:boolean = true; |
||||
|
||||
/* Close on outside click */ |
||||
public closeOnOutsideClick:boolean = true; |
||||
|
||||
/* Reference to service */ |
||||
protected service:OpModalService = this.locals.service; |
||||
|
||||
public $element:JQuery; |
||||
|
||||
constructor(public locals:OpModalLocalsMap, readonly elementRef:ElementRef) { |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.$element = jQuery(this.elementRef.nativeElement); |
||||
} |
||||
|
||||
/** |
||||
* Called when the user attempts to close the modal window. |
||||
* The service will close this modal if this method returns true |
||||
* @returns {boolean} |
||||
*/ |
||||
public onClose():boolean { |
||||
this.afterFocusOn.focus(); |
||||
return true; |
||||
} |
||||
|
||||
public closeMe() { |
||||
this.service.close(); |
||||
} |
||||
|
||||
public onOpen(modalElement:JQuery) { |
||||
} |
||||
|
||||
protected get afterFocusOn():JQuery { |
||||
return this.$element; |
||||
} |
||||
} |
@ -0,0 +1,114 @@ |
||||
import { |
||||
ApplicationRef, |
||||
ComponentFactoryResolver, ComponentRef, |
||||
Inject, |
||||
Injectable, |
||||
Injector |
||||
} from '@angular/core'; |
||||
import {ComponentPortal, ComponentType, DomPortalOutlet, PortalInjector} from '@angular/cdk/portal'; |
||||
import {TransitionService} from '@uirouter/core'; |
||||
import {FocusHelperToken, OpModalLocalsToken} from 'core-app/angular4-transition-utils'; |
||||
import {OpModalComponent} from 'core-components/op-modals/op-modal.component'; |
||||
import {keyCodes} from 'core-components/common/keyCodes.enum'; |
||||
|
||||
@Injectable() |
||||
export class OpModalService { |
||||
public active:OpModalComponent|null = null; |
||||
|
||||
// Hold a reference to the DOM node we're using as a host
|
||||
private portalHostElement:HTMLElement; |
||||
// And a reference to the actual portal host interface on top of the element
|
||||
private bodyPortalHost:DomPortalOutlet; |
||||
|
||||
constructor(private componentFactoryResolver:ComponentFactoryResolver, |
||||
@Inject(FocusHelperToken) readonly FocusHelper:any, |
||||
private appRef:ApplicationRef, |
||||
private $transitions:TransitionService, |
||||
private injector:Injector) { |
||||
|
||||
const hostElement = this.portalHostElement = document.createElement('div'); |
||||
hostElement.classList.add('op-modals--overlay'); |
||||
document.body.appendChild(hostElement); |
||||
|
||||
// Listen to keyups on window to close context menus
|
||||
jQuery(window).keydown('keydown', (evt:JQueryKeyEventObject) => { |
||||
if (this.active && this.active.closeOnEscape && evt.which === keyCodes.ESCAPE) { |
||||
this.close(); |
||||
} |
||||
|
||||
return true; |
||||
}); |
||||
|
||||
// Listen to any click when should close outside modal
|
||||
jQuery(window).click((evt) => { |
||||
if (this.active && |
||||
this.active.closeOnOutsideClick && |
||||
!this.portalHostElement.contains(evt.target)) { |
||||
this.close(); |
||||
} |
||||
}); |
||||
|
||||
this.bodyPortalHost = new DomPortalOutlet( |
||||
hostElement, |
||||
this.componentFactoryResolver, |
||||
this.appRef, |
||||
this.injector |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Open a Modal reference and append it to the portal |
||||
*/ |
||||
public show<T extends OpModalComponent>(modal:ComponentType<T>, locals:any = {}):void { |
||||
this.close(); |
||||
|
||||
// Create a portal for the given component class and render it
|
||||
const portal = new ComponentPortal(modal, null, this.injectorFor(locals)); |
||||
const ref:ComponentRef<OpModalComponent> = this.bodyPortalHost.attach(portal) as ComponentRef<OpModalComponent>; |
||||
const instance = ref.instance as T; |
||||
this.active = instance; |
||||
this.portalHostElement.style.display = 'block'; |
||||
|
||||
setTimeout(() => { |
||||
// Focus on the first element
|
||||
this.active && this.active.onOpen(this.activeModal); |
||||
}); |
||||
} |
||||
|
||||
public isActive(modal:OpModalComponent) { |
||||
return this.active && this.active === modal; |
||||
} |
||||
|
||||
/** |
||||
* Closes currently open modal window |
||||
*/ |
||||
public close() { |
||||
// Detach any component currently in the portal
|
||||
if (this.active && this.active.onClose()) { |
||||
this.bodyPortalHost.detach(); |
||||
this.portalHostElement.style.display = 'none'; |
||||
this.active = null; |
||||
} |
||||
} |
||||
|
||||
public get activeModal():JQuery { |
||||
return jQuery(this.portalHostElement).find('.op-modal--container'); |
||||
} |
||||
|
||||
/** |
||||
* Create an augmented injector that is equal to this service's injector + the additional data |
||||
* passed into +show+. |
||||
* This allows callers to pass data into the newly created modal. |
||||
* |
||||
*/ |
||||
private injectorFor(data:any) { |
||||
const injectorTokens = new WeakMap(); |
||||
// Pass the service because otherwise we're getting a cyclic dependency between the portal
|
||||
// host service and the bound portal
|
||||
data.service = this; |
||||
|
||||
injectorTokens.set(OpModalLocalsToken, data); |
||||
|
||||
return new PortalInjector(this.injector, injectorTokens); |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
import {OpModalService} from 'core-components/op-modals/op-modal.service'; |
||||
|
||||
export interface OpModalLocalsMap { |
||||
service:OpModalService; |
||||
[key:string]:any; |
||||
}; |
||||
|
@ -0,0 +1,3 @@ |
||||
<div> |
||||
<ng-content></ng-content> |
||||
</div> |
@ -0,0 +1,120 @@ |
||||
// -- 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, ElementRef, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core'; |
||||
import {ConfigurationService} from 'core-components/common/config/configuration.service'; |
||||
import {TimezoneServiceToken} from 'core-app/angular4-transition-utils'; |
||||
import {DatePicker} from 'core-components/wp-edit/op-date-picker/datepicker'; |
||||
|
||||
@Component({ |
||||
selector: 'op-date-picker', |
||||
template: require('!!raw-loader!./op-date-picker.component.html') |
||||
}) |
||||
export class OpDatePickerComponent implements OnInit { |
||||
@Output() public onChange = new EventEmitter<string>(); |
||||
@Output() public onClose = new EventEmitter<string>(); |
||||
@Input() public initialDate?:String; |
||||
|
||||
private $element:JQuery; |
||||
private datePickerInstance:any; |
||||
private input:JQuery; |
||||
|
||||
public constructor(private elementRef:ElementRef, |
||||
private ConfigurationService:ConfigurationService, |
||||
@Inject(TimezoneServiceToken)private TimezoneService:any) { |
||||
} |
||||
|
||||
|
||||
ngOnInit() { |
||||
this.$element = jQuery(this.elementRef.nativeElement); |
||||
|
||||
// we don't want the date picker in the accessibility mode
|
||||
if (!this.ConfigurationService.accessibilityModeEnabled()) { |
||||
this.input = this.$element.find('input'); |
||||
this.setup(); |
||||
} |
||||
} |
||||
|
||||
public setup() { |
||||
this.input.focus(() => this.showDatePicker()); |
||||
this.input.keydown((event) => { |
||||
if (this.isEmpty()) { |
||||
this.datePickerInstance.clear(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private isEmpty():boolean { |
||||
return this.currentValue().trim() === ''; |
||||
} |
||||
|
||||
private currentValue() { |
||||
return this.input.val(); |
||||
} |
||||
|
||||
private callbackIfSet(name:'onChange' | 'onClose') { |
||||
if (this[name]) { |
||||
this[name].emit(this.currentValue()); |
||||
} |
||||
} |
||||
|
||||
private showDatePicker() { |
||||
let options:any = { |
||||
onSelect: (date:any) => { |
||||
this.datePickerInstance.hide(); |
||||
|
||||
let val = date; |
||||
|
||||
if (this.isEmpty()) { |
||||
val = null; |
||||
} |
||||
|
||||
this.input.val(val); |
||||
this.input.change(); |
||||
this.callbackIfSet('onChange'); |
||||
}, |
||||
onClose: () => this.callbackIfSet('onClose') |
||||
}; |
||||
|
||||
let initialValue; |
||||
if (this.isEmpty && this.initialDate) { |
||||
initialValue = this.TimezoneService.parseISODate(this.initialDate).toDate(); |
||||
} else { |
||||
initialValue = this.currentValue(); |
||||
} |
||||
|
||||
this.datePickerInstance = new DatePicker( |
||||
this.ConfigurationService, |
||||
this.TimezoneService, |
||||
this.input, |
||||
initialValue, |
||||
options |
||||
); |
||||
this.datePickerInstance.show(); |
||||
} |
||||
} |
@ -0,0 +1,123 @@ |
||||
/** |
||||
* A PortalOutlet that lets multiple components live for the lifetime of the outlet, |
||||
* allowing faster switching and persistent data. |
||||
*/ |
||||
import {BasePortalOutlet, ComponentPortal, PortalOutlet} from '@angular/cdk/portal'; |
||||
import { |
||||
ApplicationRef, |
||||
ComponentFactoryResolver, |
||||
ComponentRef, |
||||
EmbeddedViewRef, |
||||
Injector |
||||
} from '@angular/core'; |
||||
|
||||
export interface TabInterface { |
||||
name:string; |
||||
componentClass:{ new(...args:any[]):TabComponent }; |
||||
} |
||||
|
||||
export interface TabComponent { |
||||
onSave:() => void; |
||||
} |
||||
|
||||
export interface ActiveTabInterface { |
||||
name:string; |
||||
portal:ComponentPortal<TabComponent>; |
||||
componentRef:ComponentRef<TabComponent>; |
||||
dispose:() => void; |
||||
} |
||||
|
||||
export class TabPortalOutlet { |
||||
|
||||
// Active tabs that have been instantiated
|
||||
public activeTabs:{ [name:string]:ActiveTabInterface } = {}; |
||||
|
||||
// The current tab
|
||||
public currentTab:ActiveTabInterface|null = null; |
||||
|
||||
constructor( |
||||
public availableTabs:TabInterface[], |
||||
public outletElement:Element, |
||||
private componentFactoryResolver:ComponentFactoryResolver, |
||||
private appRef:ApplicationRef, |
||||
private injector:Injector) { |
||||
} |
||||
|
||||
public get activeComponents():TabComponent[] { |
||||
const tabs = _.values(this.activeTabs); |
||||
return tabs.map((tab:ActiveTabInterface) => tab.componentRef.instance); |
||||
} |
||||
|
||||
public switchTo(name:string) { |
||||
const tab = _.find(this.availableTabs, tab => tab.name === name); |
||||
|
||||
if (!tab) { |
||||
throw(`Trying to swtich to unknown tab ${name}.`); |
||||
} |
||||
|
||||
// Detach any current instance
|
||||
this.detach(); |
||||
|
||||
// Get existing or new component instance
|
||||
const instance = this.activateInstance(tab); |
||||
|
||||
// At this point the component has been instantiated, so we move it to the location in the DOM
|
||||
// where we want it to be rendered.
|
||||
this.outletElement.innerHTML = ''; |
||||
this.outletElement.appendChild(this._getComponentRootNode(instance.componentRef)); |
||||
this.currentTab = instance; |
||||
} |
||||
|
||||
public detach():void { |
||||
const current = this.currentTab; |
||||
if (current !== null) { |
||||
current.portal.setAttachedHost(null); |
||||
this.currentTab = null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Clears out a portal from the DOM. |
||||
*/ |
||||
dispose():void { |
||||
// Dispose all active tabs
|
||||
_.each(this.activeTabs, active => active.dispose()); |
||||
|
||||
// Remove outlet element
|
||||
if (this.outletElement.parentNode != null) { |
||||
this.outletElement.parentNode.removeChild(this.outletElement); |
||||
} |
||||
} |
||||
|
||||
private activateInstance(tab:TabInterface):ActiveTabInterface { |
||||
if (!this.activeTabs[tab.name]) { |
||||
this.activeTabs[tab.name] = this.createComponent(tab); |
||||
} |
||||
|
||||
return this.activeTabs[tab.name] || null; |
||||
} |
||||
|
||||
private createComponent(tab:TabInterface):ActiveTabInterface { |
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(tab.componentClass); |
||||
const componentRef = componentFactory.create(this.injector); |
||||
const portal = new ComponentPortal(tab.componentClass, null, this.injector); |
||||
|
||||
// Attach component view
|
||||
this.appRef.attachView(componentRef.hostView); |
||||
|
||||
return { |
||||
name: tab.name, |
||||
portal: portal, |
||||
componentRef: componentRef, |
||||
dispose: () => { |
||||
this.appRef.detachView(componentRef.hostView); |
||||
componentRef.destroy(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** Gets the root HTMLElement for an instantiated component. */ |
||||
private _getComponentRootNode(componentRef:ComponentRef<any>):HTMLElement { |
||||
return (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement; |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
<div class="columns-modal-content select2-modal-content"> |
||||
<label |
||||
[textContent]="text.selectedColumns" |
||||
class="hidden-for-sighted"> |
||||
</label> |
||||
|
||||
<div *ngFor="let column of availableColumns; let first = first;"> |
||||
<label class="form--label-with-check-box" for="column-{{column.id}}"> |
||||
<div class="form--check-box-container"> |
||||
<input id="column-{{column.id}}" |
||||
type="checkbox" |
||||
title="{{ column.name }}" |
||||
[(ngModel)]="selectedColumnMap[column.id]" |
||||
(ngModelChange)="setSelectedColumn(column)" |
||||
focus="first" /> |
||||
</div> |
||||
{{column.name}} |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<div *ngIf="eeShowBanners" class="ee-relation-columns-upsale"> |
||||
{{text.upsaleRelationColumns}} |
||||
<a href="https://www.openproject.org/enterprise-edition/?op_edtion=community-edition&op_referrer=wp-list-columns#relations" |
||||
target='blank' |
||||
[textContent]="text.upsaleRelationColumnsLink"></a> |
||||
</div> |
@ -0,0 +1,55 @@ |
||||
import {Component, Inject, Injector} from '@angular/core'; |
||||
import {I18nToken} from 'core-app/angular4-transition-utils'; |
||||
import {QueryColumn} from 'core-components/wp-query/query-column'; |
||||
import {ConfigurationService} from 'core-components/common/config/configuration.service'; |
||||
import {WorkPackageTableColumnsService} from 'core-components/wp-fast-table/state/wp-table-columns.service'; |
||||
import {TabComponent} from 'core-components/wp-table/configuration-modal/tab-portal-outlet'; |
||||
|
||||
@Component({ |
||||
template: require('!!raw-loader!./columns-tab.component.html') |
||||
}) |
||||
export class WpTableConfigurationColumnsTab implements TabComponent { |
||||
|
||||
public availableColumns = this.wpTableColumns.all; |
||||
public unusedColumns = this.wpTableColumns.unused; |
||||
public selectedColumns = angular.copy(this.wpTableColumns.getColumns()); |
||||
|
||||
public impaired = this.ConfigurationService.accessibilityModeEnabled(); |
||||
public selectedColumnMap:{ [id:string]:boolean } = {}; |
||||
public eeShowBanners:boolean = false; |
||||
public text = { |
||||
|
||||
columnsLabel: this.I18n.t('js.label_columns'), |
||||
selectedColumns: this.I18n.t('js.description_selected_columns'), |
||||
multiSelectLabel: this.I18n.t('js.work_packages.label_column_multiselect'), |
||||
|
||||
upsaleRelationColumns: this.I18n.t('js.modals.upsale_relation_columns'), |
||||
upsaleRelationColumnsLink: this.I18n.t('js.modals.upsale_relation_columns_link') |
||||
}; |
||||
|
||||
constructor(readonly injector:Injector, |
||||
@Inject(I18nToken) readonly I18n:op.I18n, |
||||
readonly wpTableColumns:WorkPackageTableColumnsService, |
||||
readonly ConfigurationService:ConfigurationService) { |
||||
} |
||||
|
||||
public onSave() { |
||||
this.wpTableColumns.setColumns(this.selectedColumns); |
||||
} |
||||
|
||||
public setSelectedColumn(column:QueryColumn) { |
||||
if (this.selectedColumnMap[column.id]) { |
||||
this.selectedColumns.push(column); |
||||
} |
||||
else { |
||||
_.remove(this.selectedColumns, (c:QueryColumn) => c.id === column.id); |
||||
} |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.eeShowBanners = angular.element('body').hasClass('ee-banners-visible'); |
||||
this.selectedColumns.forEach((column:QueryColumn) => { |
||||
this.selectedColumnMap[column.id] = true; |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
<div> |
||||
<form> |
||||
<div class="form--field -full-width"> |
||||
<div class="form--field-container"> |
||||
<label class="option-label"> |
||||
<input type="radio" |
||||
[(ngModel)]="displayMode" |
||||
value="default" |
||||
name="display_mode_switch"> |
||||
{{ text.display_mode.default }} |
||||
</label> |
||||
<label class="option-label"> |
||||
<input type="radio" |
||||
[(ngModel)]="displayMode" |
||||
value="grouped" |
||||
name="display_mode_switch"> |
||||
{{ text.display_mode.grouped }} |
||||
</label> |
||||
<label class="option-label"> |
||||
<input type="radio" |
||||
[(ngModel)]="displayMode" |
||||
value="hierarchy" |
||||
name="display_mode_switch"> |
||||
{{ text.display_mode.hierarchy }} |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<div *ngIf="displayMode === 'grouped'"> |
||||
<h4 [textContent]="text.label_group_by"></h4> |
||||
<p [textContent]="text.display_mode.grouped_hint"></p> |
||||
<div class="form--field -full-width"> |
||||
<label |
||||
for="selected_grouping" |
||||
[textContent]="text.label_group_by" |
||||
class="form--label hidden-for-sighted"> |
||||
</label> |
||||
<div class="form--field-container"> |
||||
<div class="form--select-container"> |
||||
<select |
||||
(change)="updateGroup($event.target.value)" |
||||
id="selected_grouping" |
||||
name="selected_grouping" |
||||
class="form--select"> |
||||
<option [textContent]="text.please_select" |
||||
[selected]="!currentGroup" |
||||
disabled |
||||
[value]="''"></option> |
||||
<option *ngFor="let group of availableGroups" |
||||
[textContent]="group.name" |
||||
[selected]="currentGroup && currentGroup.href === group.href" |
||||
[value]="group.href"></option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div *ngIf="displayMode === 'hierarchy'"> |
||||
<h4 [textContent]="text.display_mode.hierarchy_mode"></h4> |
||||
<p [textContent]="text.display_mode.hierarchy_hint"></p> |
||||
</div> |
||||
|
||||
<hr/> |
||||
|
||||
<div> |
||||
<div class="form--field -full-width"> |
||||
<div class="form--field-container"> |
||||
<label class="option-label"> |
||||
<input type="checkbox" |
||||
(change)="displaySums = !displaySums" |
||||
[checked]="displaySums" |
||||
name="display_sums_switch"> |
||||
{{ text.display_sums }} |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<p [textContent]="text.display_sums_hint"></p> |
||||
</div> |
||||
</form> |
||||
</div> |
@ -0,0 +1,79 @@ |
||||
import {Component, Inject, Injector} from '@angular/core'; |
||||
import {I18nToken} from 'core-app/angular4-transition-utils'; |
||||
import {TabComponent} from 'core-components/wp-table/configuration-modal/tab-portal-outlet'; |
||||
import {WorkPackageTableGroupByService} from 'core-components/wp-fast-table/state/wp-table-group-by.service'; |
||||
import {QueryGroupByResource} from 'core-app/modules/hal/resources/query-group-by-resource'; |
||||
import {WorkPackageTableHierarchiesService} from 'core-components/wp-fast-table/state/wp-table-hierarchy.service'; |
||||
import {WorkPackageTableSumService} from 'core-components/wp-fast-table/state/wp-table-sum.service'; |
||||
|
||||
@Component({ |
||||
template: require('!!raw-loader!./display-settings-tab.component.html') |
||||
}) |
||||
export class WpTableConfigurationDisplaySettingsTab implements TabComponent { |
||||
|
||||
// Display mode
|
||||
public displayMode:'hierarchy'|'grouped'|'default' = 'default'; |
||||
|
||||
// Grouping
|
||||
public currentGroup:QueryGroupByResource|undefined; |
||||
public availableGroups:QueryGroupByResource[] = []; |
||||
|
||||
// Sums row display
|
||||
public displaySums:boolean = false; |
||||
|
||||
public text = { |
||||
label_group_by: this.I18n.t('js.label_group_by'), |
||||
title: this.I18n.t('js.label_group_by'), |
||||
placeholder: this.I18n.t('js.placeholders.default'), |
||||
please_select: this.I18n.t('js.placeholders.selection'), |
||||
display_sums: this.I18n.t('js.work_packages.query.display_sums'), |
||||
display_sums_hint: this.I18n.t('js.work_packages.table_configuration.display_sums_hint'), |
||||
display_mode: { |
||||
default: this.I18n.t('js.work_packages.table_configuration.default_mode'), |
||||
grouped: this.I18n.t('js.work_packages.table_configuration.grouped_mode'), |
||||
grouped_hint: this.I18n.t('js.work_packages.table_configuration.grouped_hint'), |
||||
hierarchy: this.I18n.t('js.work_packages.table_configuration.hierarchy_mode'), |
||||
hierarchy_hint: this.I18n.t('js.work_packages.table_configuration.hierarchy_hint') |
||||
} |
||||
}; |
||||
|
||||
constructor(readonly injector:Injector, |
||||
@Inject(I18nToken) readonly I18n:op.I18n, |
||||
readonly wpTableGroupBy:WorkPackageTableGroupByService, |
||||
readonly wpTableHierarchies:WorkPackageTableHierarchiesService, |
||||
readonly wpTableSums:WorkPackageTableSumService) { |
||||
} |
||||
|
||||
public onSave() { |
||||
// Update hierarchy state
|
||||
this.wpTableHierarchies.setEnabled(this.displayMode === 'hierarchy'); |
||||
|
||||
// Update grouping state
|
||||
let group = this.displayMode === 'grouped' ? this.currentGroup : undefined; |
||||
this.wpTableGroupBy.set(group); |
||||
|
||||
// Update sums state
|
||||
this.wpTableSums.setEnabled(this.displaySums); |
||||
} |
||||
|
||||
public updateGroup(href:string) { |
||||
this.currentGroup = _.find(this.availableGroups, group => group.href === href); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
if (this.wpTableHierarchies.isEnabled) { |
||||
this.displayMode = 'hierarchy'; |
||||
} else if (this.wpTableGroupBy.current) { |
||||
this.displayMode = 'grouped'; |
||||
} |
||||
|
||||
this.displaySums = this.wpTableSums.currentSum || false; |
||||
|
||||
this.wpTableGroupBy |
||||
.onReady() |
||||
.then(() => { |
||||
this.availableGroups = _.sortBy(this.wpTableGroupBy.available, 'name'); |
||||
this.currentGroup = this.wpTableGroupBy.current; |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
<query-filters *ngIf="!!filters" |
||||
[filters]="filters"></query-filters> |
@ -0,0 +1,43 @@ |
||||
import {Component, Inject, Injector} from '@angular/core'; |
||||
import {I18nToken} from 'core-app/angular4-transition-utils'; |
||||
import {TabComponent} from 'core-components/wp-table/configuration-modal/tab-portal-outlet'; |
||||
import WorkPackageFiltersService from 'core-components/filters/wp-filters/wp-filters.service'; |
||||
import {WorkPackageTableFiltersService} from 'core-components/wp-fast-table/state/wp-table-filters.service'; |
||||
import {WorkPackageTableFilters} from 'core-components/wp-fast-table/wp-table-filters'; |
||||
|
||||
@Component({ |
||||
template: require('!!raw-loader!./filters-tab.component.html') |
||||
}) |
||||
export class WpTableConfigurationFiltersTab implements TabComponent { |
||||
|
||||
public filters:WorkPackageTableFilters|undefined; |
||||
public eeShowBanners:boolean = false; |
||||
|
||||
public text = { |
||||
columnsLabel: this.I18n.t('js.label_columns'), |
||||
selectedColumns: this.I18n.t('js.description_selected_columns'), |
||||
multiSelectLabel: this.I18n.t('js.work_packages.label_column_multiselect'), |
||||
|
||||
upsaleRelationColumns: this.I18n.t('js.modals.upsale_relation_columns'), |
||||
upsaleRelationColumnsLink: this.I18n.t('js.modals.upsale_relation_columns_link') |
||||
}; |
||||
|
||||
constructor(readonly injector:Injector, |
||||
@Inject(I18nToken) readonly I18n:op.I18n, |
||||
readonly wpTableFilters:WorkPackageTableFiltersService, |
||||
readonly wpFiltersService:WorkPackageFiltersService) { |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.eeShowBanners = angular.element('body').hasClass('ee-banners-visible'); |
||||
this.wpTableFilters |
||||
.onReady() |
||||
.then(() => this.filters = _.cloneDeep(this.wpTableFilters.currentState)); |
||||
} |
||||
|
||||
public onSave() { |
||||
if (this.filters) { |
||||
this.wpTableFilters.replaceIfComplete(this.filters); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,48 @@ |
||||
<form name="modalSortingForm"> |
||||
<div id="modal-sorting" |
||||
class="modal-content-container loading-indicator--location" |
||||
data-indicator-name="sorting-modal"> |
||||
<div class="form--row modal-sorting-row-{{index}}" |
||||
*ngFor="let sort of sortationObjects; let index = index"> |
||||
<div class="form--field -full-width"> |
||||
<label |
||||
for="modal-sorting-attribute-{{index}}" |
||||
[textContent]="text['sort_criteria_' + index]" |
||||
class="form--label hidden-for-sighted"> |
||||
</label> |
||||
<div class="form--field-container"> |
||||
<div class="form--select-container"> |
||||
<select |
||||
id="modal-sorting-attribute-{{index}}" |
||||
name="modal-sorting-attribute-{{index}}" |
||||
(change)="updateSelection(sort, $event.target.value)" |
||||
class="form--select"> |
||||
<option *ngIf="!!sort.column.href" |
||||
[textContent]="sort.column.name" |
||||
[value]="sort.column.href" |
||||
selected></option> |
||||
<option [textContent]="emptyColumn.name" |
||||
[value]="emptyColumn.href" |
||||
[selected]="sort.column.href === null"></option> |
||||
<option *ngFor="let column of availableColumns" |
||||
[textContent]="column.name" |
||||
[value]="column.href"></option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="form--field -full-width"> |
||||
<div class="form--field-container"> |
||||
<label class="option-label" *ngFor="let availableDirection of availableDirections"> |
||||
<input type="radio" |
||||
[(ngModel)]="sort.direction" |
||||
[value]="availableDirection.$href" |
||||
[textContent]="availableDirection.name" |
||||
name="modal-sorting-attribute-{{index}}--sort-direction"> |
||||
{{ availableDirection.name }} |
||||
</label> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</form> |
@ -0,0 +1,116 @@ |
||||
import {Component, Inject, Injector} from '@angular/core'; |
||||
import {I18nToken} from 'core-app/angular4-transition-utils'; |
||||
import {TabComponent} from 'core-components/wp-table/configuration-modal/tab-portal-outlet'; |
||||
import { |
||||
QUERY_SORT_BY_ASC, |
||||
QUERY_SORT_BY_DESC, |
||||
QuerySortByResource |
||||
} from 'core-app/modules/hal/resources/query-sort-by-resource'; |
||||
import {take} from 'rxjs/operators'; |
||||
import {WorkPackageTableSortByService} from 'core-components/wp-fast-table/state/wp-table-sort-by.service'; |
||||
import WorkPackageFiltersService from 'core-components/filters/wp-filters/wp-filters.service'; |
||||
import {WorkPackageTableFiltersService} from 'core-components/wp-fast-table/state/wp-table-filters.service'; |
||||
|
||||
class SortModalObject { |
||||
constructor(public column:SortColumn, |
||||
public direction:string) { |
||||
} |
||||
} |
||||
|
||||
interface SortColumn { |
||||
name:string; |
||||
href:string|null; |
||||
} |
||||
|
||||
@Component({ |
||||
template: require('!!raw-loader!./sort-by-tab.component.html') |
||||
}) |
||||
export class WpTableConfigurationSortByTab implements TabComponent { |
||||
|
||||
public text = { |
||||
title: this.I18n.t('js.label_sort_by'), |
||||
placeholder: this.I18n.t('js.placeholders.default'), |
||||
sort_criteria_1: this.I18n.t('js.filter.sorting.criteria.one'), |
||||
sort_criteria_2: this.I18n.t('js.filter.sorting.criteria.two'), |
||||
sort_criteria_3: this.I18n.t('js.filter.sorting.criteria.three'), |
||||
}; |
||||
|
||||
readonly availableDirections = [ |
||||
{ $href: QUERY_SORT_BY_ASC, name: this.I18n.t('js.label_ascending') }, |
||||
{ $href: QUERY_SORT_BY_DESC, name: this.I18n.t('js.label_descending') } |
||||
]; |
||||
|
||||
public availableColumns:SortColumn[] = []; |
||||
public allColumns:SortColumn[] = []; |
||||
public sortationObjects:SortModalObject[] = []; |
||||
public emptyColumn:SortColumn = { name: this.text.placeholder, href: null }; |
||||
|
||||
constructor(readonly injector:Injector, |
||||
@Inject(I18nToken) readonly I18n:op.I18n, |
||||
readonly wpTableSortBy:WorkPackageTableSortByService) { |
||||
|
||||
} |
||||
|
||||
public onSave() { |
||||
let sortElements = |
||||
this.sortationObjects |
||||
.filter(object => object.column !== null) |
||||
.map(object => this.getMatchingSort(object.column.href!, object.direction)); |
||||
|
||||
this.wpTableSortBy.set(_.compact(sortElements)); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.wpTableSortBy |
||||
.state |
||||
.values$() |
||||
.pipe(take(1)) |
||||
.toPromise() |
||||
.then(() => { |
||||
let allColumns:SortColumn[] = this.wpTableSortBy.available.map( |
||||
(sort:QuerySortByResource) => { |
||||
return { name: sort.column.name, href: sort.column.$href }; |
||||
} |
||||
); |
||||
|
||||
// For whatever reason, even though the UI doesnt implement it,
|
||||
// QuerySortByResources are doubled for each column (one for asc/desc direction)
|
||||
this.allColumns = _.uniqBy(allColumns, 'href'); |
||||
|
||||
_.each(this.wpTableSortBy.currentSortBys, sort => { |
||||
this.sortationObjects.push( |
||||
new SortModalObject({ name: sort.column.name, href: sort.column.$href }, |
||||
sort.direction.$href!) |
||||
); |
||||
}); |
||||
|
||||
this.updateUsedColumns(); |
||||
this.fillUpSortElements(); |
||||
}); |
||||
} |
||||
|
||||
public updateSelection(sort:SortModalObject, selected:string|null) { |
||||
sort.column = _.find(this.allColumns, (column) => column.href === selected) || this.emptyColumn; |
||||
this.updateUsedColumns(); |
||||
} |
||||
|
||||
public updateUsedColumns() { |
||||
let usedColumns = this.sortationObjects |
||||
.filter(o => o.column !== null) |
||||
.map((object:SortModalObject) => object.column); |
||||
|
||||
this.availableColumns = _.sortBy(_.differenceBy(this.allColumns, usedColumns, 'href'), 'name'); |
||||
} |
||||
|
||||
private getMatchingSort(column:string, direction:string) { |
||||
return _.find(this.wpTableSortBy.available, sort => { |
||||
return sort.column.$href === column && sort.direction.$href === direction; |
||||
}); |
||||
} |
||||
|
||||
private fillUpSortElements() { |
||||
while (this.sortationObjects.length < 3) { |
||||
this.sortationObjects.push(new SortModalObject(this.emptyColumn, QUERY_SORT_BY_ASC)); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,48 @@ |
||||
<div> |
||||
<form> |
||||
<div> |
||||
<div class="form--field -full-width"> |
||||
<div class="form--field-container"> |
||||
<label class="option-label"> |
||||
<input type="checkbox" |
||||
(change)="timelineVisible = !timelineVisible" |
||||
[checked]="timelineVisible" |
||||
name="display_timelines_switch"> |
||||
{{ text.display_timelines }} |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<p [textContent]="text.display_timelines_hint"></p> |
||||
</div> |
||||
<hr/> |
||||
<fieldset *ngIf="timelineVisible" |
||||
class="form--fieldset"> |
||||
<legend class="form--fieldset-legend" |
||||
[textContent]="text.labels.title"> |
||||
</legend> |
||||
<p [textContent]="text.labels.description"></p> |
||||
<div class="form--row" *ngFor="let key of availableLabels"> |
||||
<div class="form--field"> |
||||
<label |
||||
for="modal-timelines-label-{{key}}" |
||||
class="form--label"> |
||||
{{ text.labels[key] }} |
||||
</label> |
||||
<div class="form--field-container"> |
||||
<div class="form--select-container"> |
||||
<select |
||||
id="modal-timelines-label-{{key}}" |
||||
(change)="updateLabels(key, $event.target.value)" |
||||
class="form--select"> |
||||
<option *ngFor="let column of availableAttributes" |
||||
[textContent]="column.name" |
||||
[value]="column.id" |
||||
[selected]="labels[key] === column.id"></option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</fieldset> |
||||
</form> |
||||
</div> |
@ -0,0 +1,69 @@ |
||||
import {Component, Inject, Injector} from '@angular/core'; |
||||
import {I18nToken} from 'core-app/angular4-transition-utils'; |
||||
import {TabComponent} from 'core-components/wp-table/configuration-modal/tab-portal-outlet'; |
||||
import {WorkPackageTableTimelineService} from 'core-components/wp-fast-table/state/wp-table-timeline.service'; |
||||
import {TimelineLabels} from 'core-app/modules/hal/resources/query-resource'; |
||||
import {WorkPackageTableColumnsService} from 'core-components/wp-fast-table/state/wp-table-columns.service'; |
||||
import {QueryColumn} from 'core-components/wp-query/query-column'; |
||||
|
||||
@Component({ |
||||
template: require('!!raw-loader!./timelines-tab.component.html') |
||||
}) |
||||
export class WpTableConfigurationTimelinesTab implements TabComponent { |
||||
|
||||
public timelineVisible:boolean = false; |
||||
public availableAttributes:{ id:string, name:string }[]; |
||||
|
||||
public labels:TimelineLabels; |
||||
public availableLabels:string[]; |
||||
|
||||
public text = { |
||||
title: this.I18n.t('js.timelines.gantt_chart'), |
||||
display_timelines: this.I18n.t('js.timelines.button_activate'), |
||||
display_timelines_hint: this.I18n.t('js.work_packages.table_configuration.show_timeline_hint'), |
||||
labels: { |
||||
title: this.I18n.t('js.timelines.labels.title'), |
||||
description: this.I18n.t('js.timelines.labels.description'), |
||||
bar: this.I18n.t('js.timelines.labels.bar'), |
||||
none: this.I18n.t('js.timelines.filter.noneSelection'), |
||||
left: this.I18n.t('js.timelines.labels.left'), |
||||
right: this.I18n.t('js.timelines.labels.right'), |
||||
farRight: this.I18n.t('js.timelines.labels.farRight') |
||||
} |
||||
}; |
||||
|
||||
constructor(readonly injector:Injector, |
||||
@Inject(I18nToken) readonly I18n:op.I18n, |
||||
readonly wpTableTimeline:WorkPackageTableTimelineService, |
||||
readonly wpTableColumns:WorkPackageTableColumnsService) { |
||||
} |
||||
|
||||
public onSave() { |
||||
this.wpTableTimeline.setVisible(this.timelineVisible); |
||||
this.wpTableTimeline.updateLabels(this.labels); |
||||
} |
||||
|
||||
public updateLabels(key:keyof TimelineLabels, value:string|null) { |
||||
if (value === '') { |
||||
value = null; |
||||
} |
||||
|
||||
this.labels[key] = value; |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.timelineVisible = this.wpTableTimeline.isVisible; |
||||
|
||||
// Current label models
|
||||
const labels = this.wpTableTimeline.labels; |
||||
this.labels = _.clone(labels); |
||||
this.availableLabels = Object.keys(this.labels); |
||||
|
||||
// Available labels
|
||||
const availableColumns = this.wpTableColumns |
||||
.allPropertyColumns |
||||
.sort((a:QueryColumn, b:QueryColumn) => a.name.localeCompare(b.name)); |
||||
|
||||
this.availableAttributes = [{ id: '', name: this.text.labels.none }].concat(availableColumns); |
||||
} |
||||
} |
@ -0,0 +1,44 @@ |
||||
<div class="op-modal--container ng-modal-window loading-indicator--location" |
||||
data-indicator-name="modal"> |
||||
<div class="ng-modal-inner wp-table--configuration-modal" tabindex="0"> |
||||
<div class="modal-header"> |
||||
<a> |
||||
<i |
||||
class="icon-close" |
||||
(click)="closeMe()" |
||||
[attr.title]="text.closePopup"> |
||||
</i> |
||||
</a> |
||||
</div> |
||||
|
||||
<h3 [textContent]="text.title"></h3> |
||||
|
||||
<div class="tabs--container"> |
||||
<ul> |
||||
<li *ngFor="let tab of availableTabs"> |
||||
<a class="tab-show" |
||||
href="#" |
||||
[ngClass]="{ 'selected': currentTab && tab.name === currentTab.name }" |
||||
[textContent]="tab.title" |
||||
(click)="switchTo(tab.name)"> |
||||
</a> |
||||
</ul> |
||||
<div class="tabs-buttons" style="display:none;"> |
||||
<button class="tab-left icon-context icon-arrow-left4"></button> |
||||
<button class="tab-right icon-context icon-arrow-right5"></button> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="tab-content" #tabContentOutlet></div> |
||||
<div class="modal--form-actions"> |
||||
<button class="button -highlight" |
||||
[textContent]="text.applyButton" |
||||
(click)="saveChanges()"> |
||||
</button> |
||||
<button class="button" |
||||
[textContent]="text.cancelButton" |
||||
(click)="closeMe()"> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,133 @@ |
||||
import { |
||||
ApplicationRef, |
||||
Component, |
||||
ComponentFactoryResolver, |
||||
ElementRef, |
||||
Inject, |
||||
Injector, |
||||
OnDestroy, |
||||
OnInit, |
||||
ViewChild |
||||
} from '@angular/core'; |
||||
import {I18nToken, OpModalLocalsToken} from 'core-app/angular4-transition-utils'; |
||||
import {OpModalLocalsMap} from 'core-components/op-modals/op-modal.types'; |
||||
import {ConfigurationService} from 'core-components/common/config/configuration.service'; |
||||
import {WorkPackageTableColumnsService} from 'core-components/wp-fast-table/state/wp-table-columns.service'; |
||||
import {OpModalComponent} from 'core-components/op-modals/op-modal.component'; |
||||
import {WpTableConfigurationService} from 'core-components/wp-table/configuration-modal/wp-table-configuration.service'; |
||||
import { |
||||
ActiveTabInterface, |
||||
TabComponent, |
||||
TabPortalOutlet |
||||
} from 'core-components/wp-table/configuration-modal/tab-portal-outlet'; |
||||
|
||||
@Component({ |
||||
template: require('!!raw-loader!./wp-table-configuration.modal.html') |
||||
}) |
||||
export class WpTableConfigurationModalComponent extends OpModalComponent implements OnInit, OnDestroy { |
||||
|
||||
/* Close on escape? */ |
||||
public closeOnEscape = false; |
||||
|
||||
/* Close on outside click */ |
||||
public closeOnOutsideClick = false; |
||||
|
||||
public $element:JQuery; |
||||
|
||||
public text = { |
||||
title: this.I18n.t('js.work_packages.table_configuration.modal_title'), |
||||
closePopup: this.I18n.t('js.close_popup_title'), |
||||
|
||||
columnsLabel: this.I18n.t('js.label_columns'), |
||||
selectedColumns: this.I18n.t('js.description_selected_columns'), |
||||
multiSelectLabel: this.I18n.t('js.work_packages.label_column_multiselect'), |
||||
applyButton: this.I18n.t('js.modals.button_apply'), |
||||
cancelButton: this.I18n.t('js.modals.button_cancel'), |
||||
|
||||
upsaleRelationColumns: this.I18n.t('js.modals.upsale_relation_columns'), |
||||
upsaleRelationColumnsLink: this.I18n.t('js.modals.upsale_relation_columns_link') |
||||
}; |
||||
|
||||
public impaired = this.ConfigurationService.accessibilityModeEnabled(); |
||||
public selectedColumnMap:{ [id:string]:boolean } = {}; |
||||
|
||||
// Get the view child we'll use as the portal host
|
||||
@ViewChild('tabContentOutlet') tabContentOutlet:ElementRef; |
||||
// And a reference to the actual portal host interface
|
||||
private tabPortalHost:TabPortalOutlet; |
||||
|
||||
constructor(@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, |
||||
@Inject(I18nToken) readonly I18n:op.I18n, |
||||
readonly wpTableConfigurationService:WpTableConfigurationService, |
||||
readonly injector:Injector, |
||||
readonly appRef:ApplicationRef, |
||||
readonly componentFactoryResolver:ComponentFactoryResolver, |
||||
readonly wpTableColumns:WorkPackageTableColumnsService, |
||||
readonly ConfigurationService:ConfigurationService, |
||||
readonly elementRef:ElementRef) { |
||||
super(locals, elementRef); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.$element = jQuery(this.elementRef.nativeElement); |
||||
|
||||
this.tabPortalHost = new TabPortalOutlet( |
||||
this.wpTableConfigurationService.tabs, |
||||
this.tabContentOutlet.nativeElement, |
||||
this.componentFactoryResolver, |
||||
this.appRef, |
||||
this.injector |
||||
); |
||||
|
||||
// Switch to the default tab
|
||||
// after a timeout to let the host initialize.
|
||||
setTimeout(() => { |
||||
const initialTab = this.locals['initialTab'] || this.availableTabs[0].name; |
||||
this.switchTo(initialTab); |
||||
}); |
||||
} |
||||
|
||||
ngOnDestroy() { |
||||
this.tabPortalHost.dispose(); |
||||
} |
||||
|
||||
public get availableTabs() { |
||||
return this.tabPortalHost.availableTabs; |
||||
} |
||||
|
||||
public get currentTab():ActiveTabInterface|null { |
||||
return this.tabPortalHost.currentTab; |
||||
} |
||||
|
||||
public switchTo(name:string) { |
||||
this.tabPortalHost.switchTo(name); |
||||
} |
||||
|
||||
public saveChanges():void { |
||||
this.tabPortalHost.activeComponents.forEach((component:TabComponent) => { |
||||
component.onSave(); |
||||
}); |
||||
|
||||
this.closeMe(); |
||||
} |
||||
|
||||
/** |
||||
* Called when the user attempts to close the modal window. |
||||
* The service will close this modal if this method returns true |
||||
* @returns {boolean} |
||||
*/ |
||||
public onClose():boolean { |
||||
this.afterFocusOn.focus(); |
||||
return true; |
||||
} |
||||
|
||||
public onOpen(modalElement:JQuery) { |
||||
modalElement |
||||
.find('.wp-table--configuration-modal') |
||||
.focus(); |
||||
} |
||||
|
||||
protected get afterFocusOn():JQuery { |
||||
return this.$element; |
||||
} |
||||
} |
@ -0,0 +1,50 @@ |
||||
import {Inject, Injectable, Injector} from '@angular/core'; |
||||
import {I18nToken} from 'core-app/angular4-transition-utils'; |
||||
import {WpTableConfigurationDisplaySettingsTab} from 'core-components/wp-table/configuration-modal/tabs/display-settings-tab.component'; |
||||
import {WpTableConfigurationColumnsTab} from 'core-components/wp-table/configuration-modal/tabs/columns-tab.component'; |
||||
import {WpTableConfigurationSortByTab} from 'core-components/wp-table/configuration-modal/tabs/sort-by-tab.component'; |
||||
import {WpTableConfigurationTimelinesTab} from 'core-components/wp-table/configuration-modal/tabs/timelines-tab.component'; |
||||
import {WpTableConfigurationFiltersTab} from 'core-components/wp-table/configuration-modal/tabs/filters-tab.component'; |
||||
|
||||
export interface WpTableConfigurationTabReference { |
||||
name:string; |
||||
title:string; |
||||
componentClass:{ new(...args:any[]):any }; |
||||
} |
||||
|
||||
@Injectable() |
||||
export class WpTableConfigurationService { |
||||
|
||||
public tabs:WpTableConfigurationTabReference[] = [ |
||||
{ |
||||
name: 'filters', |
||||
title: this.I18n.t('js.work_packages.query.filters'), |
||||
componentClass: WpTableConfigurationFiltersTab, |
||||
}, |
||||
{ |
||||
name: 'sort-by', |
||||
title: this.I18n.t('js.label_sort_by'), |
||||
componentClass: WpTableConfigurationSortByTab, |
||||
}, |
||||
{ |
||||
name: 'columns', |
||||
title: this.I18n.t('js.label_columns'), |
||||
componentClass: WpTableConfigurationColumnsTab, |
||||
}, |
||||
{ |
||||
name: 'display-settings', |
||||
title: this.I18n.t('js.work_packages.table_configuration.display_settings'), |
||||
componentClass: WpTableConfigurationDisplaySettingsTab, |
||||
}, |
||||
{ |
||||
name: 'timelines', |
||||
title: this.I18n.t('js.timelines.gantt_chart'), |
||||
componentClass: WpTableConfigurationTimelinesTab |
||||
} |
||||
]; |
||||
|
||||
constructor(readonly injector:Injector, |
||||
@Inject(I18nToken) readonly I18n:op.I18n) { |
||||
|
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue