Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store slot visibility settings on a per-Study basis #206

Merged
merged 17 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
28 changes: 21 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,11 @@ export const TEMPLATES: Record<string, TemplateInfo> = {
schemaClass: "WaterInterface",
sampleDataSlot: "water_data",
},
};
} as const;
export type TemplateName = keyof typeof TEMPLATES;

export interface MetadataSubmission {
packageName: keyof typeof TEMPLATES;
packageName: TemplateName | "";
contextForm: ContextForm;
addressForm: AddressForm;
templates: string[];
Expand Down Expand Up @@ -180,10 +181,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, string[]>>;
pkalita-lbl marked this conversation as resolved.
Show resolved Hide resolved
}

export interface SubmissionMetadata extends SubmissionMetadataCreate {
status: string;
id: string;
Expand All @@ -193,6 +199,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 +296,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 +353,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
Loading