mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-07 11:38:12 +08:00
wire up geneset reducer (#2082)
* first cut at GET /genesets route * update existing tests to match code changes * more GET /genesets and initial tests * add missing test fixture * geneset validation accepts OTA format * genesets route: better error handling, more tests * lint * genesets reducer and initial load * fix lint * add autosave support for genesets * remove debug logging * fix typo * fix another typo * update smoke test config for genesets * smoke test fixes * more fiddling with smoke tests
This commit is contained in:
@@ -0,0 +1,524 @@
|
|||||||
|
import genesetsReducer from "../../src/reducers/genesets";
|
||||||
|
|
||||||
|
describe("initial reducer state", () => {
|
||||||
|
test("some other action", () => {
|
||||||
|
expect(genesetsReducer(undefined, { type: "foo" })).toMatchObject({
|
||||||
|
initialized: false,
|
||||||
|
lastTid: undefined,
|
||||||
|
genesets: new Map(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("geneset: initial load", () => {
|
||||||
|
test("missing JSON response", () => {
|
||||||
|
expect(() =>
|
||||||
|
genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
})
|
||||||
|
).toThrow("missing or malformed JSON response");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty geneset", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: {
|
||||||
|
tid: 0,
|
||||||
|
genesets: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
initialized: true,
|
||||||
|
lastTid: 0,
|
||||||
|
genesets: new Map(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-empty geneset", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: {
|
||||||
|
tid: 99,
|
||||||
|
genesets: [
|
||||||
|
{
|
||||||
|
geneset_name: "G1",
|
||||||
|
genes: [{ gene_symbol: "F5" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
geneset_name: "G2",
|
||||||
|
geneset_description: "G2 desc",
|
||||||
|
genes: [{ gene_symbol: "F6" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
geneset_name: "G3",
|
||||||
|
geneset_description: "G3 desc",
|
||||||
|
genes: [{ gene_symbol: "F7", gene_description: "gene desc" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
initialized: true,
|
||||||
|
lastTid: 99,
|
||||||
|
genesets: new Map([
|
||||||
|
[
|
||||||
|
"G1",
|
||||||
|
{
|
||||||
|
genesetName: "G1",
|
||||||
|
genesetDescription: "",
|
||||||
|
genes: new Map([["F5", { geneSymbol: "F5", geneDescription: "" }]]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"G2",
|
||||||
|
{
|
||||||
|
genesetName: "G2",
|
||||||
|
genesetDescription: "G2 desc",
|
||||||
|
genes: new Map([["F6", { geneSymbol: "F6", geneDescription: "" }]]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"G3",
|
||||||
|
{
|
||||||
|
genesetName: "G3",
|
||||||
|
genesetDescription: "G3 desc",
|
||||||
|
genes: new Map([
|
||||||
|
["F7", { geneSymbol: "F7", geneDescription: "gene desc" }],
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("geneset: create", () => {
|
||||||
|
const initialState = genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: {
|
||||||
|
tid: 0,
|
||||||
|
genesets: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple create", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "a geneset",
|
||||||
|
genesetDescription: "",
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
...initialState,
|
||||||
|
genesets: new Map([
|
||||||
|
[
|
||||||
|
"a geneset",
|
||||||
|
{
|
||||||
|
genesetName: "a geneset",
|
||||||
|
genesetDescription: "",
|
||||||
|
genes: new Map(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("error - duplicate name", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "foo",
|
||||||
|
genesetDescription: "foo",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "foo",
|
||||||
|
genesetDescription: "bar",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).toThrow("name already defined");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("error - missing required action values", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetDescription: "foo",
|
||||||
|
});
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "foo",
|
||||||
|
});
|
||||||
|
}).toThrow("name or description unspecified");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("geneset: delete", () => {
|
||||||
|
const initialState = genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: {
|
||||||
|
tid: 0,
|
||||||
|
genesets: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple delete", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "foo",
|
||||||
|
genesetDescription: "foo",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "geneset: delete",
|
||||||
|
genesetName: "foo",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toMatchObject({
|
||||||
|
initialized: true,
|
||||||
|
lastTid: 0,
|
||||||
|
genesets: new Map(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("error - missing name", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: delete",
|
||||||
|
genesetName: "foo",
|
||||||
|
});
|
||||||
|
}).toThrow("name does not exist");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("geneset: update", () => {
|
||||||
|
const initialState = genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: {
|
||||||
|
tid: 0,
|
||||||
|
genesets: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple update", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(
|
||||||
|
genesetsReducer(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "foo1",
|
||||||
|
genesetDescription: "foo1",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "foo2",
|
||||||
|
genesetDescription: "foo2",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "geneset: update",
|
||||||
|
genesetName: "foo1",
|
||||||
|
update: {
|
||||||
|
genesetName: "bar",
|
||||||
|
genesetDescription: "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toMatchObject({
|
||||||
|
initialized: true,
|
||||||
|
lastTid: 0,
|
||||||
|
genesets: new Map([
|
||||||
|
[
|
||||||
|
"bar",
|
||||||
|
{ genesetName: "bar", genesetDescription: "bar", genes: new Map() },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"foo2",
|
||||||
|
{ genesetName: "foo2", genesetDescription: "foo2", genes: new Map() },
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("error - unknown name", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: update",
|
||||||
|
genesetName: "foo",
|
||||||
|
update: {
|
||||||
|
genesetName: "foo",
|
||||||
|
genesetDescription: "bar",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).toThrow("name unspecified or does not exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("error - duplicate name", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "foo",
|
||||||
|
genesetDescription: "foo",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "geneset: update",
|
||||||
|
genesetName: "foo",
|
||||||
|
update: {
|
||||||
|
genesetName: "foo",
|
||||||
|
genesetDescription: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).toThrow("update specified existing name");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("geneset: add genes", () => {
|
||||||
|
const initialState = genesetsReducer(
|
||||||
|
genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: {
|
||||||
|
tid: 0,
|
||||||
|
genesets: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "test",
|
||||||
|
genesetDescription: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test("add a gene", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: add genes",
|
||||||
|
genesetName: "test",
|
||||||
|
genes: [{ geneSymbol: "F5" }],
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
...initialState,
|
||||||
|
genesets: new Map([
|
||||||
|
[
|
||||||
|
"test",
|
||||||
|
{
|
||||||
|
genesetName: "test",
|
||||||
|
genesetDescription: "",
|
||||||
|
genes: new Map([["F5", { geneSymbol: "F5", geneDescription: "" }]]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: add genes",
|
||||||
|
genesetName: "test",
|
||||||
|
genes: [
|
||||||
|
{ geneSymbol: "F5", geneDescription: "desc" },
|
||||||
|
{ geneSymbol: "SET1", geneDescription: "" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
...initialState,
|
||||||
|
genesets: new Map([
|
||||||
|
[
|
||||||
|
"test",
|
||||||
|
{
|
||||||
|
genesetName: "test",
|
||||||
|
genesetDescription: "",
|
||||||
|
genes: new Map([
|
||||||
|
["F5", { geneSymbol: "F5", geneDescription: "desc" }],
|
||||||
|
["SET1", { geneSymbol: "SET1", geneDescription: "" }],
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no such geneset error", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: add genes",
|
||||||
|
genesetName: "mumble",
|
||||||
|
genes: [],
|
||||||
|
});
|
||||||
|
}).toThrow("geneset name does not exist");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("geneset: delete genes", () => {
|
||||||
|
const initialState = genesetsReducer(
|
||||||
|
genesetsReducer(
|
||||||
|
genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: {
|
||||||
|
tid: 0,
|
||||||
|
genesets: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "test",
|
||||||
|
genesetDescription: "",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "geneset: add genes",
|
||||||
|
genesetName: "test",
|
||||||
|
genes: [{ geneSymbol: "F5" }],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test("simple", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: delete genes",
|
||||||
|
genesetName: "test",
|
||||||
|
geneSymbols: ["F5"],
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
...initialState,
|
||||||
|
genesets: new Map([
|
||||||
|
[
|
||||||
|
"test",
|
||||||
|
{
|
||||||
|
genesetName: "test",
|
||||||
|
genesetDescription: "",
|
||||||
|
genes: new Map(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no such geneset error", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: delete genes",
|
||||||
|
genesetName: "mumble",
|
||||||
|
geneSymbols: [],
|
||||||
|
});
|
||||||
|
}).toThrow("name does not exist");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("geneset: set gene description", () => {
|
||||||
|
const initialState = genesetsReducer(
|
||||||
|
genesetsReducer(
|
||||||
|
genesetsReducer(undefined, {
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: {
|
||||||
|
tid: 0,
|
||||||
|
genesets: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "geneset: create",
|
||||||
|
genesetName: "test",
|
||||||
|
genesetDescription: "",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "geneset: add genes",
|
||||||
|
genesetName: "test",
|
||||||
|
genes: [{ geneSymbol: "F5" }],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test("simple set", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: set gene description",
|
||||||
|
genesetName: "test",
|
||||||
|
update: {
|
||||||
|
geneSymbol: "F5",
|
||||||
|
geneDescription: "mumble",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
...initialState,
|
||||||
|
genesets: new Map([
|
||||||
|
[
|
||||||
|
"test",
|
||||||
|
{
|
||||||
|
genesetName: "test",
|
||||||
|
genesetDescription: "",
|
||||||
|
genes: new Map([
|
||||||
|
["F5", { geneSymbol: "F5", geneDescription: "mumble" }],
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no such geneset error", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: set gene description",
|
||||||
|
genesetName: "does not exist",
|
||||||
|
update: {
|
||||||
|
geneSymbol: "F5",
|
||||||
|
geneDescription: "mumble",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).toThrow("geneset name does not exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no such gene error", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(initialState, {
|
||||||
|
type: "geneset: set gene description",
|
||||||
|
genesetName: "test",
|
||||||
|
update: {
|
||||||
|
geneSymbol: "NO SUCH GENE",
|
||||||
|
geneDescription: "mumble",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).toThrow("no such gene");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("geneset: set tid", () => {
|
||||||
|
test("simple set", () => {
|
||||||
|
expect(
|
||||||
|
genesetsReducer(undefined, {
|
||||||
|
type: "geneset: set tid",
|
||||||
|
tid: 1,
|
||||||
|
})
|
||||||
|
).toMatchObject({ lastTid: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("not a number error", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(
|
||||||
|
{ lastTid: 1 },
|
||||||
|
{
|
||||||
|
type: "geneset: set tid",
|
||||||
|
tid: "0",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).toThrow("must be a positive integer");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("decrement error", () => {
|
||||||
|
expect(() => {
|
||||||
|
genesetsReducer(
|
||||||
|
{ lastTid: 1 },
|
||||||
|
{
|
||||||
|
type: "geneset: set tid",
|
||||||
|
tid: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).toThrow("may not be decremented");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -387,3 +387,96 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const saveGenesetsAction = () => async (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
// bail if gene sets not available, or in readonly mode.
|
||||||
|
const { config } = state;
|
||||||
|
const genesetsAreAvailable =
|
||||||
|
config?.parameters?.["annotations_genesets"] ?? false;
|
||||||
|
const genesetsReadonly =
|
||||||
|
config?.parameters?.["annotations_genesets_readonly"] ?? true;
|
||||||
|
if (!genesetsAreAvailable || genesetsReadonly) {
|
||||||
|
// our non-save was completed!
|
||||||
|
return dispatch({
|
||||||
|
type: "autosave: genesets complete",
|
||||||
|
lastSavedGenesets: state.genesets,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lastTid, genesets: lastGenesets } = state.genesets;
|
||||||
|
|
||||||
|
/* Create the JSON OTA data structure */
|
||||||
|
const tid = (lastTid ?? 0) + 1;
|
||||||
|
const genesets = [];
|
||||||
|
for (const [name, gs] of lastGenesets) {
|
||||||
|
// const genes = Array.from(gs.genes.values());
|
||||||
|
const genes = [];
|
||||||
|
for (const g of gs.genes.values()) {
|
||||||
|
genes.push({
|
||||||
|
gene_symbol: g.geneSymbol,
|
||||||
|
gene_description: g.geneDescription,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
genesets.push({
|
||||||
|
geneset_name: name,
|
||||||
|
geneset_description: gs.genesetDescription,
|
||||||
|
genes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const ota = {
|
||||||
|
tid,
|
||||||
|
genesets,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Save to server */
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
dataCollectionNameIsReadOnly,
|
||||||
|
dataCollectionName,
|
||||||
|
} = state.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(ota),
|
||||||
|
credentials: "include",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
return dispatch({
|
||||||
|
type: "autosave: genesets error",
|
||||||
|
message: `HTTP error ${res.status} - ${res.statusText}`,
|
||||||
|
res,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.all([
|
||||||
|
dispatch({
|
||||||
|
type: "autosave: genesets complete",
|
||||||
|
lastSavedGenesets: genesets,
|
||||||
|
}),
|
||||||
|
dispatch({
|
||||||
|
type: "geneset: set tid",
|
||||||
|
tid,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
return dispatch({
|
||||||
|
type: "autosave: genesets error",
|
||||||
|
message: error.toString(),
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -52,6 +52,27 @@ async function userInfoFetch(dispatch) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function genesetsFetch(dispatch, config) {
|
||||||
|
/* request genesets ONLY if the backend supports the feature */
|
||||||
|
const defaultResponse = {
|
||||||
|
genesets: [],
|
||||||
|
tid: 0,
|
||||||
|
};
|
||||||
|
if (config?.parameters?.["annotations_genesets"] ?? false) {
|
||||||
|
fetchJson("genesets").then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: response ?? defaultResponse,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "geneset: initial load",
|
||||||
|
data: defaultResponse,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function prefetchEmbeddings(annoMatrix) {
|
function prefetchEmbeddings(annoMatrix) {
|
||||||
/*
|
/*
|
||||||
prefetch requests for all embeddings
|
prefetch requests for all embeddings
|
||||||
@@ -76,6 +97,8 @@ const doInitialDataLoad = () =>
|
|||||||
userInfoFetch(dispatch),
|
userInfoFetch(dispatch),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
genesetsFetch(dispatch, config);
|
||||||
|
|
||||||
const baseDataUrl = `${globals.API.prefix}${globals.API.version}`;
|
const baseDataUrl = `${globals.API.prefix}${globals.API.version}`;
|
||||||
const annoMatrix = new AnnoMatrixLoader(baseDataUrl, schema.schema);
|
const annoMatrix = new AnnoMatrixLoader(baseDataUrl, schema.schema);
|
||||||
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
|
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
|
||||||
@@ -242,6 +265,7 @@ export default {
|
|||||||
annotationRenameLabelInCategory: annoActions.annotationRenameLabelInCategory,
|
annotationRenameLabelInCategory: annoActions.annotationRenameLabelInCategory,
|
||||||
annotationLabelCurrentSelection: annoActions.annotationLabelCurrentSelection,
|
annotationLabelCurrentSelection: annoActions.annotationLabelCurrentSelection,
|
||||||
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
|
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
|
||||||
|
saveGenesetsAction: annoActions.saveGenesetsAction,
|
||||||
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
|
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
|
||||||
layoutChoiceAction: embActions.layoutChoiceAction,
|
layoutChoiceAction: embActions.layoutChoiceAction,
|
||||||
setCellSetFromSelection: selnActions.setCellSetFromSelection,
|
setCellSetFromSelection: selnActions.setCellSetFromSelection,
|
||||||
|
|||||||
@@ -5,11 +5,18 @@ import FilenameDialog from "./filenameDialog";
|
|||||||
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
annotations: state.annotations,
|
annotations: state.annotations,
|
||||||
saveInProgress: state.autosave?.saveInProgress ?? false,
|
obsAnnotationSaveInProgress:
|
||||||
|
state.autosave?.obsAnnotationSaveInProgress ?? false,
|
||||||
|
genesetSaveInProgress: state.autosave?.genesetSaveInProgress ?? false,
|
||||||
error: state.autosave?.error,
|
error: state.autosave?.error,
|
||||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
|
writableGenesetsEnabled: !(
|
||||||
|
state.config?.parameters?.["annotations_genesets_readonly"] ?? true
|
||||||
|
),
|
||||||
annoMatrix: state.annoMatrix,
|
annoMatrix: state.annoMatrix,
|
||||||
|
genesets: state.genesets,
|
||||||
lastSavedAnnoMatrix: state.autosave?.lastSavedAnnoMatrix,
|
lastSavedAnnoMatrix: state.autosave?.lastSavedAnnoMatrix,
|
||||||
|
lastSavedGenesets: state.autosave?.lastSavedGenesets,
|
||||||
}))
|
}))
|
||||||
class Autosave extends React.Component {
|
class Autosave extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -20,11 +27,11 @@ class Autosave extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { writableCategoriesEnabled } = this.props;
|
const { writableCategoriesEnabled, writableGenesetsEnabled } = this.props;
|
||||||
|
|
||||||
let { timer } = this.state;
|
let { timer } = this.state;
|
||||||
if (timer) clearInterval(timer);
|
if (timer) clearInterval(timer);
|
||||||
if (writableCategoriesEnabled) {
|
if (writableCategoriesEnabled || writableGenesetsEnabled) {
|
||||||
timer = setInterval(this.tick, 2500);
|
timer = setInterval(this.tick, 2500);
|
||||||
} else {
|
} else {
|
||||||
timer = null;
|
timer = null;
|
||||||
@@ -38,18 +45,40 @@ class Autosave extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tick = () => {
|
tick = () => {
|
||||||
const { dispatch, saveInProgress } = this.props;
|
const {
|
||||||
if (this.needToSave() && !saveInProgress) {
|
dispatch,
|
||||||
|
obsAnnotationSaveInProgress,
|
||||||
|
genesetSaveInProgress,
|
||||||
|
} = this.props;
|
||||||
|
if (!obsAnnotationSaveInProgress && this.needToSaveObsAnnotations()) {
|
||||||
dispatch(actions.saveObsAnnotationsAction());
|
dispatch(actions.saveObsAnnotationsAction());
|
||||||
}
|
}
|
||||||
|
if (!genesetSaveInProgress && this.needToSaveGenesets()) {
|
||||||
|
dispatch(actions.saveGenesetsAction());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
needToSave = () => {
|
needToSaveObsAnnotations = () => {
|
||||||
/* return true if we need to save, false if we don't */
|
/* return true if we need to save obs cell labels, false if we don't */
|
||||||
const { annoMatrix, lastSavedAnnoMatrix } = this.props;
|
const { annoMatrix, lastSavedAnnoMatrix } = this.props;
|
||||||
return actions.needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveInProgress() {
|
||||||
|
const { obsAnnotationSaveInProgress, genesetSaveInProgress } = this.props;
|
||||||
|
return obsAnnotationSaveInProgress || genesetSaveInProgress;
|
||||||
|
}
|
||||||
|
|
||||||
statusMessage() {
|
statusMessage() {
|
||||||
const { error } = this.props;
|
const { error } = this.props;
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -61,12 +90,12 @@ class Autosave extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
writableCategoriesEnabled,
|
writableCategoriesEnabled,
|
||||||
saveInProgress,
|
writableGenesetsEnabled,
|
||||||
lastSavedAnnoMatrix,
|
lastSavedAnnoMatrix,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const initialDataLoadComplete = lastSavedAnnoMatrix;
|
const initialDataLoadComplete = lastSavedAnnoMatrix;
|
||||||
|
|
||||||
if (!writableCategoriesEnabled) return null;
|
if (!writableCategoriesEnabled && !writableGenesetsEnabled) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -74,7 +103,7 @@ class Autosave extends React.Component {
|
|||||||
data-testclass={
|
data-testclass={
|
||||||
!initialDataLoadComplete
|
!initialDataLoadComplete
|
||||||
? "autosave-init"
|
? "autosave-init"
|
||||||
: this.needToSave() || saveInProgress
|
: this.saveInProgress() || this.needToSave()
|
||||||
? "autosave-incomplete"
|
? "autosave-incomplete"
|
||||||
: "autosave-complete"
|
: "autosave-complete"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
const Autosave = (
|
const Autosave = (
|
||||||
state = {
|
state = {
|
||||||
saveInProgress: false,
|
// cell labels
|
||||||
error: false,
|
obsAnnotationSaveInProgress: false,
|
||||||
lastSavedAnnoMatrix: null,
|
lastSavedAnnoMatrix: null,
|
||||||
|
|
||||||
|
// gene sets
|
||||||
|
genesetSaveInProgress: false,
|
||||||
|
lastSavedGenesets: null,
|
||||||
|
|
||||||
|
// error state
|
||||||
|
error: false,
|
||||||
},
|
},
|
||||||
action
|
action,
|
||||||
|
nextSharedState
|
||||||
) => {
|
) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "annoMatrix: init complete": {
|
case "annoMatrix: init complete": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: false,
|
error: false,
|
||||||
saveInProgress: false,
|
obsAnnotationSaveInProgress: false,
|
||||||
lastSavedAnnoMatrix: action.annoMatrix,
|
lastSavedAnnoMatrix: action.annoMatrix,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -19,7 +27,7 @@ const Autosave = (
|
|||||||
case "writable obs annotations - save started": {
|
case "writable obs annotations - save started": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
saveInProgress: true,
|
obsAnnotationSaveInProgress: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +35,7 @@ const Autosave = (
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: action.message,
|
error: action.message,
|
||||||
saveInProgress: false,
|
obsAnnotationSaveInProgress: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,12 +43,45 @@ const Autosave = (
|
|||||||
const { lastSavedAnnoMatrix } = action;
|
const { lastSavedAnnoMatrix } = action;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
saveInProgress: false,
|
obsAnnotationSaveInProgress: false,
|
||||||
error: false,
|
error: false,
|
||||||
lastSavedAnnoMatrix,
|
lastSavedAnnoMatrix,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "geneset: initial load": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesetSaveInProgress: false,
|
||||||
|
lastSavedGenesets: nextSharedState.genesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "autosave: genesets started": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesetSaveInProgress: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "autosave: genesets error": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesetSaveInProgress: false,
|
||||||
|
error: action.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "autosave: genesets complete": {
|
||||||
|
const { lastSavedGenesets } = action;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesetSaveInProgess: false,
|
||||||
|
error: false,
|
||||||
|
lastSavedGenesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { ...state };
|
return { ...state };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* Gene set state. Geneset UI state is in a different reducer.
|
||||||
|
*
|
||||||
|
* geneset reducer state is a Map object, where:
|
||||||
|
* key: the geneset name, a string.
|
||||||
|
* val: the geneset defined as an object ("geneset object")
|
||||||
|
*
|
||||||
|
* A geneset object is:
|
||||||
|
* {
|
||||||
|
* genesetName: <string> # same as the map key
|
||||||
|
* genesetDescription: <string>
|
||||||
|
* genes: Map<<string>, {
|
||||||
|
* geneSymbol: <string>, # same as the map key
|
||||||
|
* geneDescription: <string>
|
||||||
|
* }>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Geneset and genes Map order is significant, and will be preserved across
|
||||||
|
* CRUD operations on either.
|
||||||
|
*
|
||||||
|
* This reducer does light error checking, but not as much as the backend
|
||||||
|
* routes. Do not rely on it to enforce geneset integrity - eg, no duplicate
|
||||||
|
* genes in a geneset.
|
||||||
|
*/
|
||||||
|
const GeneSets = (
|
||||||
|
state = {
|
||||||
|
initialized: false,
|
||||||
|
lastTid: undefined,
|
||||||
|
genesets: new Map(),
|
||||||
|
},
|
||||||
|
action
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
/**
|
||||||
|
* Initial, load-time bootstrap.
|
||||||
|
* {
|
||||||
|
* type: "geneset: initial load"
|
||||||
|
* data: JSON response
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
case "geneset: initial load": {
|
||||||
|
const { data } = action;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!data ||
|
||||||
|
typeof data.tid !== "number" ||
|
||||||
|
!Array.isArray(data.genesets)
|
||||||
|
)
|
||||||
|
throw new Error("missing or malformed JSON response");
|
||||||
|
|
||||||
|
const lastTid = data.tid;
|
||||||
|
const genesetsData = data.genesets;
|
||||||
|
const genesets = new Map();
|
||||||
|
|
||||||
|
for (const gsData of genesetsData) {
|
||||||
|
const genes = new Map();
|
||||||
|
for (const gene of gsData.genes) {
|
||||||
|
genes.set(gene.gene_symbol, {
|
||||||
|
geneSymbol: gene.gene_symbol,
|
||||||
|
geneDescription: gene?.["gene_description"] ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const gs = {
|
||||||
|
genesetName: gsData.geneset_name,
|
||||||
|
genesetDescription: gsData?.["geneset_description"] ?? "",
|
||||||
|
genes,
|
||||||
|
};
|
||||||
|
genesets.set(gsData.geneset_name, gs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialized: true,
|
||||||
|
lastTid,
|
||||||
|
genesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new & empty geneset with the given name and description.
|
||||||
|
* {
|
||||||
|
* type: "geneset: create",
|
||||||
|
* genesetName: string, // gene set name
|
||||||
|
* genesetDescription: string, // geneset description
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
case "geneset: create": {
|
||||||
|
const { genesetName, genesetDescription } = action;
|
||||||
|
if (
|
||||||
|
typeof genesetName !== "string" ||
|
||||||
|
!genesetName ||
|
||||||
|
genesetDescription === undefined
|
||||||
|
)
|
||||||
|
throw new Error("geneset: create -- name or description unspecified.");
|
||||||
|
if (state.genesets.has(genesetName))
|
||||||
|
throw new Error("geneset: create -- name already defined.");
|
||||||
|
|
||||||
|
const genesets = new Map(state.genesets); // clone
|
||||||
|
genesets.set(genesetName, {
|
||||||
|
genesetName,
|
||||||
|
genesetDescription,
|
||||||
|
genes: new Map(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the named geneset, if it exists. Throws if it does not.
|
||||||
|
* {
|
||||||
|
* type: "geneset: delete",
|
||||||
|
* genesetName: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
case "geneset: delete": {
|
||||||
|
const { genesetName } = action;
|
||||||
|
if (!state.genesets.has(genesetName))
|
||||||
|
throw new Error("geneset: delete -- geneset name does not exist.");
|
||||||
|
|
||||||
|
const genesets = new Map(state.genesets); // clone
|
||||||
|
genesets.delete(genesetName);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the named geneset with a new name and description. Preserves the existing
|
||||||
|
* order of the geneset, even when the genesetName changes.
|
||||||
|
* {
|
||||||
|
* type: "geneset: update",
|
||||||
|
* genesetName: string, current name of geneset to be updated
|
||||||
|
* update: {
|
||||||
|
* genesetName: string, new name
|
||||||
|
* genesetDescription: string, new description
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* For example, if you want to update JUST the description:
|
||||||
|
* dispatch({
|
||||||
|
* action: "geneset: update",
|
||||||
|
* genesetName: "foo",
|
||||||
|
* update: { genesetName: "foo", genesetDescription: "a new description"}
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
case "geneset: update": {
|
||||||
|
const { genesetName, update } = action;
|
||||||
|
if (
|
||||||
|
typeof genesetName !== "string" ||
|
||||||
|
!genesetName ||
|
||||||
|
!state.genesets.has(genesetName)
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
"geneset: update -- geneset name unspecified or does not exist."
|
||||||
|
);
|
||||||
|
if (state.genesets.has(update.genesetName))
|
||||||
|
throw new Error("geneset: update -- update specified existing name.");
|
||||||
|
|
||||||
|
const prevGs = state.genesets.get(genesetName);
|
||||||
|
const newGs = {
|
||||||
|
...update,
|
||||||
|
genes: prevGs.genes,
|
||||||
|
}; // clone
|
||||||
|
|
||||||
|
// clone the map, preserving current insert order, but mapping name->newName.
|
||||||
|
const genesets = new Map();
|
||||||
|
for (const [name, gs] of state.genesets) {
|
||||||
|
if (name === genesetName) genesets.set(newGs.genesetName, newGs);
|
||||||
|
else genesets.set(name, gs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds genes to the geneset. They are appended to the END of the geneset, in the
|
||||||
|
* order provided. Duplicates or genes already in the geneset, will be ignored.
|
||||||
|
* {
|
||||||
|
* type: "geneset: add genes"
|
||||||
|
* genesetName: <string>, // gene set name
|
||||||
|
* genes: Array<{
|
||||||
|
* geneSymbol: <string>,
|
||||||
|
* geneDescription: <string>
|
||||||
|
* }>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* dispatch({
|
||||||
|
* type: "add genes",
|
||||||
|
* genesetName: "foo",
|
||||||
|
* genes: [ { geneSymbol: "FOXP", geneDescription: "test" }]
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
case "geneset: add genes": {
|
||||||
|
const { genesetName, genes } = action;
|
||||||
|
if (!state.genesets.has(genesetName))
|
||||||
|
throw new Error("geneset: add genes -- geneset name does not exist.");
|
||||||
|
|
||||||
|
// clone
|
||||||
|
const genesets = new Map(state.genesets);
|
||||||
|
const gs = {
|
||||||
|
...genesets.get(genesetName),
|
||||||
|
genes: new Map(genesets.get(genesetName).genes),
|
||||||
|
};
|
||||||
|
genesets.set(genesetName, gs);
|
||||||
|
|
||||||
|
// add
|
||||||
|
const newGenes = gs.genes;
|
||||||
|
for (const gene of genes) {
|
||||||
|
const { geneSymbol } = gene;
|
||||||
|
const geneDescription = gene?.geneDescription ?? "";
|
||||||
|
// ignore genes already present
|
||||||
|
if (!newGenes.has(geneSymbol))
|
||||||
|
newGenes.set(geneSymbol, {
|
||||||
|
geneSymbol,
|
||||||
|
geneDescription,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete genes from the named geneset. Will throw if the genesetName does
|
||||||
|
* not exist. Will ignore geneSymbols that do not exist.
|
||||||
|
* {
|
||||||
|
* type: "geneset: delete genes",
|
||||||
|
* genesetName: <string>, // the geneset from which to delete genes
|
||||||
|
* geneSymbols: [<string>, ...], // the gene symbols to delete.
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* dispatch({
|
||||||
|
* type: "geneset: delete genes",
|
||||||
|
* genesetName: "a geneset name",
|
||||||
|
* geneSymbols: ["F5"]
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
case "geneset: delete genes": {
|
||||||
|
const { genesetName, geneSymbols } = action;
|
||||||
|
if (!state.genesets.has(genesetName))
|
||||||
|
throw new Error(
|
||||||
|
"geneset: delete genes -- geneset name does not exist."
|
||||||
|
);
|
||||||
|
|
||||||
|
// clone
|
||||||
|
const genesets = new Map(state.genesets);
|
||||||
|
const gs = {
|
||||||
|
...genesets.get(genesetName),
|
||||||
|
genes: new Map(genesets.get(genesetName).genes),
|
||||||
|
};
|
||||||
|
genesets.set(genesetName, gs);
|
||||||
|
|
||||||
|
// delete
|
||||||
|
const { genes } = gs;
|
||||||
|
for (const geneSymbol of geneSymbols) {
|
||||||
|
genes.delete(geneSymbol);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set/update the description of the gene. NOTE that this does not allow the name
|
||||||
|
* of the gene to change - only "geneset: add" and "geneset: delete" can change
|
||||||
|
* the genes in a geneset. Use this to update a gene description AFTER you add it
|
||||||
|
* to the geneset.
|
||||||
|
* {
|
||||||
|
* type: "geneset: set gene description",
|
||||||
|
* genesetName: <string>, // the geneset to update
|
||||||
|
* update: {
|
||||||
|
* geneSymbol: <string>, // the gene to update, MUST exist already in the geneset
|
||||||
|
* geneDescription: <string>
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* dispatch({
|
||||||
|
* type: "geneset: set gene description",
|
||||||
|
* genesetName: "my fav geneset",
|
||||||
|
* update: {
|
||||||
|
* geneSymbol: "F5",
|
||||||
|
* geneDescription: "tada, moar description"
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
case "geneset: set gene description": {
|
||||||
|
const { genesetName, update } = action;
|
||||||
|
if (!state.genesets.has(genesetName))
|
||||||
|
throw new Error(
|
||||||
|
"geneset: set gene description -- geneset name does not exist."
|
||||||
|
);
|
||||||
|
|
||||||
|
// clone
|
||||||
|
const genesets = new Map(state.genesets);
|
||||||
|
const gs = {
|
||||||
|
...genesets.get(genesetName),
|
||||||
|
genes: new Map(genesets.get(genesetName).genes),
|
||||||
|
};
|
||||||
|
genesets.set(genesetName, gs);
|
||||||
|
|
||||||
|
const { geneSymbol, geneDescription } = update;
|
||||||
|
const gene = gs.genes.get(geneSymbol);
|
||||||
|
if (!gene)
|
||||||
|
throw new Error("geneset: set gene description -- no such gene");
|
||||||
|
gs.genes.set(geneSymbol, {
|
||||||
|
geneSymbol,
|
||||||
|
geneDescription,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
genesets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by autosave to update the server synchronization TID
|
||||||
|
*/
|
||||||
|
case "geneset: set tid": {
|
||||||
|
const { tid } = action;
|
||||||
|
if (!Number.isInteger(tid) || tid < 0)
|
||||||
|
throw new Error("TID must be a positive integer number");
|
||||||
|
if (state.lastTid !== undefined && tid < state.lastTid)
|
||||||
|
throw new Error("TID may not be decremented.");
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
lastTid: tid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GeneSets;
|
||||||
@@ -15,6 +15,7 @@ import differential from "./differential";
|
|||||||
import layoutChoice from "./layoutChoice";
|
import layoutChoice from "./layoutChoice";
|
||||||
import controls from "./controls";
|
import controls from "./controls";
|
||||||
import annotations from "./annotations";
|
import annotations from "./annotations";
|
||||||
|
import genesets from "./genesets";
|
||||||
import autosave from "./autosave";
|
import autosave from "./autosave";
|
||||||
import ontology from "./ontology";
|
import ontology from "./ontology";
|
||||||
import centroidLabels from "./centroidLabels";
|
import centroidLabels from "./centroidLabels";
|
||||||
@@ -31,6 +32,7 @@ const Reducer = undoable(
|
|||||||
["obsCrossfilter", obsCrossfilter],
|
["obsCrossfilter", obsCrossfilter],
|
||||||
["ontology", ontology],
|
["ontology", ontology],
|
||||||
["annotations", annotations],
|
["annotations", annotations],
|
||||||
|
["genesets", genesets],
|
||||||
["layoutChoice", layoutChoice],
|
["layoutChoice", layoutChoice],
|
||||||
["categoricalSelection", categoricalSelection],
|
["categoricalSelection", categoricalSelection],
|
||||||
["continuousSelection", continuousSelection],
|
["continuousSelection", continuousSelection],
|
||||||
@@ -55,6 +57,7 @@ const Reducer = undoable(
|
|||||||
"differential",
|
"differential",
|
||||||
"layoutChoice",
|
"layoutChoice",
|
||||||
"centroidLabels",
|
"centroidLabels",
|
||||||
|
"genesets",
|
||||||
"annotations",
|
"annotations",
|
||||||
],
|
],
|
||||||
undoableConfig
|
undoableConfig
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class AnnotationsLocalFile(Annotations):
|
|||||||
def read_genesets(self, data_adaptor, context=None):
|
def read_genesets(self, data_adaptor, context=None):
|
||||||
if has_request_context():
|
if has_request_context():
|
||||||
if not current_app.auth.is_user_authenticated():
|
if not current_app.auth.is_user_authenticated():
|
||||||
return ([], None)
|
return ({}, self.last_geneset_tid)
|
||||||
|
|
||||||
fname = self._get_genesets_filename(data_adaptor)
|
fname = self._get_genesets_filename(data_adaptor)
|
||||||
genesets = {}
|
genesets = {}
|
||||||
@@ -165,7 +165,7 @@ class AnnotationsLocalFile(Annotations):
|
|||||||
|
|
||||||
output_file = self.label_output_file or self.genesets_output_file
|
output_file = self.label_output_file or self.genesets_output_file
|
||||||
if output_file:
|
if output_file:
|
||||||
return os.path.dirname(self.path.abspath(output_file))
|
return os.path.dirname(os.path.abspath(output_file))
|
||||||
|
|
||||||
return os.getcwd()
|
return os.getcwd()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user