Implement column modal service

pull/6245/head
Oliver Günther 7 years ago
parent 6f01a18be2
commit 5b8bdac753
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 14
      frontend/app/angular4-modules.ts
  2. 1
      frontend/app/angular4-transition-utils.ts
  3. 61
      frontend/app/components/op-modals/modals/columns-modal.component.html
  4. 120
      frontend/app/components/op-modals/modals/columns-modal.component.ts
  5. 45
      frontend/app/components/op-modals/op-modal.component.ts
  6. 110
      frontend/app/components/op-modals/op-modal.service.ts
  7. 7
      frontend/app/components/op-modals/op-modal.types.ts
  8. 6
      frontend/app/components/wp-table/wp-table.directive.ts

@ -188,6 +188,8 @@ import {Ng1RelationsCreateWrapper} from 'core-components/wp-relations/wp-relatio
import {WpRelationsAutocompleteComponent} from 'core-components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.upgraded.component';
import {WpRelationAddChildComponent} from 'core-components/wp-relations/wp-relation-add-child/wp-relation-add-child';
import {WpRelationParentComponent} from 'core-components/wp-relations/wp-relations-parent/wp-relations-parent.component';
import {OpModalService} from 'core-components/op-modals/op-modal.service';
import {ColumnsModalComponent} from 'core-components/op-modals/modals/columns-modal.component';
@NgModule({
imports: [
@ -282,6 +284,9 @@ import {WpRelationParentComponent} from 'core-components/wp-relations/wp-relatio
WorkPackageContextMenuHelperService,
QueryFormDmService,
TableState,
// OP Modals service
OpModalService,
],
declarations: [
WorkPackagesListComponent,
@ -388,7 +393,9 @@ import {WpRelationParentComponent} from 'core-components/wp-relations/wp-relatio
WorkPackageInlineCreateComponent,
// Embedded table
WorkPackageEmbeddedTableComponent
WorkPackageEmbeddedTableComponent,
// Modals
ColumnsModalComponent,
],
entryComponents: [
WorkPackagesListComponent,
@ -433,7 +440,10 @@ import {WpRelationParentComponent} from 'core-components/wp-relations/wp-relatio
WorkPackageEmbeddedTableComponent,
// Relations tab (ng1 -> ng2)
WorkPackageRelationsHierarchyComponent
WorkPackageRelationsHierarchyComponent,
// Modals
ColumnsModalComponent,
]
})
export class OpenProjectModule {

@ -57,6 +57,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');

@ -0,0 +1,61 @@
<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"
(click)="closeMe()"
[attr.title]="text.closePopup">
</i>
</a>
</div>
<h3 [textContent]="text.columnsLabel"></h3>
<div class="columns-modal-content select2-modal-content"
*ngIf="!impaired">
</div>
<div
class="columns-modal-content select2-modal-content"
*ngIf="impaired">
<label
for="selected_columns"
[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>
<div>
<button class="button -highlight"
[textContent]="text.applyButton"
(click)="updateSelectedColumns()">
</button>
<button class="button"
[textContent]="text.cancelButton"
(click)="closeMe()">
</button>
</div>
</div>
</div>

@ -0,0 +1,120 @@
import {Component, ElementRef, Inject, OnInit} from '@angular/core';
import {I18nToken, OpModalLocalsToken} from 'core-app/angular4-transition-utils';
import {OpModalLocalsMap} from 'core-components/op-modals/op-modal.types';
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 {OpModalComponent} from 'core-components/op-modals/op-modal.component';
@Component({
template: require('!!raw-loader!./columns-modal.component.html')
})
export class ColumnsModalComponent extends OpModalComponent {
/* Close on escape? */
public closeOnEscape = false;
/* Close on outside click */
public closeOnOutsideClick = false;
public $element:JQuery;
public text = {
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 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;
// //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;
// });
constructor(@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
@Inject(I18nToken) readonly I18n:op.I18n,
readonly wpTableColumns:WorkPackageTableColumnsService,
readonly ConfigurationService:ConfigurationService,
readonly elementRef:ElementRef) {
super(locals, elementRef);
}
ngOnInit() {
this.$element = jQuery(this.elementRef.nativeElement);
this.impaired = true; // TODO
this.eeShowBanners = angular.element('body').hasClass('ee-banners-visible');
if (this.impaired) {
this.selectedColumns.forEach((column:QueryColumn) => {
this.selectedColumnMap[column.id] = true;
});
}
}
public updateSelectedColumns() {
this.wpTableColumns.setColumns(this.selectedColumns);
this.service.close();
}
/**
* 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.
*/
public updateUnusedColumns(selectedColumns:QueryColumn[]) {
this.unusedColumns = _.differenceBy(this.availableColumns, selectedColumns, '$href');
}
public setSelectedColumn(column:QueryColumn) {
if (this.selectedColumnMap[column.id]) {
this.selectedColumns.push(column);
}
else {
_.remove(this.selectedColumns, (c:QueryColumn) => c.id === column.id);
}
}
/**
* 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) {
}
protected get afterFocusOn():JQuery {
return this.$element;
}
}

@ -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,110 @@
import {
ApplicationRef,
ComponentFactoryResolver, ComponentRef,
Inject,
Injectable,
Injector
} from '@angular/core';
import {ComponentPortal, 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';
@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
Mousetrap.bind('escape', () => {
if (this.active && this.active.closeOnEscape) {
this.close();
}
});
// 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(modal:any, locals:any = {}) {
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>;
this.active = ref.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;
};

@ -49,6 +49,8 @@ import {
WorkPackageTableConfigurationObject
} from 'core-app/components/wp-table/wp-table-configuration';
import {QueryColumn} from 'core-components/wp-query/query-column';
import {OpModalService} from 'core-components/op-modals/op-modal.service';
@Component({
template: require('!!raw-loader!./wp-table.directive.html'),
@ -94,7 +96,7 @@ export class WorkPackagesTableController implements OnInit, OnDestroy {
public injector:Injector,
private states:States,
readonly tableState:TableState,
@Inject(columnsModalToken) private columnsModal:any,
readonly opModalService:OpModalService,
private opContextMenu:OPContextMenuService,
@Inject(I18nToken) private I18n:op.I18n,
private wpTableGroupBy:WorkPackageTableGroupByService,
@ -183,7 +185,7 @@ export class WorkPackagesTableController implements OnInit, OnDestroy {
public openColumnsModal() {
this.opContextMenu.close();
this.columnsModal.activate();
this.opModalService.show(ColumnsModalComponent);
}
private getTableAndTimelineElement():[HTMLElement, HTMLElement] {

Loading…
Cancel
Save