Skip to content

Commit

Permalink
Persist selected species display option (#1133)
Browse files Browse the repository at this point in the history
  • Loading branch information
azangru authored May 14, 2024
1 parent 867e781 commit b819009
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ import { useAppDispatch, useAppSelector } from 'src/store';

import SimpleSelect from 'src/shared/components/simple-select/SimpleSelect';

import {
setSpeciesNameDisplayOption,
type SpeciesNameDisplayOption
} from 'src/content/app/species-selector/state/species-selector-general-slice/speciesSelectorGeneralSlice';
import { setSpeciesNameDisplayOption } from 'src/content/app/species-selector/state/species-selector-general-slice/speciesSelectorGeneralSlice';
import { getSpeciesNameDisplayOption } from '../../state/species-selector-general-slice/speciesSelectorGeneralSelectors';

import type { SpeciesNameDisplayOption } from 'src/content/app/species-selector/types/speciesNameDisplayOption';

import styles from './SpeciesLozengeDisplaySelector.module.css';

type LozengeOptionType = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// The list of allowed options for species display name
export const speciesNameDisplayOptions = [
'common-name_assembly-name',
'common-name_type_assembly-name',
'scientific-name_assembly-name',
'scientific-name_type_assembly-name',
'assembly-accession-id'
] as const;

export const defaultSpeciesNameDisplayOption = speciesNameDisplayOptions[0];
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { createSelector } from '@reduxjs/toolkit';

import type { RootState } from 'src/store';
import type { CommittedItem } from 'src/content/app/species-selector/types/committedItem';
import type { SpeciesNameDisplayOption } from './speciesSelectorGeneralSlice';
import type { SpeciesNameDisplayOption } from 'src/content/app/species-selector/types/speciesNameDisplayOption';

export const getCommittedSpecies = (state: RootState): CommittedItem[] => {
return state.speciesSelector.general.committedItems;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ import {
type Action
} from '@reduxjs/toolkit';

import { defaultSpeciesNameDisplayOption } from 'src/content/app/species-selector/constants/speciesNameDisplayConstants';

import {
getAllSelectedSpecies,
saveMultipleSelectedSpecies,
deleteSelectedSpeciesById
} from 'src/content/app/species-selector/services/speciesSelectorStorageService';
import { deletePreviouslyViewedObjectsForGenome } from 'src/shared/services/previouslyViewedObjectsStorageService';
import {
saveSpeciesNameDisplayOption,
getSpeciesNameDisplayOption
} from 'src/shared/services/generalUIStorageService';

import { deleteSpeciesInGenomeBrowser } from 'src/content/app/genome-browser/state/browser-general/browserGeneralSlice';
import { deleteGenome as deleteSpeciesInEntityViewer } from 'src/content/app/entity-viewer/state/general/entityViewerGeneralSlice';
Expand All @@ -41,13 +47,7 @@ import {
import type { RootState } from 'src/store';
import type { CommittedItem } from 'src/content/app/species-selector/types/committedItem';
import type { SpeciesSearchMatch } from 'src/content/app/species-selector/types/speciesSearchMatch';

export type SpeciesNameDisplayOption =
| 'common-name_assembly-name'
| 'common-name_type_assembly-name'
| 'scientific-name_assembly-name'
| 'scientific-name_type_assembly-name'
| 'assembly-accession-id';
import type { SpeciesNameDisplayOption } from 'src/content/app/species-selector/types/speciesNameDisplayOption';

export type SpeciesSelectorState = {
committedItems: CommittedItem[];
Expand Down Expand Up @@ -106,7 +106,7 @@ export const deleteSpeciesAndSave =

export const initialState: SpeciesSelectorState = {
committedItems: [],
speciesNameDisplayOption: 'common-name_assembly-name'
speciesNameDisplayOption: defaultSpeciesNameDisplayOption
};

const prepareSelectedSpeciesForCommit = (
Expand Down Expand Up @@ -147,6 +147,19 @@ export const commitSelectedSpeciesAndSave = createAsyncThunk(
}
);

export const setSpeciesNameDisplayOption = createAsyncThunk(
'species-selector/setSpeciesNameDisplayOption',
(option: SpeciesNameDisplayOption) => {
saveSpeciesNameDisplayOption(option); // this is asynchronous; but there is no need to await this
return option;
}
);

export const loadSpeciesNameDisplayOption = createAsyncThunk(
'species-selector/loadSpeciesNameDisplayOption',
() => getSpeciesNameDisplayOption()
);

const speciesSelectorGeneralSlice = createSlice({
name: 'species-selector-general',
initialState,
Expand All @@ -165,10 +178,17 @@ const speciesSelectorGeneralSlice = createSlice({
builder.addCase(loadStoredSpecies.fulfilled, (state, action) => {
state.committedItems = action.payload;
});
builder.addCase(setSpeciesNameDisplayOption.fulfilled, (state, action) => {
state.speciesNameDisplayOption = action.payload;
});
builder.addCase(loadSpeciesNameDisplayOption.fulfilled, (state, action) => {
if (action.payload) {
state.speciesNameDisplayOption = action.payload;
}
});
}
});

export const { updateCommittedSpecies, setSpeciesNameDisplayOption } =
speciesSelectorGeneralSlice.actions;
export const { updateCommittedSpecies } = speciesSelectorGeneralSlice.actions;

export default speciesSelectorGeneralSlice.reducer;
20 changes: 20 additions & 0 deletions src/content/app/species-selector/types/speciesNameDisplayOption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { speciesNameDisplayOptions } from '../constants/speciesNameDisplayConstants';

export type SpeciesNameDisplayOption =
(typeof speciesNameDisplayOptions)[number];
6 changes: 5 additions & 1 deletion src/root/useRestoredReduxState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import { useEffect } from 'react';

import { useAppDispatch } from 'src/store';

import { loadStoredSpecies } from 'src/content/app/species-selector/state/species-selector-general-slice/speciesSelectorGeneralSlice';
import {
loadStoredSpecies,
loadSpeciesNameDisplayOption
} from 'src/content/app/species-selector/state/species-selector-general-slice/speciesSelectorGeneralSlice';
import { loadPreviouslyViewedEntities } from 'src/content/app/entity-viewer/state/bookmarks/entityViewerBookmarksSlice';
import { restoreUI as restoreSpeciesPageUI } from 'src/content/app/species/state/general/speciesGeneralSlice';
import { loadInitialState as loadEntityViewerGeneralState } from 'src/content/app/entity-viewer/state/general/entityViewerGeneralSlice';
Expand All @@ -33,6 +36,7 @@ const useRestoredReduxState = () => {
useEffect(() => {
// Species Selector
dispatch(loadStoredSpecies());
dispatch(loadSpeciesNameDisplayOption());

// Species Page
dispatch(restoreSpeciesPageUI());
Expand Down
6 changes: 5 additions & 1 deletion src/services/indexeddb-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@

import { openDB, IDBPDatabase } from 'idb';

import { GENERAL_UI_STORE_NAME } from 'src/shared/services/generalUIStorageConstants';
import { SELECTED_SPECIES_STORE_NAME } from 'src/content/app/species-selector/services/speciesSelectorStorageConstants';
import { GB_TRACK_SETTINGS_STORE_NAME } from 'src/content/app/genome-browser/services/track-settings/trackSettingsStorageConstants';
import { GB_FOCUS_OBJECTS_STORE_NAME } from 'src/content/app/genome-browser/services/focus-objects/focusObjectStorageConstants';
import { BLAST_SUBMISSIONS_STORE_NAME } from 'src/content/app/tools/blast/services/blastStorageServiceConstants';
import { PREVIOUSLY_VIEWED_OBJECTS_STORE_NAME } from 'src/shared/services/previouslyViewedObjectsStorageConstants';

const DB_NAME = 'ensembl-website';
const DB_VERSION = 3;
const DB_VERSION = 4;

const getDbPromise = () => {
return openDB(DB_NAME, DB_VERSION, {
Expand All @@ -32,6 +33,9 @@ const getDbPromise = () => {
if (!db.objectStoreNames.contains('contact-forms')) {
db.createObjectStore('contact-forms');
}
if (!db.objectStoreNames.contains(GENERAL_UI_STORE_NAME)) {
db.createObjectStore(GENERAL_UI_STORE_NAME);
}
if (!db.objectStoreNames.contains(SELECTED_SPECIES_STORE_NAME)) {
db.createObjectStore(SELECTED_SPECIES_STORE_NAME);
}
Expand Down
2 changes: 1 addition & 1 deletion src/shared/components/selected-species/SpeciesLozenge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from '../species-name-parts';

import type { CommittedItem } from 'src/content/app/species-selector/types/committedItem';
import type { SpeciesNameDisplayOption } from 'src/content/app/species-selector/state/species-selector-general-slice/speciesSelectorGeneralSlice';
import type { SpeciesNameDisplayOption } from 'src/content/app/species-selector/types/speciesNameDisplayOption';

import styles from './SpeciesLozenge.module.css';

Expand Down
19 changes: 19 additions & 0 deletions src/shared/services/generalUIStorageConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export const GENERAL_UI_STORE_NAME = 'general-ui';

export const SPECIES_NAME_DISPLAY_OPTION_KEY = 'species-name-display-option';
112 changes: 112 additions & 0 deletions src/shared/services/generalUIStorageService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import 'fake-indexeddb/auto';
import { openDB } from 'idb';

import IndexedDB from 'src/services/indexeddb-service';

import {
GENERAL_UI_STORE_NAME,
SPECIES_NAME_DISPLAY_OPTION_KEY
} from './generalUIStorageConstants';
import { speciesNameDisplayOptions } from 'src/content/app/species-selector/constants/speciesNameDisplayConstants';

import {
saveSpeciesNameDisplayOption,
getSpeciesNameDisplayOption
} from './generalUIStorageService';

const getDatabase = async () => {
return await openDB('test-db', 1, {
upgrade(db) {
db.createObjectStore(GENERAL_UI_STORE_NAME);
}
});
};

jest.spyOn(IndexedDB, 'getDB').mockImplementation(() => getDatabase());

afterEach(async () => {
await IndexedDB.clear(GENERAL_UI_STORE_NAME);
});

describe('generalUIStorageService', () => {
describe('storing and retrieving species name display option', () => {
test('saving species name display option', async () => {
const displayOption = speciesNameDisplayOptions[1]; // a non-default display option
await saveSpeciesNameDisplayOption(displayOption);

// now read back the stored option
const retrievedOption = await IndexedDB.get(
GENERAL_UI_STORE_NAME,
SPECIES_NAME_DISPLAY_OPTION_KEY
);
expect(retrievedOption).toEqual(displayOption);
});

test('reading species name display option', async () => {
const displayOption = speciesNameDisplayOptions[1]; // a non-default display option

// write species name display option to indexed db directly, without using the service
await IndexedDB.set(
GENERAL_UI_STORE_NAME,
SPECIES_NAME_DISPLAY_OPTION_KEY,
displayOption
);

const retrievedOption = await getSpeciesNameDisplayOption();
expect(retrievedOption).toEqual(displayOption);
});

test('updating species name display option', async () => {
const displayOption1 = speciesNameDisplayOptions[0];
const displayOption2 = speciesNameDisplayOptions[1];

await saveSpeciesNameDisplayOption(displayOption1);
await saveSpeciesNameDisplayOption(displayOption2);

// now read back the stored option
const retrievedOption = await IndexedDB.get(
GENERAL_UI_STORE_NAME,
SPECIES_NAME_DISPLAY_OPTION_KEY
);
expect(retrievedOption).toEqual(displayOption2);
});

test('reading an empty option', async () => {
// nothing has been written to the db
const retrievedOption = await getSpeciesNameDisplayOption();
expect(retrievedOption).toBe(null);
});

test('discarding invalid species name display option', async () => {
// Suppose db stores an outdated option that we do not know how to handle anymore.
// In this case, the service should just return null, as if no option were written to the db
const invalidDisplayOption = 'foo';

// write the display option to indexed db directly, without using the service
await IndexedDB.set(
GENERAL_UI_STORE_NAME,
SPECIES_NAME_DISPLAY_OPTION_KEY,
invalidDisplayOption
);

const retrievedOption = await getSpeciesNameDisplayOption();
expect(retrievedOption).toEqual(null);
});
});
});
50 changes: 50 additions & 0 deletions src/shared/services/generalUIStorageService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import IndexedDB from 'src/services/indexeddb-service';

import {
GENERAL_UI_STORE_NAME as STORE_NAME,
SPECIES_NAME_DISPLAY_OPTION_KEY
} from './generalUIStorageConstants';
import { speciesNameDisplayOptions } from 'src/content/app/species-selector/constants/speciesNameDisplayConstants';

import type { SpeciesNameDisplayOption } from 'src/content/app/species-selector/types/speciesNameDisplayOption';

export const saveSpeciesNameDisplayOption = async (
option: SpeciesNameDisplayOption
) => {
await IndexedDB.set(STORE_NAME, SPECIES_NAME_DISPLAY_OPTION_KEY, option);
};

export const getSpeciesNameDisplayOption = async () => {
const savedOption = await IndexedDB.get(
STORE_NAME,
SPECIES_NAME_DISPLAY_OPTION_KEY
);

if (savedOption && isValidSpeciesNameDisplayOption(savedOption)) {
return savedOption;
} else {
return null;
}
};

const isValidSpeciesNameDisplayOption = (
option: string
): option is SpeciesNameDisplayOption => {
return speciesNameDisplayOptions.includes(option as any);
};

0 comments on commit b819009

Please sign in to comment.