Skip to content

Commit

Permalink
Update mobxpromise and fix circular dependency caused by onResult (#4836
Browse files Browse the repository at this point in the history
)

* Update mobxpromise and fix circular dependency caused by onResult
* move mobxpromise into cbioportal-frontend-commons package
* Await study tags to avoid duplicate rendering of cancer study selector
* Add e2e test for gene input selection after modify query



---------

Co-authored-by: Onur Sumer <s.onur.sumer@gmail.com>
  • Loading branch information
alisman and onursumer authored Jan 29, 2024
1 parent 51be5d0 commit f593e6b
Show file tree
Hide file tree
Showing 74 changed files with 560 additions and 216 deletions.
18 changes: 18 additions & 0 deletions end-to-end-test/remote/specs/core/results.logic.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,24 @@ describe('case set selection in modify query form', function() {
});
});

describe('gene list input', function() {
beforeEach(function() {
var url = `${CBIOPORTAL_URL}/index.do?cancer_study_id=coadread_tcga_pub&Z_SCORE_THRESHOLD=2&RPPA_SCORE_THRESHOLD=2&data_priority=0&case_set_id=coadread_tcga_pub_rppa&gene_list=KRAS%2520NRAS%2520BRAF&geneset_list=+&tab_index=tab_visualize&Action=Submit&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=coadread_tcga_pub_mutations&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=coadread_tcga_pub_gistic`;
goToUrlAndSetLocalStorage(url);
$('#modifyQueryBtn').waitForExist({ timeout: 60000 });
});

// we're testing this because it was broken
it('allows gene textarea update', () => {
$('#modifyQueryBtn').click();
const textarea = getElementByTestHandle('geneSet');
textarea.waitForDisplayed();
textarea.setValue('TP53 BRAF');

assert(textarea.getValue() === 'TP53 BRAF');
});
});

describe('genetic profile selection in modify query form', function() {
beforeEach(function() {
var url = `${CBIOPORTAL_URL}/index.do?cancer_study_id=chol_tcga&Z_SCORE_THRESHOLD=2.0&RPPA_SCORE_THRESHOLD=2.0&data_priority=0&case_set_id=chol_tcga_all&gene_list=EGFR&geneset_list=+&tab_index=tab_visualize&Action=Submit&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=chol_tcga_mutations&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=chol_tcga_gistic&genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION=chol_tcga_rppa_Zscores`;
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,6 @@
"mobx-react-lite": "3.0.1",
"mobx-react-router": "4.1.0",
"mobx-utils": "6.0.1",
"mobxpromise": "github:cbioportal/mobxpromise#c3429672eb39be54e54ce14a8636e8d843729db3",
"numeral": "^2.0.6",
"object-sizeof": "^1.2.0",
"oncokb-frontend-commons": "^0.0.21",
Expand Down
1 change: 0 additions & 1 deletion packages/cbioportal-frontend-commons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
"jquery": "^3.2.1",
"lodash": "^4.17.15",
"measure-text": "0.0.4",
"mobxpromise": "github:cbioportal/mobxpromise#303db72588860bff0a6862a4f07a4e8a3578c94f",
"numeral": "^2.0.6",
"object-sizeof": "^1.2.0",
"oncokb-ts-api-client": "^1.3.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/cbioportal-frontend-commons/src/api/remoteData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
MobxPromise,
MobxPromiseFactory,
MobxPromiseInputUnion,
hasObservers,
} from 'mobxpromise';
} from '../lib/MobxPromise';
import { hasObservers } from '../lib/mobxPromiseUtils';

type errorHandler = (error: Error) => void;

Expand Down
2 changes: 2 additions & 0 deletions packages/cbioportal-frontend-commons/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export * from './lib/TextTruncationUtils';
export * from './lib/urls';
export * from './lib/webdriverUtils';
export * from './lib/TickUtils';
export * from './lib/MobxPromise';
export * from './lib/mobxPromiseUtils';

export { default as CBIOPORTAL_VICTORY_THEME } from './theme/cBioPortalTheme';
export * from './theme/cBioPortalTheme';
Expand Down
305 changes: 305 additions & 0 deletions packages/cbioportal-frontend-commons/src/lib/MobxPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import { observable, action, computed, makeObservable } from 'mobx';

/**
* This tagged union type describes the interoperability of MobxPromise properties.
*/
type MobxPromiseStatus = 'pending' | 'error' | 'complete';
export type MobxPromiseUnionType<R> = (
| {
status: 'pending';
isPending: true;
isError: false;
isComplete: false;
result: R | undefined;
error: Error | undefined;
}
| {
status: 'error';
isPending: false;
isError: true;
isComplete: false;
result: R | undefined;
error: Error;
}
| {
status: 'complete';
isPending: false;
isError: false;
isComplete: true;
result: R;
error: Error | undefined;
}
) & { peekStatus: MobxPromiseStatus };
export type MobxPromiseUnionTypeWithDefault<R> = (
| {
status: 'pending';
isPending: true;
isError: false;
isComplete: false;
result: R;
error: Error | undefined;
}
| {
status: 'error';
isPending: false;
isError: true;
isComplete: false;
result: R;
error: Error;
}
| {
status: 'complete';
isPending: false;
isError: false;
isComplete: true;
result: R;
error: Error | undefined;
}
) & { peekStatus: MobxPromiseStatus };

export type MobxPromiseInputUnion<R> =
| PromiseLike<R>
| (() => PromiseLike<R>)
| MobxPromiseInputParams<R>;
export type MobxPromiseInputParams<R> = {
/**
* A function that returns a list of MobxPromise objects which are dependencies of the invoke function.
*/
await?: MobxPromise_await;

/**
* A function that returns the async result or a promise for the async result.
*/
invoke: MobxPromise_invoke<R>;

/**
* Default result in place of undefined
*/
default?: R;

/**
* A function that will be called when the latest promise from invoke() is resolved.
* It will not be called for out-of-date promises.
*/
onResult?: (result?: R) => void;

/**
* A function that will be called when the latest promise from invoke() is rejected.
* It will not be called for out-of-date promises.
*/
onError?: (error: Error) => void;
};
export type MobxPromise_await = () => Array<
| MobxPromiseUnionTypeWithDefault<any>
| MobxPromiseUnionType<any>
| MobxPromise<any>
>;
export type MobxPromise_invoke<R> = () => PromiseLike<R>;
export type MobxPromiseInputParamsWithDefault<R> = {
await?: MobxPromise_await;
invoke: MobxPromise_invoke<R>;
default: R;
onResult?: (result: R) => void;
onError?: (error: Error) => void;
};

/**
* MobxPromise provides an observable interface for a computed promise.
* @author adufilie http://github.com/adufilie
*/
export class MobxPromiseImpl<R> {
static isPromiseLike(value: any) {
return (
value != null &&
typeof value === 'object' &&
typeof value.then === 'function'
);
}

static normalizeInput<R>(
input: MobxPromiseInputParamsWithDefault<R>
): MobxPromiseInputParamsWithDefault<R>;
static normalizeInput<R>(
input: MobxPromiseInputUnion<R>,
defaultResult?: R
): MobxPromiseInputParamsWithDefault<R>;
static normalizeInput<R>(
input: MobxPromiseInputUnion<R>
): MobxPromiseInputParams<R>;
static normalizeInput<R>(
input: MobxPromiseInputUnion<R>,
defaultResult?: R
) {
if (typeof input === 'function')
return { invoke: input, default: defaultResult };

if (MobxPromiseImpl.isPromiseLike(input))
return {
invoke: () => input as PromiseLike<R>,
default: defaultResult,
};

input = input as MobxPromiseInputParams<R>;
if (defaultResult !== undefined)
input = { ...input, default: defaultResult };
return input;
}

constructor(input: MobxPromiseInputUnion<R>, defaultResult?: R) {
makeObservable<MobxPromiseImpl<R>>(this);

let norm = MobxPromiseImpl.normalizeInput(input, defaultResult);
this.await = norm.await;
this.invoke = norm.invoke;
this.defaultResult = norm.default;
this.onResult = norm.onResult;
this.onError = norm.onError;
}

private await?: MobxPromise_await;
private invoke: MobxPromise_invoke<R>;
private onResult?: (result?: R) => void;
private onError?: (error: Error) => void;
private defaultResult?: R;
private invokeId: number = 0;
private _latestInvokeId: number = 0;

@observable private internalStatus: 'pending' | 'complete' | 'error' =
'pending';
@observable.ref private internalResult?: R = undefined;
@observable.ref private internalError?: Error = undefined;

@computed get status(): 'pending' | 'complete' | 'error' {
// wait until all MobxPromise dependencies are complete
if (this.await)
for (let status of this.await().map(mp => mp.status)) // track all statuses before returning
if (status !== 'complete') return status;

let status = this.internalStatus; // force mobx to track changes to internalStatus
if (this.latestInvokeId != this.invokeId) status = 'pending';
return status;
}

@computed get peekStatus(): 'pending' | 'complete' | 'error' {
// check status without triggering invoke

// check status of all MobxPromise dependencies
if (this.await)
for (let status of this.await().map(mp => mp.peekStatus))
if (status !== 'complete') return status;

// otherwise, return internal status
return this.internalStatus;
}

@computed get isPending() {
return this.status == 'pending';
}

@computed get isComplete() {
return this.status == 'complete';
}

@computed get isError() {
return this.status == 'error';
}

@computed get result(): R | undefined {
// checking status may trigger invoke
if (this.isError || this.internalResult == null)
return this.defaultResult;

return this.internalResult;
}

@computed get error(): Error | undefined {
// checking status may trigger invoke
if (!this.isComplete && this.await)
for (let error of this.await().map(mp => mp.error)) // track all errors before returning
if (error) return error;

return this.internalError;
}

/**
* This lets mobx determine when to call this.invoke(),
* taking advantage of caching based on observable property access tracking.
*/
@computed
private get latestInvokeId() {
window.clearTimeout(this._latestInvokeId);
let promise = this.invoke();
let invokeId: number = window.setTimeout(() =>
this.setPending(invokeId, promise)
);
return (this._latestInvokeId = invokeId);
}

@action
private setPending(invokeId: number, promise: PromiseLike<R>) {
this.invokeId = invokeId;
promise.then(
result => this.setComplete(invokeId, result),
error => this.setError(invokeId, error)
);
this.internalStatus = 'pending';
}

@action
private setComplete(invokeId: number, result: R) {
if (invokeId === this.invokeId) {
this.internalResult = result;
this.internalError = undefined;
this.internalStatus = 'complete';

if (this.onResult) this.onResult(result || this.defaultResult); // may use defaultResult
}
}

@action
private setError(invokeId: number, error: Error) {
if (invokeId === this.invokeId) {
this.internalError = error;
this.internalResult = undefined;
this.internalStatus = 'error';

if (this.onError) this.onError(error);
}
}
}

export type MobxPromiseFactory = {
// This provides more information for TypeScript code flow analysis
<R>(
input: MobxPromiseInputParamsWithDefault<R>
): MobxPromiseUnionTypeWithDefault<R>;
<R>(
input: MobxPromiseInputUnion<R>,
defaultResult: R
): MobxPromiseUnionTypeWithDefault<R>;
<R>(input: MobxPromiseInputUnion<R>): MobxPromiseUnionType<R>;
};

export const MobxPromise = MobxPromiseImpl as {
// This provides more information for TypeScript code flow analysis
new <R>(
input: MobxPromiseInputParamsWithDefault<R>
): MobxPromiseUnionTypeWithDefault<R>;
new <R>(
input: MobxPromiseInputUnion<R>,
defaultResult: R
): MobxPromiseUnionTypeWithDefault<R>;
new <R>(input: MobxPromiseInputUnion<R>): MobxPromiseUnionType<R>;
};

export interface MobxPromise<T>
extends Pick<
MobxPromiseImpl<T>,
| 'status'
| 'error'
| 'result'
| 'isPending'
| 'isError'
| 'isComplete'
| 'peekStatus'
> {}
Loading

0 comments on commit f593e6b

Please sign in to comment.