Create external query configuration component and service

pull/6272/head
Oliver Günther 7 years ago
parent d3f6539223
commit d760932490
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 12
      frontend/app/angular4-modules.ts
  2. 10
      frontend/app/components/op-modals/op-modal.service.ts
  3. 10
      frontend/app/components/wp-list/wp-states-initialization.service.ts
  4. 4
      frontend/app/components/wp-query/url-params-helper.ts
  5. 3
      frontend/app/components/wp-table/configuration-modal/wp-table-configuration.modal.html
  6. 34
      frontend/app/components/wp-table/configuration-modal/wp-table-configuration.modal.ts
  7. 37
      frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts
  8. 7
      frontend/app/components/wp-table/embedded/wp-embedded-table.html
  9. 41
      frontend/app/components/wp-table/external-configuration/external-query-configuration.component.ts
  10. 107
      frontend/app/components/wp-table/external-configuration/external-query-configuration.service.ts
  11. 15
      frontend/app/components/wp-table/wp-table-configuration.ts
  12. 6
      frontend/app/init-app.ts

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {NgModule} from '@angular/core';
import {APP_INITIALIZER, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {UpgradeModule} from '@angular/upgrade/static';
import {FormsModule} from '@angular/forms';
@ -213,6 +213,8 @@ import {NotificationsService} from 'core-components/common/notifications/notific
import {NotificationComponent} from 'core-components/common/notifications/notification.component';
import {NotificationsContainerComponent} from 'core-components/common/notifications/notifications-container.component';
import {UploadProgressComponent} from 'core-components/common/notifications/upload-progress.component';
import {ExternalQueryConfigurationComponent} from 'core-components/wp-table/external-configuration/external-query-configuration.component';
import {ExternalQueryConfigurationService} from 'core-components/wp-table/external-configuration/external-query-configuration.service';
@NgModule({
imports: [
@ -309,6 +311,8 @@ import {UploadProgressComponent} from 'core-components/common/notifications/uplo
WpTableConfigurationService,
AttributeHelpTextsService,
// External query configuration
ExternalQueryConfigurationService,
],
declarations: [
WorkPackagesListComponent,
@ -443,6 +447,9 @@ import {UploadProgressComponent} from 'core-components/common/notifications/uplo
NotificationsContainerComponent,
NotificationComponent,
UploadProgressComponent,
// External query configuration
ExternalQueryConfigurationComponent,
],
entryComponents: [
WorkPackagesListComponent,
@ -504,6 +511,9 @@ import {UploadProgressComponent} from 'core-components/common/notifications/uplo
// Entries for ng1 downgraded components
AttributeHelpTextComponent,
// External query configuration
ExternalQueryConfigurationComponent,
]
})
export class OpenProjectModule {

@ -59,11 +59,11 @@ export class OpModalService {
/**
* Open a Modal reference and append it to the portal
*/
public show<T extends OpModalComponent>(modal:ComponentType<T>, locals:any = {}):void {
public show<T extends OpModalComponent>(modal:ComponentType<T>, locals:any = {}, injector:Injector = this.injector):T {
this.close();
// Create a portal for the given component class and render it
const portal = new ComponentPortal(modal, null, this.injectorFor(locals));
const portal = new ComponentPortal(modal, null, this.injectorFor(injector, locals));
const ref:ComponentRef<OpModalComponent> = this.bodyPortalHost.attach(portal) as ComponentRef<OpModalComponent>;
const instance = ref.instance as T;
this.active = instance;
@ -73,6 +73,8 @@ export class OpModalService {
// Focus on the first element
this.active && this.active.onOpen(this.activeModal);
});
return this.active as T;
}
public isActive(modal:OpModalComponent) {
@ -106,7 +108,7 @@ export class OpModalService {
* This allows callers to pass data into the newly created modal.
*
*/
private injectorFor(data:any) {
private injectorFor(injector:Injector, 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
@ -114,6 +116,6 @@ export class OpModalService {
injectorTokens.set(OpModalLocalsToken, data);
return new PortalInjector(this.injector, injectorTokens);
return new PortalInjector(injector, injectorTokens);
}
}

@ -127,6 +127,16 @@ export class WorkPackageStatesInitializationService {
this.authorisationService.initModelAuth('query', query.$links);
}
public applyToQuery(query:QueryResource) {
this.wpTableFilters.applyToQuery(query);
this.wpTableSum.applyToQuery(query);
this.wpTableColumns.applyToQuery(query);
this.wpTableSortBy.applyToQuery(query);
this.wpTableGroupBy.applyToQuery(query);
this.wpTableTimeline.applyToQuery(query);
this.wpTableHierarchies.applyToQuery(query);
}
public clearStates() {
const reason = 'Clearing states before re-initialization.';

@ -64,7 +64,7 @@ export class UrlParamsHelperService {
return parts.join('&');
}
public encodeQueryJsonParams(query:QueryResource, additional:any) {
public encodeQueryJsonParams(query:QueryResource, additional:any = {}) {
var paramsData:any = {
c: query.columns.map(function (column) {
return column.id;
@ -188,7 +188,7 @@ export class UrlParamsHelperService {
return queryData;
}
public buildV3GetQueryFromQueryResource(query:QueryResource, additionalParams:any) {
public buildV3GetQueryFromQueryResource(query:QueryResource, additionalParams:any = {}) {
var queryData:any = {};
queryData["columns[]"] = this.buildV3GetColumnsFromQueryResource(query);

@ -13,7 +13,8 @@
<h3 [textContent]="text.title"></h3>
<div class="tabs--container">
<div *ngIf="!!tabPortalHost"
class="tabs--container">
<ul>
<li *ngFor="let tab of availableTabs">
<a class="tab-show"

@ -2,7 +2,7 @@ import {
ApplicationRef,
Component,
ComponentFactoryResolver,
ElementRef,
ElementRef, EventEmitter,
Inject,
Injector,
OnDestroy,
@ -20,6 +20,11 @@ import {
TabComponent,
TabPortalOutlet
} from 'core-components/wp-table/configuration-modal/tab-portal-outlet';
import {QueryFormDmService} from 'core-app/modules/hal/dm-services/query-form-dm.service';
import {WorkPackageStatesInitializationService} from 'core-components/wp-list/wp-states-initialization.service';
import {TableState} from 'core-components/wp-table/table-state/table-state';
import {QueryFormResource} from 'core-app/modules/hal/resources/query-form-resource';
import {LoadingIndicatorService} from 'core-components/common/loading-indicator/loading-indicator.service';
@Component({
template: require('!!raw-loader!./wp-table-configuration.modal.html')
@ -48,6 +53,7 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme
upsaleRelationColumnsLink: this.I18n.t('js.modals.upsale_relation_columns_link')
};
public onDataUpdated = new EventEmitter<void>();
public impaired = this.ConfigurationService.accessibilityModeEnabled();
public selectedColumnMap:{ [id:string]:boolean } = {};
@ -62,6 +68,10 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme
readonly injector:Injector,
readonly appRef:ApplicationRef,
readonly componentFactoryResolver:ComponentFactoryResolver,
readonly loadingIndicator:LoadingIndicatorService,
readonly tableState:TableState,
readonly queryFormDm:QueryFormDmService,
readonly wpStatesInitialization:WorkPackageStatesInitializationService,
readonly wpTableColumns:WorkPackageTableColumnsService,
readonly ConfigurationService:ConfigurationService,
readonly elementRef:ElementRef) {
@ -79,15 +89,15 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme
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);
});
this.loadingIndicator.indicator('modal').promise = this.loadForm()
.then(() => {
const initialTab = this.locals['initialTab'] || this.availableTabs[0].name;
this.switchTo(initialTab);
});
}
ngOnDestroy() {
this.onDataUpdated.complete();
this.tabPortalHost.dispose();
}
@ -108,6 +118,7 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme
component.onSave();
});
this.onDataUpdated.emit();
this.service.close();
}
@ -130,4 +141,13 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme
protected get afterFocusOn():JQuery {
return this.$element;
}
protected loadForm() {
const query = this.tableState.query.value!;
return this.queryFormDm.load(query).then((form:QueryFormResource) => {
this.wpStatesInitialization.updateStatesFromForm(query, form);
return form;
});
}
}

@ -1,4 +1,4 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {Component, Injector, Input, OnDestroy, OnInit} from '@angular/core';
import {CurrentProjectService} from '../../projects/current-project.service';
import {TableState} from '../table-state/table-state';
import {WorkPackageStatesInitializationService} from '../../wp-list/wp-states-initialization.service';
@ -25,6 +25,9 @@ import {WorkPackageTableSelection} from 'core-components/wp-fast-table/state/wp-
import {QueryResource} from 'core-app/modules/hal/resources/query-resource';
import {QueryDmService} from 'core-app/modules/hal/dm-services/query-dm.service';
import {WorkPackageCollectionResource} from 'core-app/modules/hal/resources/wp-collection-resource';
import {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper';
import {WpTableConfigurationModalComponent} from 'core-components/wp-table/configuration-modal/wp-table-configuration.modal';
import {OpModalService} from 'core-components/op-modals/op-modal.service';
@Component({
selector: 'wp-embedded-table',
@ -52,7 +55,7 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, OnDestroy {
@Input('queryProps') public queryProps:any = {};
@Input() public configuration:WorkPackageTableConfigurationObject;
@Input() public uniqueEmbeddedTableName:string = `embedded-table-${Date.now()}`;
@Input() public tableActions?:OpTableActionFactory[];
@Input() public tableActions:OpTableActionFactory[] = [];
@Input() public compactTableStyle:boolean = false;
private query:QueryResource;
@ -61,6 +64,9 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, OnDestroy {
constructor(readonly QueryDm:QueryDmService,
readonly tableState:TableState,
readonly injector:Injector,
readonly opModalService:OpModalService,
readonly urlParamsHelper:UrlParamsHelperService,
readonly loadingIndicatorService:LoadingIndicatorService,
readonly tableActionsService:OpTableActionsService,
readonly wpTablePagination:WorkPackageTablePaginationService,
@ -89,22 +95,41 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, OnDestroy {
).subscribe(([pagination, query]) => {
this.QueryDm.loadResults(query, this.wpTablePagination.paginationObject)
.then((results) => this.initializeStates(query, results));
});
});
}
ngOnDestroy():void {
}
public openConfigurationModal(onUpdated:() => void) {
this.tableState.query
.valuesPromise()
.then(() => {
const modal = this.opModalService
.show<WpTableConfigurationModalComponent>(WpTableConfigurationModalComponent, {}, this.injector);
// Detach this component when the modal closes and pass along the query data
modal.onDataUpdated.subscribe(onUpdated);
});
}
get projectIdentifier() {
let identifier:string|null = null;
if (this.configuration['projectContext']) {
if (this.configuration.projectContext) {
identifier = this.currentProject.identifier;
}
return identifier || undefined;
}
public buildQueryProps() {
const query = this.tableState.query.value!;
this.wpStatesInitialization.applyToQuery(query);
return this.urlParamsHelper.buildV3GetQueryFromQueryResource(query);
}
private initializeStates(query:QueryResource, results:WorkPackageCollectionResource) {
this.tableState.ready.doAndTransition('Query loaded', () => {
this.wpStatesInitialization.clearStates();
@ -114,7 +139,7 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, OnDestroy {
return this.tableState.tableRendering.onQueryUpdated.valuesPromise()
.then(() => {
this.showTablePagination = results.total > results.count;
this.tableInformationLoaded = true;
this.tableInformationLoaded = this.configuration.tableVisible === true;
});
});
}
@ -144,5 +169,5 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, OnDestroy {
// TODO: remove as this should also work by angular2 only
opUiComponentsModule.directive(
'wpEmbeddedTable',
downgradeComponent({component: WorkPackageEmbeddedTableComponent})
downgradeComponent({ component: WorkPackageEmbeddedTableComponent })
);

@ -1,15 +1,14 @@
<div class="work-packages-embedded-view--container loading-indicator--location"
*ngIf="tableInformationLoaded"
[ngClass]="{ '-compact-tables': compactTableStyle }"
[attr.data-indicator-name]="uniqueEmbeddedTableName">
<!-- TABLE + TIMELINE horizontal split -->
<wp-table *ngIf="tableInformationLoaded"
[projectIdentifier]="projectIdentifier"
<wp-table [projectIdentifier]="projectIdentifier"
[configuration]="configuration"
class="work-packages-split-view--tabletimeline-content"></wp-table>
<!-- Footer -->
<div class="work-packages-split-view--tabletimeline-footer hide-when-print"
*ngIf="tableInformationLoaded">
<div class="work-packages-split-view--tabletimeline-footer hide-when-print">
<wp-table-pagination *ngIf="showTablePagination"></wp-table-pagination>
</div>
</div>

@ -0,0 +1,41 @@
import {AfterViewInit, Component, Inject, ViewChild} from '@angular/core';
import {WorkPackageEmbeddedTableComponent} from 'core-components/wp-table/embedded/wp-embedded-table.component';
import {
ExternalQueryConfigurationService,
OpQueryConfigurationLocals
} from 'core-components/wp-table/external-configuration/external-query-configuration.service';
interface QueryConfigurationLocals {
service:ExternalQueryConfigurationService;
currentQuery:any;
originator:JQuery;
}
@Component({
template: `
<wp-embedded-table #embeddedTableForConfiguration
[queryProps]="locals.currentQuery || {}"
[configuration]="{ tableVisible: false }">
</wp-embedded-table>`
})
export class ExternalQueryConfigurationComponent implements AfterViewInit {
@ViewChild('embeddedTableForConfiguration') private embeddedTable:WorkPackageEmbeddedTableComponent;
constructor(@Inject(OpQueryConfigurationLocals) readonly locals:QueryConfigurationLocals) {
}
ngAfterViewInit() {
// Open the configuration modal in an asynchronous step
// to avoid nesting components in the view initialization.
setTimeout(() => {
this.embeddedTable.openConfigurationModal(() => {
this.service.close(this.locals.originator, this.embeddedTable.buildQueryProps());
});
});
}
public get service():ExternalQueryConfigurationService {
return this.locals.service;
}
}

@ -0,0 +1,107 @@
import {ApplicationRef, ComponentFactoryResolver, Inject, Injectable, InjectionToken, Injector} from '@angular/core';
import {ComponentPortal, DomPortalOutlet, PortalInjector} from '@angular/cdk/portal';
import {TransitionService} from '@uirouter/core';
import {FocusHelperToken} from 'core-app/angular4-transition-utils';
import {OpModalComponent} from 'core-components/op-modals/op-modal.component';
import {ExternalQueryConfigurationComponent} from 'core-components/wp-table/external-configuration/external-query-configuration.component';
import {downgradeInjectable} from '@angular/upgrade/static';
import {opServicesModule} from 'core-app/angular-modules';
export const external_table_trigger_class = 'external-table-configuration--container';
export const OpQueryConfigurationLocals = new InjectionToken<any>('OpQueryConfigurationLocals');
export const OpQueryConfigurationTriggerEvent = 'op:queryconfiguration:trigger';
export const OpQueryConfigurationUpdatedEvent = 'op:queryconfiguration:updated';
@Injectable()
export class ExternalQueryConfigurationService {
// 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) {
}
public setupListener() {
// Listen to keyups on window to close context menus
jQuery(window).on(OpQueryConfigurationTriggerEvent, (event:JQueryEventObject, originator:JQuery, currentQuery:any) => {
this.show(originator, currentQuery);
return false;
});
}
/**
* Create a portal host element to contain the table configuration components.
*/
private get bodyPortalHost() {
if (!this._portalHostElement) {
const hostElement = this._portalHostElement = document.createElement('div');
hostElement.classList.add('op-external-query-configuration--container');
document.body.appendChild(hostElement);
this._bodyPortalHost = new DomPortalOutlet(
hostElement,
this.componentFactoryResolver,
this.appRef,
this.injector
);
}
return this._bodyPortalHost;
}
/**
* Open a Modal reference and append it to the portal
*/
public show<T extends OpModalComponent>(originator:JQuery, currentQuery:any):void {
this.detach();
// Create a portal for the given component class and render it
const portal = new ComponentPortal(
ExternalQueryConfigurationComponent,
null,
this.injectorFor({ originator: originator, currentQuery: currentQuery }));
this.bodyPortalHost.attach(portal);
this._portalHostElement.style.display = 'block';
}
/**
* Closes currently open modal window
*/
public close(originator:JQuery, queryProps:any) {
this.detach();
originator.data('queryProps', queryProps);
originator.trigger(OpQueryConfigurationUpdatedEvent, [queryProps]);
}
private detach() {
// Detach any component currently in the portal
if (this.bodyPortalHost.hasAttached()) {
this.bodyPortalHost.detach();
this._portalHostElement.style.display = 'none';
}
}
/**
* 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(OpQueryConfigurationLocals, data);
return new PortalInjector(this.injector, injectorTokens);
}
}
opServicesModule.service('externalQueryConfiguration', downgradeInjectable(ExternalQueryConfigurationService))

@ -30,13 +30,28 @@
export type WorkPackageTableConfigurationObject = Partial<{ [field in keyof WorkPackageTableConfiguration]:boolean }>;
export class WorkPackageTableConfiguration {
/** Render the table results, set to false when only wanting the table initialization */
public tableVisible:boolean = true;
/** Render the action column (last column) with the actions defined in the TableActionsService */
public actionsColumnEnabled:boolean = true;
/** Whether the work package context menu is enabled*/
public contextMenuEnabled:boolean = true;
/** Whether the column dropdown menu is enabled*/
public columnMenuEnabled:boolean = true;
/** Whether the query should be resolved using the current project identifier */
public projectContext:boolean = true;
/** Whether inline create is enabled*/
public inlineCreateEnabled:boolean = true;
/** Whether the hierarchy toggler item in the subject column is enabled */
public hierarchyToggleEnabled:boolean = true;
/** Whether this table is in an embedded context*/
public isEmbedded:boolean = false;
constructor(private providedConfig:WorkPackageTableConfigurationObject) {

@ -58,6 +58,7 @@ import {openprojectModule} from './angular-modules';
import {whenDebugging} from 'core-app/helpers/debug_output';
import {enableReactiveStatesLogging} from 'reactivestates';
import {TimezoneService} from 'core-components/datetime/timezone.service';
import {ExternalQueryConfigurationService} from 'core-components/wp-table/external-configuration/external-query-configuration.service';
window.appBasePath = jQuery('meta[name=app_base_path]').attr('content') || '';
@ -111,10 +112,12 @@ openprojectModule
.run([
'$rootScope',
'$window',
'externalQueryConfiguration',
'ExpressionService',
'KeyboardShortcutService',
function($rootScope:any,
$window:ng.IWindowService,
externalQueryConfiguration:ExternalQueryConfigurationService,
ExpressionService:ExpressionService,
KeyboardShortcutService:any) {
@ -133,6 +136,9 @@ openprojectModule
'collapsed';
}
// Setup query configuration listener
externalQueryConfiguration.setupListener();
KeyboardShortcutService.activate();
angular.element('body').addClass('__ng-bootstrap-has-run');

Loading…
Cancel
Save