Merge branch 'houskeeping/consolidated-ng2-modals' into angular/migrate-hal-resources

pull/6263/head
Oliver Günther 7 years ago
commit 14e3e88255
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 1
      .gitattributes
  2. 36
      app/assets/stylesheets/content/_modal.sass
  3. 3
      app/assets/stylesheets/content/_tabs.sass
  4. 2
      app/assets/stylesheets/content/work_packages/_table_content.sass
  5. 2
      app/models/query.rb
  6. 2
      app/models/query/timelines.rb
  7. 14
      config/locales/js-en.yml
  8. 73
      frontend/app/angular4-modules.ts
  9. 2
      frontend/app/angular4-test-setup.ts
  10. 5
      frontend/app/angular4-transition-utils.ts
  11. 49
      frontend/app/components/a11y/accessible-by-keyboard.component.ng2.test.ts
  12. 71
      frontend/app/components/a11y/accessible-by-keyboard.component.ts
  13. 46
      frontend/app/components/a11y/accessible-by-keyboard.ng1.directive.ts
  14. 0
      frontend/app/components/a11y/accessible_by_keyboard.html
  15. 24
      frontend/app/components/angular/debounced-event-emitter.ts
  16. 21
      frontend/app/components/angular/tracking-functions.ts
  17. 20
      frontend/app/components/common/icon/op-icon.ng1.directive.ts
  18. 24
      frontend/app/components/common/icon/op-icon.ts
  19. 66
      frontend/app/components/common/notification-box/notification-box.directive.test.js
  20. 4
      frontend/app/components/common/path-helper/path-helper.service.ts
  21. 0
      frontend/app/components/context-menus/settings-menu/settings-menu.controller.ts
  22. 26
      frontend/app/components/filters/abstract-filter-date-time-value/abstract-filter-date-time-value.controller.ts
  23. 15
      frontend/app/components/filters/filter-boolean-value/filter-boolean-value.component.html
  24. 43
      frontend/app/components/filters/filter-boolean-value/filter-boolean-value.component.ts
  25. 10
      frontend/app/components/filters/filter-boolean-value/filter-boolean-value.directive.html
  26. 5
      frontend/app/components/filters/filter-container/filter-container.directive.html
  27. 29
      frontend/app/components/filters/filter-container/filter-container.directive.ts
  28. 18
      frontend/app/components/filters/filter-date-time-value/filter-date-time-value.component.html
  29. 46
      frontend/app/components/filters/filter-date-time-value/filter-date-time-value.component.ts
  30. 18
      frontend/app/components/filters/filter-date-time-value/filter-date-time-value.directive.html
  31. 31
      frontend/app/components/filters/filter-date-times-value/filter-date-times-value.component.html
  32. 48
      frontend/app/components/filters/filter-date-times-value/filter-date-times-value.component.ts
  33. 36
      frontend/app/components/filters/filter-date-times-value/filter-date-times-value.directive.html
  34. 12
      frontend/app/components/filters/filter-date-value/filter-date-value.component.html
  35. 62
      frontend/app/components/filters/filter-date-value/filter-date-value.component.ts
  36. 13
      frontend/app/components/filters/filter-date-value/filter-date-value.directive.html
  37. 29
      frontend/app/components/filters/filter-dates-value/filter-dates-value.component.html
  38. 67
      frontend/app/components/filters/filter-dates-value/filter-dates-value.component.ts
  39. 32
      frontend/app/components/filters/filter-dates-value/filter-dates-value.directive.html
  40. 14
      frontend/app/components/filters/filter-integer-value/filter-integer-value.component.html
  41. 47
      frontend/app/components/filters/filter-integer-value/filter-integer-value.component.ts
  42. 18
      frontend/app/components/filters/filter-integer-value/filter-integer-value.directive.html
  43. 12
      frontend/app/components/filters/filter-string-value/filter-string-value.component.html
  44. 49
      frontend/app/components/filters/filter-string-value/filter-string-value.component.ts
  45. 15
      frontend/app/components/filters/filter-string-value/filter-string-value.directive.html
  46. 49
      frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.html
  47. 246
      frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ng2.test.ts
  48. 101
      frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts
  49. 37
      frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.html
  50. 227
      frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.test.ts
  51. 32
      frontend/app/components/filters/filters.constants.ts
  52. 83
      frontend/app/components/filters/query-filter/query-filter.component.html
  53. 85
      frontend/app/components/filters/query-filter/query-filter.component.ts
  54. 67
      frontend/app/components/filters/query-filter/query-filter.directive.ts
  55. 54
      frontend/app/components/filters/query-filters/query-filters-ng1-wrapper.component.ts
  56. 61
      frontend/app/components/filters/query-filters/query-filters.component.html
  57. 139
      frontend/app/components/filters/query-filters/query-filters.component.ts
  58. 125
      frontend/app/components/filters/query-filters/query-filters.directive.html
  59. 127
      frontend/app/components/filters/query-filters/query-filters.directive.ts
  60. 115
      frontend/app/components/modals/columns-modal/columns-modal.controller.ts
  61. 89
      frontend/app/components/modals/columns-modal/columns-modal.service.html
  62. 54
      frontend/app/components/modals/grouping-modal/grouping-modal.controller.ts
  63. 47
      frontend/app/components/modals/grouping-modal/grouping-modal.service.html
  64. 40
      frontend/app/components/modals/grouping-modal/grouping-modal.service.ts
  65. 107
      frontend/app/components/modals/sorting-modal/sorting-modal.controller.ts
  66. 56
      frontend/app/components/modals/sorting-modal/sorting-modal.service.html
  67. 40
      frontend/app/components/modals/sorting-modal/sorting-modal.service.ts
  68. 75
      frontend/app/components/modals/timelines-modal/timelines-modal.controller.ts
  69. 45
      frontend/app/components/modals/timelines-modal/timelines-modal.service.html
  70. 40
      frontend/app/components/modals/timelines-modal/timelines-modal.service.ts
  71. 11
      frontend/app/components/op-context-menu/handlers/op-columns-context-menu.directive.ts
  72. 102
      frontend/app/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts
  73. 9
      frontend/app/components/op-context-menu/op-context-menu.service.ts
  74. 45
      frontend/app/components/op-modals/op-modal.component.ts
  75. 114
      frontend/app/components/op-modals/op-modal.service.ts
  76. 7
      frontend/app/components/op-modals/op-modal.types.ts
  77. 2
      frontend/app/components/work-packages/work-package-cache.service.test.ts
  78. 83
      frontend/app/components/wp-edit/op-date-picker/datepicker.ts
  79. 3
      frontend/app/components/wp-edit/op-date-picker/op-date-picker.component.html
  80. 120
      frontend/app/components/wp-edit/op-date-picker/op-date-picker.component.ts
  81. 16
      frontend/app/components/wp-edit/op-date-picker/op-date-picker.directive.ts
  82. 2
      frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts
  83. 4
      frontend/app/components/wp-fast-table/state/wp-table-base.service.ts
  84. 7
      frontend/app/components/wp-fast-table/state/wp-table-sum.service.ts
  85. 6
      frontend/app/components/wp-fast-table/state/wp-table-timeline.service.ts
  86. 123
      frontend/app/components/wp-table/configuration-modal/tab-portal-outlet.ts
  87. 26
      frontend/app/components/wp-table/configuration-modal/tabs/columns-tab.component.html
  88. 55
      frontend/app/components/wp-table/configuration-modal/tabs/columns-tab.component.ts
  89. 79
      frontend/app/components/wp-table/configuration-modal/tabs/display-settings-tab.component.html
  90. 79
      frontend/app/components/wp-table/configuration-modal/tabs/display-settings-tab.component.ts
  91. 2
      frontend/app/components/wp-table/configuration-modal/tabs/filters-tab.component.html
  92. 43
      frontend/app/components/wp-table/configuration-modal/tabs/filters-tab.component.ts
  93. 48
      frontend/app/components/wp-table/configuration-modal/tabs/sort-by-tab.component.html
  94. 116
      frontend/app/components/wp-table/configuration-modal/tabs/sort-by-tab.component.ts
  95. 48
      frontend/app/components/wp-table/configuration-modal/tabs/timelines-tab.component.html
  96. 69
      frontend/app/components/wp-table/configuration-modal/tabs/timelines-tab.component.ts
  97. 44
      frontend/app/components/wp-table/configuration-modal/wp-table-configuration.modal.html
  98. 133
      frontend/app/components/wp-table/configuration-modal/wp-table-configuration.modal.ts
  99. 50
      frontend/app/components/wp-table/configuration-modal/wp-table-configuration.service.ts
  100. 10
      frontend/app/components/wp-table/wp-table.directive.html
  101. Some files were not shown because too many files have changed in this diff Show More

1
.gitattributes vendored

@ -0,0 +1 @@
app/assets/vendor/* binary

@ -107,28 +107,6 @@ $ng-modal-image-width: $ng-modal-image-height
.form--space.-left-spacing
padding-left: 1rem
#modal-sorting
min-height: 120px
.select2-container
float: left
margin-right: 20px
margin-bottom: 0px
@media(min-width: 1801px)
width: 300px
@media(max-width: 1800px)
width: 280px
@media(max-width: 1400px)
width: 230px
@media(max-width: 1100px)
width: 160px
@media(max-width: 800px)
width: 120px
label.option-label
float: left
margin-right: 20px
input
margin-top: 0px
.columns-modal-content
margin-bottom: 15px
label
@ -254,3 +232,17 @@ ul.export-options
.modal--footer
justify-content: inherit
.wp-table--configuration-modal
min-height: 120px
max-height: 90vh
width: 75vh
label.option-label
float: left
margin-right: 20px
input
margin-top: 0px
.modal--form-actions
margin: 1em 0

@ -28,7 +28,8 @@
/***** Tabs ****
#content .tabs
#content .tabs,
.tabs--container
position: relative
height: 2.6em
margin-bottom: 1.2em

@ -61,7 +61,7 @@
padding: 0 !important
// Shrink column of details / inline-create icons
.wp-table--context-menu-column
.wp-table--configuration-modal--trigger
width: 60px
// Center the th icon
text-align: center !important

@ -29,7 +29,7 @@
#++
class Query < ActiveRecord::Base
include ::Query::Timelines
include Timelines
include Queries::AvailableFilters
belongs_to :project

@ -26,7 +26,7 @@
#
# See docs/COPYRIGHT.rdoc for more details.
#++
#
module Query::Timelines
extend ActiveSupport::Concern

@ -357,6 +357,7 @@ en:
timelines:
gantt_chart: 'Gantt chart'
labels:
title: 'Label configuration'
bar: 'Bar labels'
left: 'Left'
right: 'Right'
@ -540,7 +541,6 @@ en:
group: "Group by"
group_by_disabled_by_hierarchy: "Group by is disabled due to the hierarchy mode being active."
hierarchy_disabled_by_group_by: "Hierarchy mode is disabled due to results being grouped by %{column}."
hierarchy_mode: "Hierarchy mode"
sort_ascending: "Sort ascending"
sort_descending: "Sort descending"
move_column_left: "Move column left"
@ -554,10 +554,22 @@ en:
not_found: "There is no such query"
text_no_results: "No matching queries were found."
table:
configure_button: 'Configure work package table'
summary: "Table with rows of work package and columns of work package attributes."
text_inline_edit: "Most cells of this table are buttons that activate inline-editing functionality of that attribute."
text_sort_hint: "With the links in the table headers you can sort, group, reorder, remove and add table columns."
text_select_hint: "Select boxes should be opened with 'ALT' and arrow keys."
table_configuration:
button: 'Configure this work package table'
modal_title: 'Work package table configuration'
display_settings: 'Display settings'
grouped_mode: "Grouped mode"
grouped_hint: "Table results will be grouped by the given attribute."
default_mode: "Default mode"
hierarchy_mode: "Hierarchy mode"
hierarchy_hint: "All filtered table results will be augmented with their ancestors. Hierarchies can be expanded and collapsed."
display_sums_hint: "Display sums of all summable attributes in a row below the table results."
show_timeline_hint: "Show an interactive gantt chart on the right side of the table. You can change its width by dragging the divider between table and gantt chart."
tabs:
overview: Overview
activity: Activity

@ -31,7 +31,6 @@ import {BrowserModule} from '@angular/platform-browser';
import {UpgradeModule} from '@angular/upgrade/static';
import {FormsModule} from '@angular/forms';
import {TablePaginationComponent} from 'core-app/components/table-pagination/table-pagination.component';
import {AccessibleByKeyboardDirectiveUpgraded} from 'core-app/ui_components/accessible-by-keyboard-directive-upgraded';
import {SimpleTemplateRenderer} from 'core-components/angular/simple-template-renderer';
import {OpIcon} from 'core-components/common/icon/op-icon';
import {WorkPackagesListComponent} from 'core-components/routing/wp-list/wp-list.component';
@ -51,7 +50,6 @@ import {WorkPackageTableSortByService} from 'core-components/wp-fast-table/state
import {WorkPackageTableTimelineService} from 'core-components/wp-fast-table/state/wp-table-timeline.service';
import {WorkPackageInlineCreateComponent,} from 'core-components/wp-inline-create/wp-inline-create.component';
import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service';
import {WorkPackageRelationsService} from 'core-components/wp-relations/wp-relations.service';
import {WpResizerDirectiveUpgraded} from 'core-components/wp-resizer/wp-resizer.directive';
import {SortHeaderDirective} from 'core-components/wp-table/sort-header/sort-header.directive';
import {WorkPackageTablePaginationComponent} from 'core-components/wp-table/table-pagination/wp-table-pagination.component';
@ -69,10 +67,8 @@ import {
$rootScopeToken,
$stateToken,
$timeoutToken,
columnsModalToken,
exportModalToken,
FocusHelperToken,
groupingModalToken,
halRequestToken,
HalResourceToken,
HookServiceToken,
@ -84,8 +80,6 @@ import {
saveModalToken,
settingsModalToken,
shareModalToken,
sortingModalToken,
timelinesModalToken,
TimezoneServiceToken,
upgradeService,
upgradeServiceWithToken, UrlParamsHelperServiceToken,
@ -115,7 +109,6 @@ import {WorkPackageTimelineButtonComponent} from 'core-components/wp-buttons/wp-
import {WorkPackageZenModeButtonComponent} from 'core-components/wp-buttons/wp-zen-mode-toggle-button/wp-zen-mode-toggle-button.component';
import {WorkPackageFilterContainerComponent} from 'core-components/filters/filter-container/filter-container.directive';
import WorkPackageFiltersService from 'core-components/filters/wp-filters/wp-filters.service';
import {Ng1QueryFiltersComponentWrapper} from 'core-components/filters/query-filters/query-filters-ng1-wrapper.component';
import {UIRouterUpgradeModule} from '@uirouter/angular-hybrid';
import {WorkPackageSplitViewComponent} from 'core-components/routing/wp-split-view/wp-split-view.component';
import {WorkPackageBreadcrumbComponent} from 'core-components/work-packages/wp-breadcrumb/wp-breadcrumb.component';
@ -192,8 +185,29 @@ import {WpRelationAddChildComponent} from 'core-components/wp-relations/wp-relat
import {WpRelationParentComponent} from 'core-components/wp-relations/wp-relations-parent/wp-relations-parent.component';
import {OpenprojectHalModule} from 'core-app/modules/hal/openproject-hal.module';
import {QueryFormDmService} from 'core-app/modules/dm-services/query-form-dm.service';
import {OpModalService} from 'core-components/op-modals/op-modal.service';
import {WpTableConfigurationModalComponent} from 'core-components/wp-table/configuration-modal/wp-table-configuration.modal';
import {WpTableConfigurationColumnsTab} from 'core-components/wp-table/configuration-modal/tabs/columns-tab.component';
import {WpTableConfigurationDisplaySettingsTab} from 'core-components/wp-table/configuration-modal/tabs/display-settings-tab.component';
import {WpTableConfigurationFiltersTab} from 'core-components/wp-table/configuration-modal/tabs/filters-tab.component';
import {WpTableConfigurationService} from 'core-components/wp-table/configuration-modal/wp-table-configuration.service';
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 {QueryFilterComponent} from 'core-components/filters/query-filter/query-filter.component';
import {QueryFiltersComponent} from 'core-components/filters/query-filters/query-filters.component';
import {FilterDateValueComponent} from 'core-components/filters/filter-date-value/filter-date-value.component';
import {FilterDateTimeValueComponent} from 'core-components/filters/filter-date-time-value/filter-date-time-value.component';
import {FilterDateTimesValueComponent} from 'core-components/filters/filter-date-times-value/filter-date-times-value.component';
import {FilterDatesValueComponent} from 'core-components/filters/filter-dates-value/filter-dates-value.component';
import {FilterIntegerValueComponent} from 'core-components/filters/filter-integer-value/filter-integer-value.component';
import {FilterToggledMultiselectValueComponent} from 'core-components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component';
import {FilterStringValueComponent} from 'core-components/filters/filter-string-value/filter-string-value.component';
import {FilterBooleanValueComponent} from 'core-components/filters/filter-boolean-value/filter-boolean-value.component';
import {OpDatePickerComponent} from 'core-components/wp-edit/op-date-picker/op-date-picker.component';
import {AccessibleByKeyboardComponent} from 'core-components/a11y/accessible-by-keyboard.component';
import {WorkPackageFormQueryGroupComponent} from 'core-components/wp-form-group/wp-query-group.component';
import {WorkPackageFormAttributeGroupComponent} from 'core-components/wp-form-group/wp-attribute-group.component';
import {WorkPackageRelationsService} from 'core-components/wp-relations/wp-relations.service';
@NgModule({
imports: [
@ -219,7 +233,6 @@ import {WorkPackageFormAttributeGroupComponent} from 'core-components/wp-form-gr
upgradeServiceWithToken('$timeout', $timeoutToken),
upgradeServiceWithToken('$locale', $localeToken),
upgradeServiceWithToken('NotificationsService', NotificationsServiceToken),
upgradeServiceWithToken('columnsModal', columnsModalToken),
upgradeServiceWithToken('FocusHelper', FocusHelperToken),
upgradeServiceWithToken('PathHelper', PathHelperToken),
upgradeServiceWithToken('halRequest', halRequestToken),
@ -227,13 +240,10 @@ import {WorkPackageFormAttributeGroupComponent} from 'core-components/wp-form-gr
upgradeServiceWithToken('TimezoneService', TimezoneServiceToken),
upgradeServiceWithToken('v3Path', v3PathToken),
upgradeServiceWithToken('wpDestroyModal', wpDestroyModalToken),
upgradeServiceWithToken('sortingModal', sortingModalToken),
upgradeServiceWithToken('groupingModal', groupingModalToken),
upgradeServiceWithToken('shareModal', shareModalToken),
upgradeServiceWithToken('saveModal', saveModalToken),
upgradeServiceWithToken('settingsModal', settingsModalToken),
upgradeServiceWithToken('exportModal', exportModalToken),
upgradeServiceWithToken('timelinesModal', timelinesModalToken),
upgradeServiceWithToken('UrlParamsHelper', UrlParamsHelperServiceToken),
upgradeService('wpRelations', WorkPackageRelationsService),
WorkPackageCacheService,
@ -289,11 +299,16 @@ import {WorkPackageFormAttributeGroupComponent} from 'core-components/wp-form-gr
WorkPackageContextMenuHelperService,
QueryFormDmService,
TableState,
// OP Modals service
OpModalService,
WpTableConfigurationService,
],
declarations: [
WorkPackagesListComponent,
OpIcon,
AccessibleByKeyboardDirectiveUpgraded,
OpDatePickerComponent,
AccessibleByKeyboardComponent,
TablePaginationComponent,
WorkPackageTablePaginationComponent,
WorkPackageTimelineHeaderController,
@ -307,14 +322,25 @@ import {WorkPackageFormAttributeGroupComponent} from 'core-components/wp-form-gr
WorkPackageDetailsViewButtonComponent,
WorkPackageTimelineButtonComponent,
WorkPackageZenModeButtonComponent,
WorkPackageFilterContainerComponent,
Ng1QueryFiltersComponentWrapper,
WpResizerDirectiveUpgraded,
WpCustomActionComponent,
WpCustomActionsComponent,
WorkPackageTableSumsRowController,
SortHeaderDirective,
// Query filters
WorkPackageFilterContainerComponent,
QueryFiltersComponent,
QueryFilterComponent,
FilterBooleanValueComponent,
FilterDateValueComponent,
FilterDatesValueComponent,
FilterDateTimeValueComponent,
FilterDateTimesValueComponent,
FilterIntegerValueComponent,
FilterStringValueComponent,
FilterToggledMultiselectValueComponent,
// Add functionality to rails rendered templates
HideSectionComponent,
HideSectionLinkComponent,
@ -397,7 +423,14 @@ import {WorkPackageFormAttributeGroupComponent} from 'core-components/wp-form-gr
WorkPackageInlineCreateComponent,
// Embedded table
WorkPackageEmbeddedTableComponent
WorkPackageEmbeddedTableComponent,
// Modals
WpTableConfigurationModalComponent,
WpTableConfigurationColumnsTab,
WpTableConfigurationDisplaySettingsTab,
WpTableConfigurationFiltersTab,
WpTableConfigurationSortByTab,
WpTableConfigurationTimelinesTab,
],
entryComponents: [
WorkPackagesListComponent,
@ -442,7 +475,15 @@ import {WorkPackageFormAttributeGroupComponent} from 'core-components/wp-form-gr
WorkPackageEmbeddedTableComponent,
// Relations tab (ng1 -> ng2)
WorkPackageRelationsHierarchyComponent
WorkPackageRelationsHierarchyComponent,
// Modals
WpTableConfigurationModalComponent,
WpTableConfigurationColumnsTab,
WpTableConfigurationDisplaySettingsTab,
WpTableConfigurationFiltersTab,
WpTableConfigurationSortByTab,
WpTableConfigurationTimelinesTab,
]
})
export class OpenProjectModule {

@ -29,6 +29,8 @@
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!!! NEVER CHANGE THE ORDER OF THE REQUIRE IMPORTS !!!!
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// tslint:disable:ordered-imports
require('angular');
require('zone.js');
require('zone.js/dist/long-stack-trace-zone');

@ -37,14 +37,10 @@ export const $localeToken = new InjectionToken<any>('$locale');
export const $stateToken = new InjectionToken<StateService>('$state');
export const I18nToken = new InjectionToken<op.I18n>('I18n');
export const columnsModalToken = new InjectionToken<any>('columnsModal');
export const sortingModalToken = new InjectionToken<any>('sortingModal');
export const groupingModalToken = new InjectionToken<any>('groupingModal');
export const shareModalToken = new InjectionToken<any>('shareModal');
export const saveModalToken = new InjectionToken<any>('saveModal');
export const settingsModalToken = new InjectionToken<any>('settingsModal');
export const exportModalToken = new InjectionToken<any>(' exportModal');
export const timelinesModalToken = new InjectionToken<any>('timelinesModal');
export const FocusHelperToken = new InjectionToken<any>('FocusHelper');
export const NotificationsServiceToken = new InjectionToken<any>('NotificationsService');
@ -57,6 +53,7 @@ export const $httpToken = new InjectionToken<any>('$http');
export const halResourceFactoryToken = new InjectionToken<any>('halResourceFactory');
export const wpDestroyModalToken = new InjectionToken<any>('wpDestroyModal');
export const OpContextMenuLocalsToken = new InjectionToken<any>('CONTEXT_MENU_LOCALS');
export const OpModalLocalsToken = new InjectionToken<any>('OP_MODAL_LOCALS');
export const HookServiceToken = new InjectionToken<any>('HookService');
export const UrlParamsHelperToken = new InjectionToken<any>('UrlParamsHelper');
export const QueryResourceToken = new InjectionToken<any>('QueryResource');

@ -26,15 +26,44 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {wpControllersModule} from '../../../angular-modules';
function columnsModalService(btfModal:any) {
return btfModal({
controller: 'ColumnsModalController',
controllerAs: '$ctrl',
afterFocusOn: '#work-packages-settings-button',
templateUrl: '/components/modals/columns-modal/columns-modal.service.html'
require('core-app/angular4-test-setup');
import {async, TestBed} from '@angular/core/testing';
import {AccessibleByKeyboardComponent} from 'core-components/a11y/accessible-by-keyboard.component';
import {ComponentFixture} from '@angular/core/testing/src/component_fixture';
describe('accessibleByKeyboard component', () => {
beforeEach(async(() => {
// noinspection JSIgnoredPromiseFromCall
TestBed.configureTestingModule({
declarations: [
AccessibleByKeyboardComponent
]
}).compileComponents();
}));
describe('inner element', function() {
let app:AccessibleByKeyboardComponent;
let fixture:ComponentFixture<AccessibleByKeyboardComponent>
let element:JQuery;
it('should render an inner link with specified classes', function() {
fixture = TestBed.createComponent(AccessibleByKeyboardComponent);
app = fixture.debugElement.componentInstance;
element = jQuery(fixture.elementRef.nativeElement);
app.linkClass = 'a-link-class';
app.spanClass = 'a-span-class';
fixture.detectChanges();
expect(element.find('a').hasClass('a-link-class')).to.be.true;
expect(element.find('a > span').hasClass('a-span-class')).to.be.true;
});
});
}
});
wpControllersModule.factory('columnsModal', columnsModalService);

@ -1,6 +1,6 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
// Copyright (C) 2012-2017 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.
@ -23,38 +23,41 @@
// 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.
// See doc/COPYRIGHT.rdoc for more details.
//++
/*jshint expr: true*/
describe('accessibleByKeyboard Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.uiComponents'));
beforeEach(angular.mock.module('openproject.templates'));
beforeEach(inject(function($rootScope, $compile) {
var html =
'<accessible-by-keyboard link-class="blue"></accessible-by-keyboard>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('inner element', function() {
beforeEach(function() {
compile();
});
it('should render an inner link with specified class', function() {
expect(element.find('a').hasClass('blue')).to.be.true;
});
});
});
import {Component, EventEmitter, Input, Output} from '@angular/core';
@Component({
selector: 'accessible-by-keyboard',
template: `
<a (click)="handleClick($event)"
(keydown.enter)="handleClick($event)"
role="link"
[ngClass]="linkClass"
[attr.disabled]="isDisabled"
[attr.title]="linkTitle"
[attr.aria-label]="linkAriaLabel"
href>
<span [ngClass]="spanClass">
<ng-content></ng-content>
</span>
</a>
`
})
export class AccessibleByKeyboardComponent {
@Output() execute = new EventEmitter<JQueryEventObject>();
@Input() isDisabled:boolean;
@Input() linkClass:string;
@Input() linkTitle:string;
@Input() spanClass:string;
@Input() linkAriaLabel:string;
public handleClick(event:JQueryEventObject) {
if (!this.isDisabled) {
this.execute.emit(event);
}
return false;
}
}

@ -26,22 +26,34 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {Component, Directive, ElementRef, Injector, Input, Output} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
import {opUiComponentsModule} from 'core-app/angular-modules';
@Directive({
selector: 'accessible-by-keyboard'
})
export class AccessibleByKeyboardDirectiveUpgraded extends UpgradeComponent {
@Output() execute: any;
@Input() isDisabled: boolean;
@Input() linkClass: string;
@Input() linkTitle: string;
@Input() spanClass: string;
@Input() linkAriaLabel: string;
constructor(elementRef:ElementRef, injector:Injector) {
super('accessibleByKeyboard', elementRef, injector);
opUiComponentsModule.directive(
'accessibleByKeyboard',
function() {
return {
restrict: 'E',
transclude: true,
scope: {
execute: '&',
isDisabled: '=',
linkClass: '@',
linkTitle: '@',
spanClass: '@',
linkAriaLabel: '@'
},
template: `
<a data-ng-click='isDisabled || execute({ "$event": $event })'
role="link"
class='{{ linkClass }}'
ng-disabled="isDisabled"
title='{{ linkTitle }}'
aria-label="{{ linkAriaLabel }}"
data-click-on-keypress="[13, 32]"
href>
<span ng-transclude class='{{ spanClass }}'></span>
</a>
`
};
}
}
);

@ -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);

@ -26,29 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {Component, ElementRef, Injector, Input} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
import {opUiComponentsModule} from '../../../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);
import {Component, Input} from '@angular/core';
@Component({
selector: 'op-icon',

@ -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');
});
});

@ -31,8 +31,8 @@ import {ApiV3FilterBuilder} from '../../api/api-v3/api-v3-filter-builder';
export class PathHelperService {
public readonly appBasePath:string;
constructor(public $window:ng.IWindowService) {
this.appBasePath = $window.appBasePath ? $window.appBasePath : '';
constructor() {
this.appBasePath = window.appBasePath ? window.appBasePath : '';
}
public get staticBase() {

@ -26,16 +26,13 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {filtersModule} from '../../../angular-modules';
import {Moment} from 'moment';
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource';
export abstract class AbstractDateTimeValueController {
public filter:QueryFilterInstanceResource;
constructor(protected $scope:ng.IScope,
protected I18n:op.I18n,
constructor(protected I18n:op.I18n,
protected TimezoneService:any) {
_.remove(this.filter.values as string[], value => !this.TimezoneService.isValidISODateTime(value));
}
@ -43,12 +40,21 @@ export abstract class AbstractDateTimeValueController {
public abstract get lowerBoundary():Moment
public abstract get upperBoundary():Moment
public get filterDateModelOptions() {
return {
updateOn: 'default change blur',
debounce: {'default': 400, 'change': 0, 'blur': 0}
};
};
public isoDateParser(data:string) {
if (!this.TimezoneService.isValidISODate(data)) {
return '';
}
var d = this.TimezoneService.parseLocalDateTime(data);
return this.TimezoneService.formattedISODateTime(d);
}
public isoDateFormatter(data:string) {
if (!this.TimezoneService.isValidISODateTime(data)) {
return '';
}
var d = this.TimezoneService.parseISODatetime(data);
return this.TimezoneService.formattedISODate(d);
}
public get isTimeZoneDifferent() {
let value = this.lowerBoundary || this.upperBoundary;

@ -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>

@ -26,21 +26,25 @@
// 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 {Component, EventEmitter, Inject, Input, Output} from '@angular/core';
import {I18nToken} from 'core-app/angular4-transition-utils';
export class BooleanValueController {
public filter:QueryFilterInstanceResource;
@Component({
selector: 'filter-boolean-value',
template: require('!!raw-loader!./filter-boolean-value.component.html')
})
export class FilterBooleanValueComponent {
@Input() public filter:QueryFilterInstanceResource;
@Output() public filterChanged = new EventEmitter<QueryFilterInstanceResource>();
public text:{ [key: string]: string; };
public text = {
placeholder: this.I18n.t('js.placeholders.selection'),
true: this.I18n.t('js.general_text_Yes'),
false: this.I18n.t('js.general_text_No')
}
constructor(public $scope:ng.IScope,
private I18n:op.I18n) {
this.text = {
placeholder: I18n.t('js.placeholders.selection'),
true: I18n.t('js.general_text_Yes'),
false: I18n.t('js.general_text_No')
};
constructor(@Inject(I18nToken) readonly I18n:op.I18n) {
}
public get value() {
@ -49,6 +53,7 @@ export class BooleanValueController {
public set value(val) {
this.filter.values[0] = val;
this.filterChanged.emit(this.filter);
}
public get hasNoValue() {
@ -59,19 +64,3 @@ export class BooleanValueController {
return [true, false];
}
}
function booleanValue():any {
return {
restrict: 'E',
replace: true,
scope: {
filter: '=',
},
templateUrl: '/components/filters/filter-boolean-value/filter-boolean-value.directive.html',
controller: BooleanValueController,
bindToController: true,
controllerAs: '$ctrl'
};
};
filtersModule.directive('filterBooleanValue', booleanValue);

@ -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>

@ -26,22 +26,31 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {filtersModule} from '../../../angular-modules';
import {Component} from '@angular/core';
import {Component, OnDestroy} from '@angular/core';
import {WorkPackageTableFiltersService} from 'core-components/wp-fast-table/state/wp-table-filters.service';
import {WorkPackageTableFilters} from 'core-components/wp-fast-table/wp-table-filters';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
import WorkPackageFiltersService from 'core-components/filters/wp-filters/wp-filters.service';
import {downgradeComponent} from '@angular/upgrade/static';
@Component({
template: require('!!raw-loader!core-components/filters/filter-container/filter-container.directive.html'),
selector: 'filter-container',
})
export class WorkPackageFilterContainerComponent {
constructor(public wpFiltersService:WorkPackageFiltersService) {
export class WorkPackageFilterContainerComponent implements OnDestroy {
public filters = this.wpTableFilters.currentState;
constructor(readonly wpTableFilters:WorkPackageTableFiltersService,
readonly wpFiltersService:WorkPackageFiltersService) {
this.wpTableFilters
.observeUntil(componentDestroyed(this))
.subscribe(() => this.filters = this.wpTableFilters.currentState);
}
}
filtersModule.directive(
'filterContainer',
downgradeComponent({ component: WorkPackageFilterContainerComponent })
);
ngOnDestroy() {
// Nothing to do, added for interface compatibility
}
public replaceIfComplete(filters:WorkPackageTableFilters) {
this.wpTableFilters.replaceIfComplete(this.filters);
}
}

@ -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>

@ -26,17 +26,28 @@
// 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 {AbstractDateTimeValueController} from '../abstract-filter-date-time-value/abstract-filter-date-time-value.controller'
import {Component, EventEmitter, Inject, Input, OnDestroy, Output} from '@angular/core';
import {I18nToken, TimezoneServiceToken} from 'core-app/angular4-transition-utils';
import {DebouncedEventEmitter} from 'core-components/angular/debounced-event-emitter';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
@Component({
selector: 'filter-date-time-value',
template: require('!!raw-loader!./filter-date-time-value.component.html')
})
export class FilterDateTimeValueComponent extends AbstractDateTimeValueController implements OnDestroy {
@Input() public filter:QueryFilterInstanceResource;
@Output() public filterChanged = new DebouncedEventEmitter<QueryFilterInstanceResource>(componentDestroyed(this));
export class DateTimeValueController extends AbstractDateTimeValueController {
constructor(@Inject(I18nToken) readonly I18n:op.I18n,
@Inject(TimezoneServiceToken) readonly TimezoneService:any) {
super(I18n, TimezoneService);
}
constructor(protected $scope:ng.IScope,
protected I18n:op.I18n,
protected TimezoneService:any) {
super($scope, I18n, TimezoneService);
ngOnDestroy() {
// Nothing to do, added for interface compatibility
}
public get value() {
@ -45,37 +56,18 @@ export class DateTimeValueController extends AbstractDateTimeValueController {
public set value(val) {
this.filter.values = [val as string];
this.filterChanged.emit(this.filter);
}
public get lowerBoundary() {
if (this.value && this.TimezoneService.isValidISODateTime(this.value)) {
return this.TimezoneService.parseDatetime(this.value);
} else {
null
}
}
public get upperBoundary() {
if (this.value && this.TimezoneService.isValidISODateTime(this.value)) {
return this.TimezoneService.parseDatetime(this.value).add(24, 'hours');
} else {
null
}
}
}
function dateTimeValue():any {
return {
restrict: 'E',
replace: true,
scope: {
filter: '=',
},
templateUrl: '/components/filters/filter-date-time-value/filter-date-time-value.directive.html',
controller: DateTimeValueController,
bindToController: true,
controllerAs: '$ctrl'
};
};
filtersModule.directive('filterDateTimeValue', dateTimeValue);

@ -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>

@ -26,17 +26,33 @@
// 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 {AbstractDateTimeValueController} from '../abstract-filter-date-time-value/abstract-filter-date-time-value.controller'
import {Component, Inject, Input, OnDestroy, Output} from '@angular/core';
import {TimezoneServiceToken} from 'core-app/angular4-transition-utils';
import {I18nToken} from '../../../angular4-transition-utils';
import {DebouncedEventEmitter} from 'core-components/angular/debounced-event-emitter';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
@Component({
selector: 'filter-date-times-value',
template: require('!!raw-loader!./filter-date-times-value.component.html')
})
export class FilterDateTimesValueComponent extends AbstractDateTimeValueController implements OnDestroy {
@Input() public filter:QueryFilterInstanceResource;
@Output() public filterChanged = new DebouncedEventEmitter<QueryFilterInstanceResource>(componentDestroyed(this));
readonly text = {
spacer: this.I18n.t('js.filter.value_spacer')
};
export class DateTimesValueController extends AbstractDateTimeValueController {
constructor(@Inject(I18nToken) readonly I18n:op.I18n,
@Inject(TimezoneServiceToken) readonly TimezoneService:any) {
super(I18n, TimezoneService);
}
constructor(protected $scope:ng.IScope,
protected I18n:op.I18n,
protected TimezoneService:any) {
super($scope, I18n, TimezoneService);
ngOnDestroy() {
// Nothing to do, added for interface compatibility
}
public get begin() {
@ -45,6 +61,7 @@ export class DateTimesValueController extends AbstractDateTimeValueController {
public set begin(val) {
this.filter.values[0] = val || '';
this.filterChanged.emit(this.filter);
}
public get end() {
@ -53,6 +70,7 @@ export class DateTimesValueController extends AbstractDateTimeValueController {
public set end(val) {
this.filter.values[1] = val || '';
this.filterChanged.emit(this.filter);
}
public get lowerBoundary() {
@ -71,19 +89,3 @@ export class DateTimesValueController extends AbstractDateTimeValueController {
}
}
}
function dateTimesValue():any {
return {
restrict: 'E',
replace: true,
scope: {
filter: '=',
},
templateUrl: '/components/filters/filter-date-times-value/filter-date-times-value.directive.html',
controller: DateTimesValueController,
bindToController: true,
controllerAs: '$ctrl'
};
};
filtersModule.directive('filterDateTimesValue', dateTimesValue);

@ -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>

@ -26,15 +26,26 @@
// 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 {I18nToken, TimezoneServiceToken} from 'core-app/angular4-transition-utils';
import {Component, EventEmitter, Inject, Input, OnDestroy, Output} from '@angular/core';
import {DebouncedEventEmitter} from 'core-components/angular/debounced-event-emitter';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
@Component({
selector: 'filter-date-value',
template: require('!!raw-loader!./filter-date-value.component.html')
})
export class FilterDateValueComponent implements OnDestroy {
@Input() public filter:QueryFilterInstanceResource;
@Output() public filterChanged = new DebouncedEventEmitter<QueryFilterInstanceResource>(componentDestroyed(this));
export class DateValueController {
public filter:QueryFilterInstanceResource;
constructor(@Inject(TimezoneServiceToken) readonly TimezoneService:any,
@Inject(I18nToken) readonly I18n:op.I18n) {
}
constructor(public $scope:ng.IScope,
public I18n:op.I18n) {
ngOnDestroy() {
// Nothing to do, added for interface compatibility
}
public get value() {
@ -43,28 +54,23 @@ export class DateValueController {
public set value(val) {
this.filter.values = [val as string];
this.filterChanged.emit(this.filter);
}
public get filterDateModelOptions() {
return {
updateOn: 'default change blur',
debounce: {'default': 400, 'change': 0, 'blur': 0}
};
};
}
function dateValue():any {
return {
restrict: 'E',
replace: true,
scope: {
filter: '=',
},
templateUrl: '/components/filters/filter-date-value/filter-date-value.directive.html',
controller: DateValueController,
bindToController: true,
controllerAs: '$ctrl'
};
};
public parser(data:any) {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
return data;
} else {
return null;
}
}
filtersModule.directive('filterDateValue', dateValue);
public formatter(data:any) {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
var d = this.TimezoneService.parseDate(data);
return this.TimezoneService.formattedISODate(d);
} else {
return null;
}
}
}

@ -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>

@ -26,15 +26,30 @@
// 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 {Component, Inject, Input, OnDestroy, Output} from '@angular/core';
import {I18nToken, TimezoneServiceToken} from 'core-app/angular4-transition-utils';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
import {DebouncedEventEmitter} from 'core-components/angular/debounced-event-emitter';
@Component({
selector: 'filter-dates-value',
template: require('!!raw-loader!./filter-dates-value.component.html')
})
export class FilterDatesValueComponent implements OnDestroy {
@Input() public filter:QueryFilterInstanceResource;
@Output() public filterChanged = new DebouncedEventEmitter<QueryFilterInstanceResource>(componentDestroyed(this));
readonly text = {
spacer: this.I18n.t('js.filter.value_spacer')
};
export class DatesValueController {
public filter:QueryFilterInstanceResource;
constructor(@Inject(TimezoneServiceToken) readonly TimezoneService:any,
@Inject(I18nToken) readonly I18n:op.I18n) {
}
constructor(public $scope:ng.IScope,
public I18n:op.I18n) {
ngOnDestroy() {
// Nothing to do, added for interface compatibility
}
public get begin() {
@ -43,6 +58,7 @@ export class DatesValueController {
public set begin(val) {
this.filter.values[0] = val || '';
this.filterChanged.emit(this.filter);
}
public get end() {
@ -51,28 +67,23 @@ export class DatesValueController {
public set end(val) {
this.filter.values[1] = val || '';
this.filterChanged.emit(this.filter);
}
public get filterDateModelOptions() {
return {
updateOn: 'default change blur',
debounce: {'default': 400, 'change': 0, 'blur': 0}
};
};
}
function datesValue():any {
return {
restrict: 'E',
replace: true,
scope: {
filter: '=',
},
templateUrl: '/components/filters/filter-dates-value/filter-dates-value.directive.html',
controller: DatesValueController,
bindToController: true,
controllerAs: '$ctrl'
};
};
public parser(data:any) {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
return data;
} else {
return null;
}
}
filtersModule.directive('filterDatesValue', datesValue);
public formatter(data:any) {
if (moment(data, 'YYYY-MM-DD', true).isValid()) {
var d = this.TimezoneService.parseDate(data);
return this.TimezoneService.formattedISODate(d);
} else {
return null;
}
}
}

@ -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>

@ -27,16 +27,26 @@
//++
import {filtersModule} from '../../../angular-modules';
import {HalResource} from 'core-app/modules/hal/resources/hal-resource';
import {QueryFilterResource} from 'core-app/modules/hal/resources/query-filter-resource';
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource';
import {Component, EventEmitter, Inject, Input, OnDestroy, Output} from '@angular/core';
import {I18nToken} from 'core-app/angular4-transition-utils';
import {DebouncedEventEmitter} from 'core-components/angular/debounced-event-emitter';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
export class IntegerValueController {
public filter:QueryFilterInstanceResource;
@Component({
selector: 'filter-integer-value',
template: require('!!raw-loader!./filter-integer-value.component.html')
})
export class FilterIntegerValueComponent implements OnDestroy {
@Input() public filter:QueryFilterInstanceResource;
@Output() public filterChanged = new DebouncedEventEmitter<QueryFilterInstanceResource>(componentDestroyed(this));
constructor(public $scope:ng.IScope,
public I18n:op.I18n) {
constructor(@Inject(I18nToken) readonly I18n:op.I18n) {
}
ngOnDestroy() {
// Nothing to do, added for interface compatibility
}
public get value() {
@ -49,14 +59,9 @@ export class IntegerValueController {
} else {
this.filter.values = [];
}
}
public get filterModelOptions() {
return {
updateOn: 'default blur',
debounce: {'default': 400, 'blur': 0 }
};
};
this.filterChanged.emit(this.filter);
}
public get unit() {
switch ((this.filter.schema.filter.allowedValues as QueryFilterResource[])[0].id) {
@ -70,19 +75,3 @@ export class IntegerValueController {
}
}
}
function integerValue():any {
return {
restrict: 'E',
replace: true,
scope: {
filter: '=',
},
templateUrl: '/components/filters/filter-integer-value/filter-integer-value.directive.html',
controller: IntegerValueController,
bindToController: true,
controllerAs: '$ctrl'
};
};
filtersModule.directive('filterIntegerValue', integerValue);

@ -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>

@ -26,14 +26,29 @@
// 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 {Component, Inject, Input, OnDestroy, Output} from '@angular/core';
import {I18nToken} from 'core-app/angular4-transition-utils';
import {DebouncedEventEmitter} from 'core-components/angular/debounced-event-emitter';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
@Component({
selector: 'filter-string-value',
template: require('!!raw-loader!./filter-string-value.component.html')
})
export class FilterStringValueComponent implements OnDestroy {
@Input() public filter:QueryFilterInstanceResource;
@Output() public filterChanged = new DebouncedEventEmitter<QueryFilterInstanceResource>(componentDestroyed(this));
readonly text = {
enter_text: this.I18n.t('js.work_packages.description_enter_text')
};
export class StringValueController {
public filter:QueryFilterInstanceResource;
constructor(@Inject(I18nToken) readonly I18n:op.I18n) {
}
constructor(public $scope:ng.IScope,
public I18n:op.I18n) {
ngOnDestroy() {
// Nothing to do, added for interface compatibility
}
public get value() {
@ -42,28 +57,6 @@ export class StringValueController {
public set value(val) {
this.filter.values[0] = val || '';
this.filterChanged.emit(this.filter);
}
public get filterModelOptions() {
return {
updateOn: 'default blur',
debounce: {'default': 400, 'blur': 0}
};
};
}
function stringValue():any {
return {
restrict: 'E',
replace: true,
scope: {
filter: '=',
},
templateUrl: '/components/filters/filter-string-value/filter-string-value.directive.html',
controller: StringValueController,
bindToController: true,
controllerAs: '$ctrl'
};
};
filtersModule.directive('filterStringValue', stringValue);

@ -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);
});
});
});
});

@ -26,57 +26,60 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {filtersModule} from '../../../angular-modules';
import {HalResource} from 'core-app/modules/hal/resources/hal-resource';
import {UserResource} from 'core-app/modules/hal/resources/user-resource';
import {CollectionResource} from 'core-app/modules/hal/resources/collection-resource';
import {RootResource} from 'core-app/modules/hal/resources/root-resource';
import {PathHelperService} from '../../common/path-helper/path-helper.service';
import {$injectFields} from '../../angular/angular-injector-bridge.functions';
import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource';
import {RootDmService} from 'core-app/modules/dm-services/root-dm.service';
import {Injector} from '@angular/core';
import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core';
import {I18nToken, PathHelperToken} from 'core-app/angular4-transition-utils';
import {AngularTrackingHelpers} from 'core-components/angular/tracking-functions';
import {HalResourceFactoryService} from 'core-app/modules/hal/services/hal-resource-factory.service';
export class ToggledMultiselectController {
// Injected
public PathHelper:PathHelperService;
@Component({
selector: 'filter-toggled-multiselect-value',
template: require('!!raw-loader!./filter-toggled-multiselect-value.component.html')
})
export class FilterToggledMultiselectValueComponent implements OnInit {
@Input() public filter:QueryFilterInstanceResource;
@Output() public filterChanged = new EventEmitter<QueryFilterInstanceResource>();
public isMultiselect: boolean;
public filter:QueryFilterInstanceResource;
public isMultiselect:boolean;
public availableOptions:HalResource[] = [];
public compareByHrefOrString = AngularTrackingHelpers.compareByHrefOrString;
public text:{ [key: string]: string; };
constructor(public $scope:ng.IScope,
public injector:Injector,
private I18n:op.I18n,
private $q:ng.IQService,
private RootDm:RootDmService) {
$injectFields(this, 'PathHelper');
this.isMultiselect = this.isValueMulti(true);
readonly text = {
placeholder: this.I18n.t('js.placeholders.selection'),
enableMulti: this.I18n.t('js.work_packages.label_enable_multi_select'),
disableMulti: this.I18n.t('js.work_packages.label_disable_multi_select')
};
this.text = {
placeholder: I18n.t('js.placeholders.selection'),
enableMulti: I18n.t('js.work_packages.label_enable_multi_select'),
disableMulti: I18n.t('js.work_packages.label_disable_multi_select')
};
constructor(readonly RootDm:RootDmService,
readonly halResourceFactory:HalResourceFactoryService,
@Inject(PathHelperToken) readonly PathHelper:PathHelperService,
@Inject(I18nToken) readonly I18n:op.I18n) {
}
ngOnInit() {
this.isMultiselect = this.isValueMulti(true);
this.fetchAllowedValues();
}
public get value() {
if (this.isValueMulti()) {
return this.filter.values;
} else {
} else if (this.filter.values.length > 0) {
return this.filter.values[0];
} else {
return null;
}
}
public set value(val) {
let valToSet = Array.isArray(val) ? val as HalResource[] : [val as HalResource]
this.filter.values = valToSet;
public set value(val:any) {
this.filter.values = _.castArray(val);
this.filterChanged.emit(this.filter);
}
public isValueMulti(ignoreStatus = false) {
@ -86,16 +89,13 @@ export class ToggledMultiselectController {
public toggleMultiselect() {
this.isMultiselect = !this.isMultiselect;
};
return false;
}
public get hasNoValue() {
return _.isEmpty(this.filter.values);
}
private setAvailableOptions(options:CollectionResource) {
this.availableOptions = options.elements;
}
private fetchAllowedValues() {
if ((this.filter.currentSchema!.values!.allowedValues as CollectionResource)['$load']) {
this.loadAllowedValues();
@ -118,7 +118,7 @@ export class ToggledMultiselectController {
loadingPromises.push(this.RootDm.load());
}
this.$q.all(loadingPromises)
Promise.all(loadingPromises)
.then(((resources:Array<HalResource>) => {
let options = (resources[0] as CollectionResource).elements;
@ -127,7 +127,7 @@ export class ToggledMultiselectController {
}
this.availableOptions = options;
}).bind(this));
}));
}
private addMeValue(options:HalResource[], currentUser:UserResource) {
@ -135,31 +135,18 @@ export class ToggledMultiselectController {
return;
}
let me:HalResource = new HalResource(this.injector, {
_links: {
self: {
href: this.PathHelper.apiV3UserMePath(),
title: this.I18n.t('js.label_me')
let me:HalResource = this.halResourceFactory.createHalResourceOfType(
HalResource,
{
_links: {
self: {
href: this.PathHelper.apiV3UserMePath(),
title: this.I18n.t('js.label_me')
}
}
}
}, true);
}, true
);
options.unshift(me);
}
}
function toggledMultiselect():any {
return {
restrict: 'EA',
replace: true,
scope: {
filter: '=',
},
templateUrl: '/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.html',
controller: ToggledMultiselectController,
bindToController: true,
controllerAs: '$ctrl'
};
};
filtersModule.directive('filterToggledMultiselectValue', toggledMultiselect);

@ -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);

@ -28,7 +28,7 @@
import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service";
import {Directive, ElementRef, Inject, Input} from "@angular/core";
import {columnsModalToken, I18nToken} from "core-app/angular4-transition-utils";
import {I18nToken} from "core-app/angular4-transition-utils";
import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-context-menu-trigger.directive";
import {QueryColumn} from "core-components/wp-query/query-column";
import {WorkPackageTableSortByService} from "core-components/wp-fast-table/state/wp-table-sort-by.service";
@ -36,6 +36,8 @@ import {WorkPackageTableHierarchiesService} from "core-components/wp-fast-table/
import {WorkPackageTableGroupByService} from "core-components/wp-fast-table/state/wp-table-group-by.service";
import {WorkPackageTableColumnsService} from "core-components/wp-fast-table/state/wp-table-columns.service";
import {WorkPackageTable} from 'core-components/wp-fast-table/wp-fast-table';
import {WpTableConfigurationModalComponent} from 'core-components/wp-table/configuration-modal/wp-table-configuration.modal';
import {OpModalService} from 'core-components/op-modals/op-modal.service';
@Directive({
selector: '[opColumnsContextMenu]'
@ -50,7 +52,7 @@ export class OpColumnsContextMenu extends OpContextMenuTrigger {
readonly wpTableSortBy:WorkPackageTableSortByService,
readonly wpTableGroupBy:WorkPackageTableGroupByService,
readonly wpTableHierarchies:WorkPackageTableHierarchiesService,
@Inject(columnsModalToken) readonly columnsModal:any,
readonly opModalService:OpModalService,
@Inject(I18nToken) readonly I18n:op.I18n) {
super(elementRef, opContextMenu);
@ -165,7 +167,10 @@ export class OpColumnsContextMenu extends OpContextMenuTrigger {
linkText: this.I18n.t('js.work_packages.query.insert_columns'),
icon: 'icon-columns',
onClick: () => {
this.columnsModal.activate();
this.opModalService.show<WpTableConfigurationModalComponent>(
WpTableConfigurationModalComponent,
{ initialTab: 'columns' }
);
return true;
}
}

@ -28,26 +28,16 @@
import {Directive, ElementRef, Inject, Input, OnDestroy} from '@angular/core';
import {
columnsModalToken,
exportModalToken,
groupingModalToken,
I18nToken,
saveModalToken,
settingsModalToken,
shareModalToken,
sortingModalToken,
timelinesModalToken
shareModalToken
} from 'core-app/angular4-transition-utils';
import {AuthorisationService} from 'core-components/common/model-auth/model-auth.service';
import {OpContextMenuTrigger} from 'core-components/op-context-menu/handlers/op-context-menu-trigger.directive';
import {OPContextMenuService} from 'core-components/op-context-menu/op-context-menu.service';
import {States} from 'core-components/states.service';
import {WorkPackageTableColumnsService} from 'core-components/wp-fast-table/state/wp-table-columns.service';
import {WorkPackageTableGroupByService} from 'core-components/wp-fast-table/state/wp-table-group-by.service';
import {WorkPackageTableHierarchiesService} from 'core-components/wp-fast-table/state/wp-table-hierarchy.service';
import {WorkPackageTableSortByService} from 'core-components/wp-fast-table/state/wp-table-sort-by.service';
import {WorkPackageTableSumService} from 'core-components/wp-fast-table/state/wp-table-sum.service';
import {WorkPackageTableTimelineService} from 'core-components/wp-fast-table/state/wp-table-timeline.service';
import {WorkPackagesListService} from 'core-components/wp-list/wp-list.service';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
import {takeUntil} from 'rxjs/operators';
@ -64,23 +54,13 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements OnD
constructor(readonly elementRef:ElementRef,
readonly opContextMenu:OPContextMenuService,
readonly wpTableColumns:WorkPackageTableColumnsService,
readonly wpTableSortBy:WorkPackageTableSortByService,
readonly wpTableGroupBy:WorkPackageTableGroupByService,
readonly wpTableHierarchies:WorkPackageTableHierarchiesService,
readonly wpTableTimeline:WorkPackageTableTimelineService,
readonly wpTableSum:WorkPackageTableSumService,
readonly wpListService:WorkPackagesListService,
readonly authorisationService:AuthorisationService,
readonly states:States,
@Inject(columnsModalToken) readonly columnsModal:any,
@Inject(sortingModalToken) readonly sortingModal:any,
@Inject(groupingModalToken) readonly groupingModal:any,
@Inject(shareModalToken) readonly shareModal:any,
@Inject(saveModalToken) readonly saveModal:any,
@Inject(settingsModalToken) readonly settingsModal:any,
@Inject(exportModalToken) readonly exportModal:any,
@Inject(timelinesModalToken) readonly timelinesModal:any,
@Inject(I18nToken) readonly I18n:op.I18n) {
super(elementRef, opContextMenu);
@ -167,76 +147,6 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements OnD
private buildItems() {
this.items = [
{
// Columns modal
linkText: this.I18n.t('js.toolbar.settings.columns'),
icon: 'icon-columns',
onClick: () => {
this.columnsModal.activate();
return true;
}
},
{
// Sort-by modal
linkText: this.I18n.t('js.toolbar.settings.sort_by'),
icon: 'icon-sort-by',
onClick: () => {
this.sortingModal.activate();
return true;
}
},
{
// Group-by modal
linkText: this.I18n.t('js.toolbar.settings.group_by'),
icon: 'icon-group-by',
onClick: () => {
this.groupingModal.activate();
return true;
}
},
{
// Sums (not active)
hidden: !this.wpTableSum.isEnabled,
linkText: this.I18n.t('js.toolbar.settings.display_sums'),
ariaLabel: this.I18n.t('js.toolbar.settings.display_sums'),
icon: 'no-icon',
onClick: () => {
this.wpTableSum.toggle();
return true;
}
},
{
// Sums (active)
hidden: this.wpTableSum.isEnabled,
linkText: this.I18n.t('js.toolbar.settings.display_sums'),
ariaLabel: this.I18n.t('js.toolbar.settings.hide_sums'),
icon: 'icon-checkmark',
onClick: () => {
this.wpTableSum.toggle();
return true;
}
},
{
// Hierarchies (not active)
hidden: this.wpTableHierarchies.isEnabled,
linkText: this.I18n.t('js.toolbar.settings.display_hierarchy'),
icon: 'icon-hierarchy',
onClick: () => {
this.wpTableHierarchies.setEnabled(true);
return true;
}
},
{
// Hierarchies (active)
hidden: !this.wpTableHierarchies.isEnabled,
linkText: this.I18n.t('js.toolbar.settings.hide_hierarchy'),
icon: 'icon-no-hierarchy',
onClick: () => {
this.wpTableHierarchies.setEnabled(false);
return true;
}
},
{divider: true},
{
// Query save modal
disabled: this.authorisationService.cannot('query', 'updateImmediately'),
@ -330,16 +240,6 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger implements OnD
linkText: this.query.results.customFields && this.query.results.customFields.name,
icon: 'icon-custom-fields',
onClick: () => false
},
{
// Timelines modal
disabled: !this.wpTableTimeline.isVisible,
linkText: this.I18n.t('js.timelines.gantt_chart') + ' ...',
icon: 'icon-view-timeline',
onClick: () => {
this.timelinesModal.activate();
return true;
}
}
];
}

@ -8,6 +8,7 @@ import {OpContextMenuHandler} from "core-components/op-context-menu/op-context-m
import {FocusHelperToken, OpContextMenuLocalsToken} from "core-app/angular4-transition-utils";
import {OpContextMenuLocalsMap} from "core-components/op-context-menu/op-context-menu.types";
import {OPContextMenuComponent} from "core-components/op-context-menu/op-context-menu.component";
import {keyCodes} from 'core-components/common/keyCodes.enum';
@Injectable()
export class OPContextMenuService {
@ -42,7 +43,13 @@ export class OPContextMenuService {
$transitions.onStart({}, () => this.close());
// Listen to keyups on window to close context menus
Mousetrap.bind('escape', () => this.close());
jQuery(window).keydown('keydown', (evt:JQueryKeyEventObject) => {
if (this.active && evt.which === keyCodes.ESCAPE) {
this.close();
}
return true;
});
// Listen to any click and close the active context menu
jQuery(window).click((evt) => {

@ -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;
};

@ -28,7 +28,7 @@
import {TestBed} from '@angular/core/testing';
require('../../angular4-test-setup');
require('core-app/angular4-test-setup');
import {SchemaCacheService} from 'core-components/schemas/schema-cache.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';

@ -26,79 +26,78 @@
// See docs/COPYRIGHT.rdoc for more details.
//++
module.exports = function(TimezoneService, ConfigurationService, $timeout) {
var datepickerFormat = 'yy-mm-dd';
import {ConfigurationService} from '../../common/config/configuration.service';
function Datepicker(datepickerElem, date, options) {
this.date = date;
this.datepickerCont = angular.element(datepickerElem);
this.datepickerInstance = null;
export class DatePicker {
public datepickerFormat = 'yy-mm-dd';
private datepickerCont = jQuery(this.datepickerElem);
private datepickerInstance:any = null;
constructor(readonly ConfigurationService:ConfigurationService,
readonly TimezoneService:any,
private datepickerElem:JQuery,
private date:any,
private options:any) {
this.initialize(options);
}
Datepicker.prototype.initialize = function(options) {
var self = this,
firstDayOfWeek = ConfigurationService.startOfWeekPresent() ?
ConfigurationService.startOfWeek() :
jQuery.datepicker._defaults.firstDay;
private initialize(options:any) {
const firstDayOfWeek = this.ConfigurationService.startOfWeekPresent() ?
this.ConfigurationService.startOfWeek() : (jQuery.datepicker as any)._defaults.firstDay;
var mergedOptions = angular.extend({}, options, {
firstDay: firstDayOfWeek,
showWeeks: true,
changeMonth: true,
changeYear: true,
dateFormat: datepickerFormat,
defaultDate: TimezoneService.formattedISODate(self.date),
dateFormat: this.datepickerFormat,
defaultDate: this.TimezoneService.formattedISODate(this.date),
showButtonPanel: true
});
this.datepickerInstance = this.datepickerCont.datepicker(mergedOptions);
};
}
Datepicker.prototype.clear = function() {
this.datepickerInstance.datepicker('setDate' , null);
};
private clear() {
this.datepickerInstance.datepicker('setDate', null);
}
Datepicker.prototype.hide = function() {
private hide() {
this.datepickerInstance.datepicker('hide');
this.datepickerCont.scrollParent().off('scroll');
};
}
Datepicker.prototype.show = function() {
private show() {
this.datepickerInstance.datepicker('show');
this.hideDuringScroll();
};
}
Datepicker.prototype.reshow = function() {
private reshow() {
this.datepickerInstance.datepicker('show');
};
Datepicker.prototype.hideDuringScroll = function() {
var hide = jQuery.proxy(function() { this.datepickerInstance.datepicker('hide'); }, this),
show = jQuery.proxy(function() { this.datepickerInstance.datepicker('show'); }, this),
reshowTimeout,
scrollParent = this.datepickerCont.scrollParent(),
visibleAndActive = jQuery.proxy(this.visibleAndActive, this);
}
scrollParent.scroll(function() {
hide();
private hideDuringScroll() {
let reshowTimeout:any = null;
let scrollParent = this.datepickerCont.scrollParent();
$timeout.cancel(reshowTimeout);
scrollParent.scroll(() => {
this.datepickerInstance.datepicker('hide');
if (reshowTimeout) {
clearTimeout(reshowTimeout);
}
reshowTimeout = $timeout(function() {
if(visibleAndActive()) {
show();
reshowTimeout = setTimeout(() => {
if (this.visibleAndActive()) {
this.datepickerInstance.datepicker('show');
}
}, 50);
});
};
}
Datepicker.prototype.visibleAndActive = function() {
private visibleAndActive() {
var input = this.datepickerCont;
return document.elementFromPoint(input.offset().left, input.offset().top) === input[0] &&
document.activeElement === input[0];
};
return Datepicker;
};
}

@ -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();
}
}

@ -26,6 +26,11 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
// TODO we still have non-upgraded components depending on the ng1 datepicker
// Remove when this is no longer the case and migrate to the ng2 component instead.
import {DatePicker} from 'core-components/wp-edit/op-date-picker/datepicker';
class OPDatePickerController {
public onChange?:Function;
public onClose?:Function;
@ -36,8 +41,7 @@ class OPDatePickerController {
public constructor(private $element:ng.IAugmentedJQuery,
private ConfigurationService:any,
private TimezoneService:any,
private Datepicker:any) {
private TimezoneService:any) {
'ngInject';
}
@ -102,7 +106,13 @@ class OPDatePickerController {
initialValue = this.currentValue();
}
this.datePickerInstance = new this.Datepicker(this.input, initialValue, options);
this.datePickerInstance = new DatePicker(
this.ConfigurationService,
this.TimezoneService,
this.input,
initialValue,
options
);
this.datePickerInstance.show();
}
}

@ -33,5 +33,3 @@ export function scrollTableRowIntoView(workPackageId:string):void {
console.warn("Can't scroll row element into view: " + e);
}
}

@ -77,8 +77,8 @@ export abstract class WorkPackageTableBaseService<T> {
return this.state.values$().pipe(takeUntil(unsubscribe));
}
public async onReady(scope:ng.IScope) {
return scopedObservable(scope, this.state.values$())
public async onReady() {
return this.state.values$()
.pipe(
take(1),
mapTo(null)

@ -75,6 +75,13 @@ export class WorkPackageTableSumService extends WorkPackageTableBaseService<Work
this.state.putValue(currentState);
}
public setEnabled(value:boolean) {
let currentState = this.current;
currentState.current = value;
this.state.putValue(currentState);
}
public get isEnabled() {
return this.current.isEnabled;
}

@ -71,8 +71,12 @@ export class WorkPackageTableTimelineService extends WorkPackageTableBaseService
public toggle() {
let currentState = this.current;
this.setVisible(!currentState.isVisible);
}
currentState.toggle();
public setVisible(value:boolean) {
let currentState = this.current;
currentState.visible = value;
this.state.putValue(currentState);
}

@ -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) {
}
}

@ -23,16 +23,16 @@
[opColumnsContextMenu-table]="workPackageTable">
</sortHeader>
</th>
<th class="wp-table--context-menu-column -short hide-when-print"
<th class="wp-table--configuration-modal--trigger -short hide-when-print"
*ngIf="configuration.columnMenuEnabled || configuration.actionsColumnEnabled">
<div class="generic-table--sort-header-outer">
<accessible-by-keyboard
(execute)="openColumnsModal()"
(execute)="openTableConfigurationModal()"
linkClass="wp-table--columns-selection"
linkTitle="{{text.addColumns}}"
linkAriaLabel="{{text.addColumns}}"
[linkTitle]="text.addColumns"
[linkAriaLabel]="text.configureTable"
*ngIf="configuration.columnMenuEnabled">
<op-icon icon-classes="icon-button icon-small icon-add"></op-icon>
<op-icon icon-classes="icon-button icon-small icon-settings"></op-icon>
</accessible-by-keyboard>
</div>
</th>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save