Merge pull request #4769 from romanroe/reactive_fassade

Reactive Fassade
pull/4900/head
ulferts 8 years ago committed by GitHub
commit c517f25803
  1. 2
      frontend/app/components/routing/wp-list/wp-list.controller.test.ts
  2. 2
      frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts
  3. 28
      frontend/app/components/states.service.ts
  4. 29
      frontend/app/components/work-packages/work-package-cache.service.test.ts
  5. 45
      frontend/app/components/work-packages/work-package-cache.service.ts
  6. 1
      frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.test.ts
  7. 3
      frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.ts
  8. 2
      frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.ts
  9. 3
      frontend/app/components/work-packages/wp-subject/wp-subject.directive.ts
  10. 3
      frontend/app/components/work-packages/wp-watcher-button/wp-watcher-button.directive.ts
  11. 3
      frontend/app/components/wp-copy/wp-copy.controller.ts
  12. 3
      frontend/app/components/wp-create/wp-create.controller.ts
  13. 3
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.directive.ts
  14. 41
      frontend/app/components/wp-edit/wp-edit-form.directive.ts
  15. 3
      frontend/app/components/wp-panels/activity-panel/activity-panel.directive.ts
  16. 1
      frontend/app/components/wp-panels/relations-panel/relations-panel.directive.ts
  17. 3
      frontend/app/components/wp-panels/watchers-panel/watchers-panel.controller.ts
  18. 188
      frontend/app/helpers/reactive-fassade.ts
  19. 23
      frontend/npm-debug.log.36f01dc452d79c12d9a4173c67fbf367

@ -45,6 +45,7 @@ describe('WorkPackagesListController', () => {
beforeEach(angular.mock.module('openproject.api', 'openproject.workPackages.controllers',
'openproject.workPackages.services', 'ng-context-menu', 'btford.modal', 'openproject.layout',
'openproject.services', 'openproject.wpButtons'));
beforeEach(angular.mock.module('openproject.templates', ($provide) => {
var configurationService = {
isTimezoneSet: sinon.stub().returns(false)
@ -53,6 +54,7 @@ describe('WorkPackagesListController', () => {
$provide.constant('$stateParams', stateParams);
$provide.constant('ConfigurationService', configurationService);
}));
beforeEach(angular.mock.inject(($rootScope, $controller, $timeout, $q, $cacheFactory) => {
scope = $rootScope.$new();
win = {

@ -72,7 +72,7 @@ export class WorkPackageViewController {
* Needs to be run explicitly by descendants.
*/
protected observeWorkPackage() {
scopedObservable(this.$scope, this.wpCacheService.loadWorkPackage(this.workPackageId))
this.wpCacheService.loadWorkPackage(this.workPackageId).observe(this.$scope)
.subscribe((wp:WorkPackageResourceInterface) => {
this.workPackage = wp;
this.init();

@ -0,0 +1,28 @@
import {MultiState, initStates} from "../helpers/reactive-fassade";
import {WorkPackageResource} from "./api/api-v3/hal-resources/work-package-resource.service";
import {opServicesModule} from "../angular-modules";
export class States {
workPackages = new MultiState<WorkPackageResource>();
constructor() {
initStates(this, function (msg: any) {
if (~location.hostname.indexOf("localhost")) {
(console.trace as any)(msg); // RR: stupid hack to avoid compiler error
}
});
}
}
opServicesModule.service('states', States);

@ -53,29 +53,30 @@ describe('WorkPackageCacheService', () => {
wpCacheService.updateWorkPackageList(dummyWorkPackages);
let workPackage: WorkPackageResource;
wpCacheService.loadWorkPackage(1).subscribe(wp => {
wpCacheService.loadWorkPackage(1).observe(null).subscribe(wp => {
workPackage = wp;
});
expect(workPackage.id).to.eq(1);
});
it('should return a work package once the list gets initialized', () => {
let workPackage: WorkPackageResource = null;
wpCacheService.loadWorkPackage(1).subscribe(wp => {
workPackage = wp;
});
expect(workPackage).to.null;
wpCacheService.updateWorkPackageList(dummyWorkPackages);
expect(workPackage.id).to.eq(1);
});
// it('should return a work package once the list gets initialized', () => {
// let workPackage: WorkPackageResource = null;
//
// wpCacheService.loadWorkPackage(1).observe(null).subscribe(wp => {
// workPackage = wp;
// });
//
// expect(workPackage).to.null;
//
// wpCacheService.updateWorkPackageList(dummyWorkPackages);
//
// expect(workPackage.id).to.eq(1);
// });
it('should return/stream a work package every time it gets updated', () => {
let loaded: WorkPackageResource & {dummy: string} = null;
wpCacheService.loadWorkPackage(1).subscribe((wp: any) => {
wpCacheService.loadWorkPackage(1).observe(null).subscribe((wp: any) => {
loaded = wp;
});

@ -30,19 +30,22 @@
import {opWorkPackagesModule} from "../../angular-modules";
import {WorkPackageResource} from "../api/api-v3/hal-resources/work-package-resource.service";
import {ApiWorkPackagesService} from "../api/api-work-packages/api-work-packages.service";
import {State} from "../../helpers/reactive-fassade";
import IScope = angular.IScope;
import {States} from "../states.service";
export class WorkPackageCacheService {
private workPackageCache: {[id: number]: WorkPackageResource} = {};
function getWorkPackageId(id: number|string): string {
return (id || "__new_work_package__").toString();
}
private workPackagesSubject = new Rx.ReplaySubject<{[id: number]: WorkPackageResource}>(1);
export class WorkPackageCacheService {
private newWorkPackageCreatedSubject = new Rx.Subject<WorkPackageResource>();
/*@ngInject*/
constructor(private $rootScope: IScope, private apiWorkPackages: ApiWorkPackagesService) {
constructor(private states: States,
private apiWorkPackages: ApiWorkPackagesService) {
}
newWorkPackageCreated(wp: WorkPackageResource) {
@ -54,27 +57,27 @@ export class WorkPackageCacheService {
}
updateWorkPackageList(list: WorkPackageResource[]) {
for (const wp of list) {
var cached = this.workPackageCache[wp.id];
if (cached && cached.dirty) {
this.workPackageCache[wp.id] = cached;
} else {
this.workPackageCache[wp.id] = wp;
}
for (var wp of list) {
const workPackageId = getWorkPackageId(wp.id);
const wpState = this.states.workPackages.get(workPackageId);
const wpForPublish = wpState.hasValue() && wpState.getCurrentValue().dirty
? wpState.getCurrentValue() // dirty, use current wp
: wp; // not dirty or unknown, use new wp
this.states.workPackages.put(workPackageId, wpForPublish);
}
this.workPackagesSubject.onNext(this.workPackageCache);
}
loadWorkPackage(workPackageId: number, forceUpdate = false): Rx.Observable<WorkPackageResource> {
if (forceUpdate || this.workPackageCache[workPackageId] === undefined) {
this.apiWorkPackages.loadWorkPackageById(workPackageId, forceUpdate).then(wp => {
this.updateWorkPackage(wp);
});
loadWorkPackage(workPackageId: number, forceUpdate = false): State<WorkPackageResource> {
const state = this.states.workPackages.get(getWorkPackageId(workPackageId));
if (forceUpdate) {
state.clear();
}
return this.workPackagesSubject
.map(cache => cache[workPackageId])
.filter(wp => wp !== undefined);
state.putFromPromiseIfPristine(
() => this.apiWorkPackages.loadWorkPackageById(workPackageId, forceUpdate));
return state;
}
onNewWorkPackage(): Rx.Observable<WorkPackageResource> {

@ -26,6 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
describe('wpDisplayAttr directive', () => {
var compile;
var element;

@ -32,7 +32,6 @@ import {WorkPackageEditFieldController} from "../../wp-edit/wp-edit-field/wp-edi
import {WorkPackageCacheService} from "../work-package-cache.service";
import {DisplayField} from "../../wp-display/wp-display-field/wp-display-field.module";
import {WorkPackageDisplayFieldService} from "../../wp-display/wp-display-field/wp-display-field.service";
import {scopedObservable} from "../../../helpers/angular-rx-utils";
import {WorkPackageResource} from "../../api/api-v3/hal-resources/work-package-resource.service";
export class WorkPackageDisplayAttributeController {
@ -118,7 +117,7 @@ function wpDisplayAttrDirective(wpCacheService:WorkPackageCacheService) {
controllers) {
if (!scope.$ctrl.customSchema) {
scopedObservable(scope, wpCacheService.loadWorkPackage(scope.$ctrl.workPackage.id))
wpCacheService.loadWorkPackage(scope.$ctrl.workPackage.id).observe(scope)
.subscribe((wp: WorkPackageResource) => {
scope.$ctrl.updateAttribute(wp);
});

@ -69,7 +69,7 @@ export class WorkPackageSingleViewController {
idLabel: ''
};
scopedObservable($scope, wpCacheService.loadWorkPackage(wpId)).subscribe(wp => this.init(wp));
wpCacheService.loadWorkPackage(wpId).observe($scope).subscribe(wp => this.init(wp));
$scope.$on('workPackageUpdatedInEditor', () => {
this.wpNotificationsService.showSave(this.workPackage);
});

@ -27,7 +27,6 @@
// ++
import {opWorkPackagesModule} from "../../../angular-modules";
import {scopedObservable} from "../../../helpers/angular-rx-utils";
import {WorkPackageResource} from "../../api/api-v3/hal-resources/work-package-resource.service";
export class WorkPackageSubjectController {
@ -38,7 +37,7 @@ export class WorkPackageSubjectController {
protected $stateParams,
protected wpCacheService) {
if (!this.workPackage) {
scopedObservable($scope, wpCacheService.loadWorkPackage($stateParams.workPackageId))
wpCacheService.loadWorkPackage($stateParams.workPackageId).observe($scope)
.subscribe((wp: WorkPackageResource) => {
this.workPackage = wp;
});

@ -30,7 +30,6 @@
import {wpDirectivesModule} from '../../../angular-modules';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {WorkPackageCacheService} from '../work-package-cache.service';
import {scopedObservable} from '../../../helpers/angular-rx-utils';
export class WorkPackageWatcherButtonController {
@ -46,7 +45,7 @@ export class WorkPackageWatcherButtonController {
public I18n,
public wpCacheService:WorkPackageCacheService) {
scopedObservable($scope, wpCacheService.loadWorkPackage(<number> this.workPackage.id))
wpCacheService.loadWorkPackage(<number> this.workPackage.id).observe($scope)
.subscribe((wp: WorkPackageResourceInterface) => {
this.workPackage = wp;
this.setWatchStatus();

@ -28,7 +28,6 @@
import {wpDirectivesModule} from '../../angular-modules';
import {WorkPackageCreateController} from '../wp-create/wp-create.controller';
import {scopedObservable} from '../../helpers/angular-rx-utils';
import {
WorkPackageResource,
WorkPackageResourceInterface
@ -38,7 +37,7 @@ export class WorkPackageCopyController extends WorkPackageCreateController {
protected newWorkPackageFromParams(stateParams) {
var deferred = this.$q.defer();
scopedObservable(this.$scope, this.wpCacheService.loadWorkPackage(stateParams.copiedFromWorkPackageId))
this.wpCacheService.loadWorkPackage(stateParams.copiedFromWorkPackageId).observe(this.$scope)
.subscribe((wp:WorkPackageResourceInterface) => {
this.createCopyFrom(wp).then(newWorkPackage => {
deferred.resolve(newWorkPackage);

@ -30,7 +30,6 @@ import {wpDirectivesModule} from "../../angular-modules";
import {WorkPackageCreateService} from "./wp-create.service";
import {WorkPackageResource} from "../api/api-v3/hal-resources/work-package-resource.service";
import {WorkPackageCacheService} from "../work-packages/work-package-cache.service";
import {scopedObservable} from "../../helpers/angular-rx-utils";
import IRootScopeService = angular.IRootScopeService;
import {WorkPackageEditModeStateService} from "../wp-edit/wp-edit-mode-state.service";
import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service';
@ -66,7 +65,7 @@ export class WorkPackageCreateController {
wpCacheService.updateWorkPackage(wp);
if ($state.params.parent_id) {
scopedObservable($scope, wpCacheService.loadWorkPackage($state.params.parent_id))
wpCacheService.loadWorkPackage($state.params.parent_id).observe($scope)
.subscribe(parent => {
this.parentWorkPackage = parent;
this.newWorkPackage.parent = parent;

@ -29,7 +29,6 @@
import {WorkPackageEditFormController} from "./../wp-edit-form.directive";
import {WorkPackageEditFieldService} from "./wp-edit-field.service";
import {EditField} from "./wp-edit-field.module";
import {scopedObservable} from "../../../helpers/angular-rx-utils";
import {WorkPackageResource} from "../../api/api-v3/hal-resources/work-package-resource.service";
import {WorkPackageCacheService} from "../../work-packages/work-package-cache.service";
@ -334,7 +333,7 @@ function wpEditField(wpCacheService: WorkPackageCacheService) {
controllers[1].formCtrl = formCtrl;
formCtrl.registerField(scope.vm);
scopedObservable(scope, wpCacheService.loadWorkPackage(formCtrl.workPackage.id))
wpCacheService.loadWorkPackage(formCtrl.workPackage.id).observe(scope)
.subscribe((wp: WorkPackageResource) => {
scope.vm.workPackage = wp;
scope.vm.initializeField();

@ -26,44 +26,45 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {ErrorResource} from '../api/api-v3/hal-resources/error-resource.service';
import {WorkPackageEditModeStateService} from './wp-edit-mode-state.service';
import {WorkPackageEditFieldController} from './wp-edit-field/wp-edit-field.directive';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {scopedObservable} from '../../helpers/angular-rx-utils';
import {WorkPackageResource} from '../api/api-v3/hal-resources/work-package-resource.service';
import {ErrorResource} from "../api/api-v3/hal-resources/error-resource.service";
import {WorkPackageEditModeStateService} from "./wp-edit-mode-state.service";
import {WorkPackageEditFieldController} from "./wp-edit-field/wp-edit-field.directive";
import {WorkPackageCacheService} from "../work-packages/work-package-cache.service";
import {WorkPackageResource} from "../api/api-v3/hal-resources/work-package-resource.service";
import {States} from "../states.service";
export class WorkPackageEditFormController {
public workPackage;
public hasEditMode:boolean;
public errorHandler:Function;
public successHandler:Function;
public hasEditMode: boolean;
public errorHandler: Function;
public successHandler: Function;
public fields = {};
private errorsPerAttribute:Object = {};
public firstActiveField:string;
private errorsPerAttribute: Object = {};
public firstActiveField: string;
constructor(protected $scope:ng.IScope,
constructor(protected states: States,
protected $scope: ng.IScope,
protected $q,
protected $rootScope,
protected wpNotificationsService,
protected QueryService,
protected loadingIndicator,
protected wpEditModeState:WorkPackageEditModeStateService,
protected wpCacheService:WorkPackageCacheService) {
protected wpEditModeState: WorkPackageEditModeStateService,
protected wpCacheService: WorkPackageCacheService) {
if (this.hasEditMode) {
wpEditModeState.register(this);
}
scopedObservable($scope, wpCacheService.loadWorkPackage(this.workPackage.id))
states.workPackages.get(this.workPackage.id.toString()).observe($scope)
.subscribe((wp: WorkPackageResource) => {
this.workPackage = wp;
});
}
public isFieldRequired() {
return _.filter((this.fields as any), (name:string) => {
return _.filter((this.fields as any), (name: string) => {
return !this.workPackage[name] && this.workPackage.requiredValueFor(name);
});
}
@ -73,7 +74,7 @@ export class WorkPackageEditFormController {
field.setErrors(this.errorsPerAttribute[field.fieldName] || []);
}
public toggleEditMode(state:boolean) {
public toggleEditMode(state: boolean) {
this.$scope.$evalAsync(() => {
angular.forEach(this.fields, (field) => {
@ -91,7 +92,7 @@ export class WorkPackageEditFormController {
}
public closeAllFields() {
angular.forEach(this.fields, (field:WorkPackageEditFieldController) => {
angular.forEach(this.fields, (field: WorkPackageEditFieldController) => {
field.deactivate();
});
}
@ -149,13 +150,13 @@ export class WorkPackageEditFormController {
return deferred.promise;
}
private handleSubmissionErrors(error:any, deferred:any) {
private handleSubmissionErrors(error: any, deferred: any) {
// Process single API errors
this.handleErroneousAttributes(error);
return deferred.reject();
}
private handleErroneousAttributes(error:any) {
private handleErroneousAttributes(error: any) {
let attributes = error.getInvolvedAttributes();
// Save erroneous fields for when new fields appear
this.errorsPerAttribute = error.getMessagesPerAttribute();

@ -29,7 +29,6 @@
import {wpDirectivesModule} from '../../../angular-modules';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {scopedObservable} from '../../../helpers/angular-rx-utils';
export class ActivityPanelController {
@ -43,7 +42,7 @@ export class ActivityPanelController {
this.reverse = wpActivity.order === 'asc';
scopedObservable($scope, wpCacheService.loadWorkPackage(<number> this.workPackage.id))
wpCacheService.loadWorkPackage(<number> this.workPackage.id).observe($scope)
.subscribe((wp:WorkPackageResourceInterface) => {
this.workPackage = wp;
this.wpActivity.aggregateActivities(this.workPackage).then(activities => {

@ -28,7 +28,6 @@
import {wpDirectivesModule} from "../../../angular-modules";
import {WorkPackageResourceInterface} from "../../api/api-v3/hal-resources/work-package-resource.service";
import {scopedObservable} from "../../../helpers/angular-rx-utils";
import {WorkPackageRelationsService} from "../../wp-relations/wp-relations.service";
export class RelationsPanelController {

@ -27,7 +27,6 @@
// ++
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {scopedObservable} from '../../../helpers/angular-rx-utils';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
export class WatchersPanelController {
@ -64,7 +63,7 @@ export class WatchersPanelController {
return;
}
scopedObservable($scope, wpCacheService.loadWorkPackage(<number> this.workPackage.id))
wpCacheService.loadWorkPackage(<number> this.workPackage.id).observe($scope)
.subscribe((wp:WorkPackageResourceInterface) => {
this.workPackage = wp;
this.fetchWatchers();

@ -0,0 +1,188 @@
import {scopedObservable} from "./angular-rx-utils";
import Observable = Rx.Observable;
import IScope = angular.IScope;
import IPromise = Rx.IPromise;
export abstract class StoreElement {
public pathInStore: string = null;
public logFn: (msg: any) => any = null;
log(msg: string, reason?: string) {
reason = reason === undefined ? "" : " // " + reason;
if (this.pathInStore && this.logFn) {
this.logFn("[" + this.pathInStore + "] " + msg + reason);
}
}
}
interface PromiseLike<T> {
then(successCallback: (value: T) => any, errorCallback: (value: T) => any): any;
}
export class State<T> extends StoreElement {
private timestampOfLastValue = -1;
private timestampOfLastPromise = -1;
private subject = new Rx.BehaviorSubject<T>(null);
private lastValue: T = null;
private cleared = new Rx.Subject();
private observable: Observable<T>;
constructor() {
super();
this.observable = this.subject.filter(val => val !== null && val !== undefined);
}
/**
* Returns true if this state either has a value of if
* a value is awaited from a promise (via putFromPromise).
*/
public isPristine(): boolean {
return this.timestampOfLastValue === -1 && this.timestampOfLastPromise === -1;
}
public isValueOrPromiseOlderThan(timeoutInMs: number) {
const ageValue = Date.now() - this.timestampOfLastValue;
const agePromise = Date.now() - this.timestampOfLastPromise;
return ageValue > timeoutInMs && agePromise > timeoutInMs;
}
public hasValue(): boolean {
return this.lastValue !== null && this.lastValue !== undefined;
}
/**
* Returns the current value or 'null', if no value is present.
* Therefore, calls to this method should always be guarded by State#hasValue().
*
* However, it is usually better to use State#get()/State#observe().
*/
public getCurrentValue(): T {
return this.lastValue;
}
public clear(reason?: string): this {
this.log("State#clear()", reason);
this.setState(null);
return this;
}
public put(value: T, reason?: string): this {
this.log("State#put(...)", reason);
this.setState(value);
return this;
}
public putFromPromise(promise: PromiseLike<T>): this {
this.clear();
this.timestampOfLastPromise = Date.now();
promise.then(
// success
(value: T) => {
this.log("State#putFromPromise(...)");
this.setState(value);
},
// error
() => {
this.log("State#putFromPromise ERROR");
this.timestampOfLastPromise = -1;
}
);
return this;
}
public putFromPromiseIfPristine(calledIfPristine: () => PromiseLike<T>): this {
if (this.isPristine()) {
this.putFromPromise(calledIfPristine());
}
return this;
}
public get(): IPromise<T> {
return this.observable.take(1).toPromise();
}
public observe(scope: IScope): Observable<T> {
return this.scopedObservable(scope);
}
public observeCleared(scope: IScope): Observable<any> {
return scope ? scopedObservable(scope, this.cleared.asObservable()) : this.cleared.asObservable();
}
private setState(val: T) {
this.lastValue = val;
this.subject.onNext(val);
if (val === null || val === undefined) {
this.timestampOfLastValue = -1;
this.timestampOfLastPromise = -1;
this.cleared.onNext(null);
} else {
this.timestampOfLastValue = Date.now();
}
}
private scopedObservable(scope: IScope): Observable<T> {
return scope ? scopedObservable(scope, this.observable) : this.observable;
}
}
export class MultiState<T> extends StoreElement {
private states: {[id: string]: State<T>} = {};
constructor() {
super();
}
clearAll() {
this.states = {};
}
put(id: string, value: T): State<T> {
this.log("MultiState#put(" + id + ")");
const state = this.get(id);
state.put(value);
return state;
}
get(id: string): State<T> {
if (this.states[id] === undefined) {
this.states[id] = new State<T>();
}
return this.states[id];
}
}
function traverse(elem: any, path: string, logFn: (msg: any) => any) {
for (const key in elem) {
if (!elem.hasOwnProperty(key)) {
continue;
}
const value = elem[key];
let location = path.length > 0 ? path + "." + key : key;
if (value instanceof StoreElement) {
value.pathInStore = location;
value.logFn = logFn;
} else {
traverse(value, location, logFn);
}
}
}
export function initStates(states: any, logFn?: (msg: any) => any) {
return traverse(states, "", logFn);
}

@ -1,23 +0,0 @@
0 info it worked if it ends with ok
1 verbose cli [ '/home/roman/.nodenv/versions/4.2.1/bin/node',
1 verbose cli '/home/roman/.nodenv/versions/4.2.1/bin/npm',
1 verbose cli 'bin' ]
2 info using npm@2.14.7
3 info using node@v4.2.1
4 verbose exit [ 0, true ]
5 verbose stack Error: write EPIPE
5 verbose stack at Object.exports._errnoException (util.js:874:11)
5 verbose stack at exports._exceptionWithHostPort (util.js:897:20)
5 verbose stack at WriteWrap.afterWrite (net.js:763:14)
6 verbose cwd /home/roman/prjs/openproject/repo/frontend
7 error Linux 3.13.0-24-generic
8 error argv "/home/roman/.nodenv/versions/4.2.1/bin/node" "/home/roman/.nodenv/versions/4.2.1/bin/npm" "bin"
9 error node v4.2.1
10 error npm v2.14.7
11 error code EPIPE
12 error errno EPIPE
13 error syscall write
14 error write EPIPE
15 error If you need help, you may report this error at:
15 error <https://github.com/npm/npm/issues>
16 verbose exit [ 1, true ]
Loading…
Cancel
Save