Skip to content

Commit

Permalink
Merge pull request #206 from microbiomedata/issue-150-slot-visibility…
Browse files Browse the repository at this point in the history
…-per-study

Store slot visibility settings on a per-Study basis
  • Loading branch information
pkalita-lbl authored Dec 9, 2024
2 parents aacc500 + afb2376 commit 236b84c
Show file tree
Hide file tree
Showing 29 changed files with 926 additions and 462 deletions.
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import "@ionic/react/css/palettes/dark.class.css";
/* Theme variables */
import "./theme/variables.css";

/* Our own global styles */
import "./theme/global.css";

setupIonicReact();

const App: React.FC = () => {
Expand Down
6 changes: 0 additions & 6 deletions src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import GuidePage from "./pages/GuidePage/GuidePage";
import SettingsPage from "./pages/SettingsPage/SettingsPage";
import AppUrlListener from "./components/AppUrlListener/AppUrlListener";
import RootPage from "./pages/RootPage/RootPage";
import FieldVisibilitySettingsPage from "./pages/FieldVisibilitySettingsPage/FieldVisibilitySettingsPage";
import paths, { IN } from "./paths";

const Router: React.FC = () => {
Expand Down Expand Up @@ -78,11 +77,6 @@ const Router: React.FC = () => {
<StudyCreatePage />
</AuthRoute>

{/* SETTINGS ROUTES */}
<AuthRoute exact path={paths.fieldVisibilitySettings}>
<FieldVisibilitySettingsPage />
</AuthRoute>

<Route exact path={paths.root}>
<RootPage />
</Route>
Expand Down
76 changes: 1 addition & 75 deletions src/Store.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,7 @@ import userEvent from "@testing-library/user-event";
import { server, tokenExchangeError } from "./mocks/server";

const TestStoreConsumer: React.FC = () => {
const {
login,
logout,
isLoggedIn,
loggedInUser,
store,
getHiddenSlotsForSchemaClass,
setHiddenSlotsForSchemaClass,
} = useStore();
const { login, logout, isLoggedIn, loggedInUser, store } = useStore();

const handleLoginClick = async () => {
await login("access-token", "refresh-token");
Expand All @@ -25,9 +17,6 @@ const TestStoreConsumer: React.FC = () => {
await logout();
};

const soilHiddenSlots = getHiddenSlotsForSchemaClass("soil");
const waterHiddenSlots = getHiddenSlotsForSchemaClass("water");

return (
<>
<div data-testid="store-status">
Expand All @@ -42,23 +31,6 @@ const TestStoreConsumer: React.FC = () => {
<button data-testid="logout-button" onClick={handleLogoutClick}>
Log out
</button>

<div data-testid="hidden-slots-soil">
{soilHiddenSlots === undefined
? "undefined"
: soilHiddenSlots.join(", ")}
</div>
<div data-testid="hidden-slots-water">
{waterHiddenSlots === undefined
? "undefined"
: waterHiddenSlots.join(", ")}
</div>
<button
data-testid="set-hidden-slots-soil"
onClick={() => setHiddenSlotsForSchemaClass("soil", ["slot1", "slot2"])}
>
Set soil slots
</button>
</>
);
};
Expand All @@ -84,9 +56,6 @@ const renderTestStoreConsumer = () => {
loggedInUser: screen.getByTestId("logged-in-user"),
loginButton: screen.getByTestId("login-button"),
logoutButton: screen.getByTestId("logout-button"),
hiddenSlotsSoil: screen.getByTestId("hidden-slots-soil"),
hiddenSlotsWater: screen.getByTestId("hidden-slots-water"),
setHiddenSlotsSoil: screen.getByTestId("set-hidden-slots-soil"),
},
};
};
Expand Down Expand Up @@ -197,47 +166,4 @@ describe("Store", () => {
expect(elements.loggedInUser.textContent).toBe("");
expect(setTokensSpy).not.toHaveBeenCalled();
});

it("getHiddenSlotsForSchemaClass should return undefined by default", async () => {
const { elements } = renderTestStoreConsumer();

await waitFor(() =>
expect(elements.storeStatus.textContent).toBe("store created"),
);
expect(elements.hiddenSlotsWater.textContent).toBe("undefined");
expect(elements.hiddenSlotsSoil.textContent).toBe("undefined");
});

it("getHiddenSlotsForSchemaClass should return values from storage if defined", async () => {
window.localStorage.setItem(
"nmdc_field_notes/app_store/hiddenSlots",
'{"soil":["slotA","slotB"]}',
);

const { elements } = renderTestStoreConsumer();

await waitFor(() =>
expect(elements.storeStatus.textContent).toBe("store created"),
);
expect(elements.hiddenSlotsSoil.textContent).toBe("slotA, slotB");
expect(elements.hiddenSlotsWater.textContent).toBe("undefined");
});

it("setHiddenSlotsForSchemaClass should update the store", async () => {
const { elements, user } = renderTestStoreConsumer();

await waitFor(() =>
expect(elements.storeStatus.textContent).toBe("store created"),
);
expect(elements.hiddenSlotsSoil.textContent).toBe("undefined");
expect(elements.hiddenSlotsWater.textContent).toBe("undefined");

await user.click(elements.setHiddenSlotsSoil);

expect(elements.hiddenSlotsSoil.textContent).toBe("slot1, slot2");
expect(elements.hiddenSlotsWater.textContent).toBe("undefined");
expect(
window.localStorage.getItem("nmdc_field_notes/app_store/hiddenSlots"),
).toBe('{"soil":["slot1","slot2"]}');
});
});
59 changes: 0 additions & 59 deletions src/Store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import {
ColorPaletteMode,
isValidColorPaletteMode,
} from "./theme/colorPalette";
import { produce } from "immer";
import { Network } from "@capacitor/network";
import { TourId } from "./components/AppTourProvider/AppTourProvider";

enum StorageKey {
REFRESH_TOKEN = "refreshToken",
LOGGED_IN_USER = "loggedInUser",
COLOR_PALETTE_MODE = "colorPaletteMode",
HIDDEN_SLOTS = "hiddenSlots",
PRESENTED_TOUR_IDS = "presentedTourIds",
}

Expand All @@ -35,12 +33,6 @@ interface StoreContextValue {
colorPaletteMode: ColorPaletteMode | null;
setColorPaletteMode: (colorPaletteMode: ColorPaletteMode) => void;

getHiddenSlotsForSchemaClass: (className: string) => string[] | undefined;
setHiddenSlotsForSchemaClass: (
className: string,
hiddenSlots: string[],
) => void;

checkWhetherTourHasBeenPresented: (tourId: TourId) => boolean;
rememberTourHasBeenPresented: (tourId: TourId | null) => void;
forgetTourHasBeenPresented: (tourId: TourId | null) => void;
Expand All @@ -63,13 +55,6 @@ const StoreContext = createContext<StoreContextValue>({
throw new Error("setColorPaletteMode called outside of provider");
},

getHiddenSlotsForSchemaClass: () => {
throw new Error("getHiddenSlotsForSchemaClass called outside of provider");
},
setHiddenSlotsForSchemaClass: () => {
throw new Error("setHiddenSlotsForSchemaClass called outside of provider");
},

checkWhetherTourHasBeenPresented: () => {
throw new Error(
"checkWhetherTourHasBeenPresented called outside of provider",
Expand All @@ -89,7 +74,6 @@ const StoreProvider: React.FC<PropsWithChildren> = ({ children }) => {
const [loggedInUser, setLoggedInUser] = useState<User | null>(null);
const [colorPaletteMode, setColorPaletteMode] =
useState<ColorPaletteMode | null>(null);
const [hiddenSlots, setHiddenSlots] = useState<Record<string, string[]>>({});
const [presentedTourIds, setPresentedTourIds] = useState<Set<TourId>>(
new Set(),
);
Expand Down Expand Up @@ -150,12 +134,6 @@ const StoreProvider: React.FC<PropsWithChildren> = ({ children }) => {
setColorPaletteMode(sanitizedColorPaletteMode);
applyColorPalette(sanitizedColorPaletteMode);

// If persistent storage contains hidden slots, load them into the Context.
const hiddenSlotsFromStorage = await storage.get(StorageKey.HIDDEN_SLOTS);
if (hiddenSlotsFromStorage) {
setHiddenSlots(hiddenSlotsFromStorage);
}

// If persistent storage contains presented tour IDs, load them into the Context.
//
// Note: If we were storing our `Set` into browser storage directly, we'd have to convert it into an array first.
Expand Down Expand Up @@ -255,40 +233,6 @@ const StoreProvider: React.FC<PropsWithChildren> = ({ children }) => {
}
}

/**
* Returns a list of hidden slot names for the specified schema class or undefined if the given
* class name is not in the hidden slots map yet. The implication is that `undefined` means the
* user has not made any choice about which slots to hide for this schema class yet. Whereas an
* empty array means the user has made a choice to hide no slots for this schema class.
*/
function getHiddenSlotsForSchemaClass(
className: string,
): string[] | undefined {
return hiddenSlots[className];
}

/**
* Updates the hidden slots for the specified schema class in the Context and the store.
*/
async function setHiddenSlotsForSchemaClass(
className: string,
slotNames: string[],
) {
const updatedHiddenSlots = produce(hiddenSlots, (draft) => {
draft[className] = slotNames;
});

setHiddenSlots(updatedHiddenSlots);
if (store === null) {
console.warn(
"setHiddenSlotsForSchemaClass called before storage initialization",
);
return;
} else {
return store.set(StorageKey.HIDDEN_SLOTS, updatedHiddenSlots);
}
}

/**
* Returns `true` if the specified tour has been presented; otherwise returns `false`.
*
Expand Down Expand Up @@ -357,9 +301,6 @@ const StoreProvider: React.FC<PropsWithChildren> = ({ children }) => {
colorPaletteMode: colorPaletteMode,
setColorPaletteMode: _setColorPaletteMode,

getHiddenSlotsForSchemaClass,
setHiddenSlotsForSchemaClass,

checkWhetherTourHasBeenPresented,
rememberTourHasBeenPresented,
forgetTourHasBeenPresented,
Expand Down
30 changes: 23 additions & 7 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export interface TemplateInfo {
schemaClass: string;
sampleDataSlot: string;
}
export const TEMPLATES: Record<string, TemplateInfo> = {
export const TEMPLATES = {
air: {
displayName: "air",
schemaClass: "AirInterface",
Expand Down Expand Up @@ -148,10 +148,13 @@ export const TEMPLATES: Record<string, TemplateInfo> = {
schemaClass: "WaterInterface",
sampleDataSlot: "water_data",
},
};
} as const;
export type TemplateName = keyof typeof TEMPLATES;

export type SlotName = string;

export interface MetadataSubmission {
packageName: keyof typeof TEMPLATES;
packageName: TemplateName | "";
contextForm: ContextForm;
addressForm: AddressForm;
templates: string[];
Expand Down Expand Up @@ -180,10 +183,15 @@ export interface SubmissionMetadataCreate extends SubmissionMetadataBase {
export interface SubmissionMetadataUpdate extends SubmissionMetadataBase {
id: string;
status?: string;
field_notes_metadata?: Nullable<FieldNotesMetadata>;
// Map of ORCID iD to permission level
permissions?: Record<string, string>;
}

export interface FieldNotesMetadata {
fieldVisibility?: Partial<Record<TemplateName, SlotName[]>>;
}

export interface SubmissionMetadata extends SubmissionMetadataCreate {
status: string;
id: string;
Expand All @@ -193,6 +201,7 @@ export interface SubmissionMetadata extends SubmissionMetadataCreate {
lock_updated?: string;
locked_by: Nullable<User>;
permission_level?: string;
field_notes_metadata?: Nullable<FieldNotesMetadata>;
}

export interface PaginationOptions {
Expand Down Expand Up @@ -289,6 +298,11 @@ export class FetchClient {
}
}

interface GetSubmissionListOptions extends PaginationOptions {
column_sort?: string;
sort_order?: "asc" | "desc";
}

class NmdcServerClient extends FetchClient {
private refreshToken: string | null = null;
private exchangeRefreshTokenCache: Promise<TokenResponse> | null = null;
Expand Down Expand Up @@ -341,13 +355,15 @@ class NmdcServerClient extends FetchClient {
}
}

async getSubmissionList(pagination: PaginationOptions = {}) {
pagination = {
async getSubmissionList(options: GetSubmissionListOptions = {}) {
options = {
limit: 10,
offset: 0,
...pagination,
column_sort: "created",
sort_order: "desc",
...options,
};
const query = new URLSearchParams(pagination as Record<string, string>);
const query = new URLSearchParams(options as Record<string, string>);
return await this.fetchJson<Paginated<SubmissionMetadata>>(
`/api/metadata_submission?${query}`,
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Banner/Banner.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ ion-list.banner {

& ion-item {
--min-height: auto;
font-size: 0.875rem;
font-size: var(--nmdc-font-size-sm);
}

& ion-button {
Expand Down
3 changes: 1 addition & 2 deletions src/components/Checklist/Checklist.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.info {
padding: 0px 15px 0px 15px;
font-size: 0.875rem;
font-size: var(--nmdc-font-size-sm);
opacity: 0.6;
}

Expand All @@ -11,7 +11,6 @@
}

.listIcon {
position: absolute;
position: absolute;
top: 5px;
left: 15px;
Expand Down
5 changes: 3 additions & 2 deletions src/components/Checklist/md-in-js/util.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import config from "../../../config";
import { TEMPLATES } from "../../../api";
import { TemplateName, TEMPLATES } from "../../../api";

// Generate schemas Markdown from the TEMPLATES
export function createSchemasMD() {
let schemasMD = "";

Object.keys(TEMPLATES).forEach((schema) => {
schemasMD += `* [${TEMPLATES[schema].displayName}](${config.NMDC_SUBMISSION_SCHEMA_DOCS_BASE_URL}/${TEMPLATES[schema].schemaClass}/)\n`;
const template = TEMPLATES[schema as TemplateName];
schemasMD += `* [${template.displayName}](${config.NMDC_SUBMISSION_SCHEMA_DOCS_BASE_URL}/${template.schemaClass}/)\n`;
});

return schemasMD;
Expand Down
Loading

0 comments on commit 236b84c

Please sign in to comment.