diff --git a/client/src/actions/annotation.js b/client/src/actions/annotation.js index 136fe9b3..1836c983 100644 --- a/client/src/actions/annotation.js +++ b/client/src/actions/annotation.js @@ -387,3 +387,82 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => { }); } }; + +export const saveGenesetsAction = () => async (dispatch, getState) => { + const state = getState(); + const { config, genesets, annotations } = state; + + // bail if gene sets not available, or in readonly mode. + const genesetsAreAvailable = + config?.parameters?.["annotations_genesets"] ?? false; + const genesetsReadonly = + config?.parameters?.["annotations_genesets_readonly"] ?? true; + if (!genesetsAreAvailable || genesetsReadonly) { + // our non-save was completed! + dispatch({ + type: "autosave: genesets complete", + lastSavedGenesets: genesets, + }); + } + + /* + JSON data structure is an array of arrays, where the first + element is gene set name, remainder are the genes. Eg, + + { + "genesets": [ + [ "gs1", ["TNFRSF4","SUMO3","BRWD1"]], + [ "gs2", ["DSCR3", "BRWD1", "BACE2", "SIK1", "C21orf33", "ICOSLG", "SUMO3"]] + ] + } + + Order of gene sets and genes is significant + */ + const gsArr = []; + for (const [gsName, gsGenes] of genesets.genesets) { + gsArr.push([gsName, Array.from(gsGenes)]); + } + + try { + const { dataCollectionNameIsReadOnly, dataCollectionName } = annotations; + const queryString = + !dataCollectionNameIsReadOnly && !!dataCollectionName + ? `?annotation-collection-name=${encodeURIComponent( + dataCollectionName + )}` + : ""; + + const res = await fetch( + `${globals.API.prefix}${globals.API.version}genesets${queryString}`, + { + method: "PUT", + headers: new Headers({ + Accept: "application/json", + "Content-Type": "application/json", + }), + body: JSON.stringify({ + genesets: gsArr, + }), + credentials: "include", + } + ); + if (res.ok) { + dispatch({ + type: "autosave: genesets complete", + lastSavedGenesets: genesets, + }); + } else { + dispatch({ + type: "autosave: genesets error", + message: `HTTP error ${res.status} - ${res.statusText}`, + res, + }); + } + } catch (error) { + dispatch({ + type: "autosave: genesets error", + message: error.toString(), + error, + }); + } +}; diff --git a/client/src/actions/index.js b/client/src/actions/index.js index fd8749a3..e0858de5 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -52,14 +52,21 @@ async function userInfoFetch(dispatch) { }); } -async function genesetsFetch(dispatch) { - return fetchJson("genesets").then((response) => { - const genesets = response?.genesets ?? {}; +async function genesetsFetch(dispatch, config) { + if (config?.parameters?.["annotations_genesets"] ?? false) { + fetchJson("genesets").then((response) => { + const genesets = response?.genesets ?? {}; + dispatch({ + type: "geneset: initial load", + init: genesets, + }); + }); + } else { dispatch({ type: "geneset: initial load", - init: genesets, + init: [], }); - }) + } } function prefetchEmbeddings(annoMatrix) { @@ -84,9 +91,10 @@ const doInitialDataLoad = () => schemaFetch(dispatch), userColorsFetchAndLoad(dispatch), userInfoFetch(dispatch), - genesetsFetch(dispatch), ]); + genesetsFetch(dispatch, config); + const baseDataUrl = `${globals.API.prefix}${globals.API.version}`; const annoMatrix = new AnnoMatrixLoader(baseDataUrl, schema.schema); const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix); @@ -253,6 +261,7 @@ export default { annotationRenameLabelInCategory: annoActions.annotationRenameLabelInCategory, annotationLabelCurrentSelection: annoActions.annotationLabelCurrentSelection, saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction, + saveGenesetsAction: annoActions.saveGenesetsAction, needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations, layoutChoiceAction: embActions.layoutChoiceAction, setCellSetFromSelection: selnActions.setCellSetFromSelection, diff --git a/client/src/components/autosave/filenameDialog.js b/client/src/components/autosave/filenameDialog.js index 3ff7e500..c6ee50dc 100644 --- a/client/src/components/autosave/filenameDialog.js +++ b/client/src/components/autosave/filenameDialog.js @@ -17,6 +17,9 @@ import { auth: state.config?.authentication, userInfo: state.userInfo, writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, + writableGenesetsEnabled: !( + state.config?.parameters?.["annotations_genesets_readonly"] ?? true + ), })) class FilenameDialog extends React.Component { constructor(props) { @@ -101,7 +104,7 @@ class FilenameDialog extends React.Component { } = this.props; const { filenameText } = this.state; - return writableCategoriesEnabled && + return (writableCategoriesEnabled || writableGenesetsEnabled) && annotations.promptForFilename && !annotations.dataCollectionNameIsReadOnly && !annotations.dataCollectionName && diff --git a/client/src/components/autosave/index.js b/client/src/components/autosave/index.js index c03cdfd5..dcf93831 100644 --- a/client/src/components/autosave/index.js +++ b/client/src/components/autosave/index.js @@ -5,11 +5,18 @@ import FilenameDialog from "./filenameDialog"; @connect((state) => ({ annotations: state.annotations, - saveInProgress: state.autosave?.saveInProgress ?? false, + obsAnnotationSaveInProgress: + state.autosave?.obsAnnotationSaveInProgress ?? false, + genesetSaveInProgress: state.autosave?.genesetSaveInProgress ?? false, error: state.autosave?.error, writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, + writableGenesetsEnabled: !( + state.config?.parameters?.annotations_genesets_readonly ?? true + ), annoMatrix: state.annoMatrix, + genesets: state.genesets, lastSavedAnnoMatrix: state.autosave?.lastSavedAnnoMatrix, + lastSavedGenesets: state.autosave?.lastSavedGenesets, })) class Autosave extends React.Component { constructor(props) { @@ -38,18 +45,40 @@ class Autosave extends React.Component { } tick = () => { - const { dispatch, saveInProgress } = this.props; - if (this.needToSave() && !saveInProgress) { + const { + dispatch, + obsAnnotationSaveInProgress, + genesetSaveInProgress, + } = this.props; + if (!obsAnnotationSaveInProgress && this.needToSaveObsAnnotations()) { dispatch(actions.saveObsAnnotationsAction()); } + if (!genesetSaveInProgress && this.needToSaveGenesets()) { + dispatch(actions.saveGenesetsAction()); + } }; - needToSave = () => { - /* return true if we need to save, false if we don't */ + saveInProgress() { + const { obsAnnotationSaveInProgress, genesetSaveInProgress } = this.props; + return obsAnnotationSaveInProgress || genesetSaveInProgress; + } + + needToSaveObsAnnotations = () => { + /* return true if we need to save obs cell labels, false if we don't */ const { annoMatrix, lastSavedAnnoMatrix } = this.props; return actions.needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix); }; + needToSaveGenesets = () => { + /* return true if we need to save gene ses, false if we do not */ + const { genesets, lastSavedGenesets } = this.props; + return genesets.initialized && genesets !== lastSavedGenesets; + }; + + needToSave() { + return this.needToSaveGenesets() || this.needToSaveObsAnnotations(); + } + statusMessage() { const { error } = this.props; if (error) { @@ -59,11 +88,7 @@ class Autosave extends React.Component { } render() { - const { - writableCategoriesEnabled, - saveInProgress, - lastSavedAnnoMatrix, - } = this.props; + const { writableCategoriesEnabled, lastSavedAnnoMatrix } = this.props; const initialDataLoadComplete = lastSavedAnnoMatrix; if (!writableCategoriesEnabled) return null; @@ -74,7 +99,7 @@ class Autosave extends React.Component { data-testclass={ !initialDataLoadComplete ? "autosave-init" - : this.needToSave() || saveInProgress + : this.saveInProgress() || this.needToSave() ? "autosave-incomplete" : "autosave-complete" }