Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 9 additions & 9 deletions src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { Select } from "antd";
import { map } from "lodash-es";
import { Dictionary, PackingInputs } from "../../types";

interface DropdownProps {
placeholder: string;
defaultValue?: string;
options: Dictionary<PackingInputs>;
onChange: (value: string) => void;
}

const Dropdown = (props: DropdownProps): JSX.Element => {
const { placeholder, options, onChange } = props;
const selectOptions = Object.entries(options).map(([key]) => (
{
label: <span>{key}</span>,
value: key,
}
));
const { placeholder, options, onChange, defaultValue } = props;
const selectOptions = map(options, (opt, key) => ({
label: opt.name || key,
value: opt.recipe,
}));

return (
<Select
defaultValue={undefined}
defaultValue={defaultValue}
onChange={onChange}
placeholder={placeholder}
options={selectOptions}
Expand All @@ -27,4 +27,4 @@ const Dropdown = (props: DropdownProps): JSX.Element => {
);
};

export default Dropdown;
export default Dropdown;
7 changes: 4 additions & 3 deletions src/components/PackingInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useInputOptions,
useIsLoading,
useLoadInputOptions,
useSelectInput,
useSelectRecipe,
useUpdateRecipeString,
useStartPacking,
useLoadAllRecipes,
Expand Down Expand Up @@ -36,7 +36,7 @@ const PackingInput = (props: PackingInputProps): JSX.Element => {

const loadInputOptions = useLoadInputOptions();
const loadAllRecipes = useLoadAllRecipes();
const selectInput = useSelectInput();
const selectRecipe = useSelectRecipe();
const updateRecipeString = useUpdateRecipeString();
const storeStartPacking = useStartPacking();

Expand Down Expand Up @@ -69,9 +69,10 @@ const PackingInput = (props: PackingInputProps): JSX.Element => {
<div className="recipe-select">
<div>Choose Recipe</div>
<Dropdown
defaultValue={selectedRecipeId}
placeholder="Select an option"
options={inputOptions}
onChange={selectInput}
onChange={selectRecipe}
/>
</div>
<Tabs defaultActiveKey="1" className="recipe-content">
Expand Down
125 changes: 73 additions & 52 deletions src/state/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import { get as lodashGet, set as lodashSet } from 'lodash-es';
import { get as lodashGet, set as lodashSet } from "lodash-es";
import { PackingInputs } from "../types";
import { getFirebaseRecipe, jsonToString } from "../utils/recipeLoader";
import { getPackingInputsDict } from "../utils/firebase";
Expand All @@ -13,7 +13,6 @@ export interface RecipeData {
}

export interface RecipeState {
selectedInputName: string;
selectedRecipeId: string;
inputOptions: Record<string, PackingInputs>;
recipes: Record<string, RecipeData>;
Expand All @@ -27,23 +26,31 @@ export interface UIState {
type Actions = {
loadInputOptions: () => Promise<void>;
loadAllRecipes: () => Promise<void>;
selectInput: (inputName: string) => Promise<void>;
selectRecipe: (inputName: string) => Promise<void>;
loadRecipe: (recipeId: string) => Promise<void>;
updateRecipeString: (recipeId: string, newString: string) => void;
updateRecipeObj: (recipeId: string, updates: Record<string, string | number>) => void;
updateRecipeObj: (
recipeId: string,
updates: Record<string, string | number>
) => void;
restoreRecipeDefault: (recipeId: string) => void;
getCurrentValue: (path: string) => string | number | undefined;
getOriginalValue: (path: string) => string | number | undefined;
startPacking: (
callback: (recipeId: string, configId: string, recipeString: string) => Promise<void>
callback: (
recipeId: string,
configId: string,
recipeString: string
) => Promise<void>
) => Promise<void>;
};

export type RecipeStore = RecipeState & UIState & Actions;

const INITIAL_RECIPE_ID = "peroxisome_v_gradient_packing";

const initialState: RecipeState & UIState = {
selectedInputName: "",
selectedRecipeId: "",
selectedRecipeId: INITIAL_RECIPE_ID,
inputOptions: {},
recipes: {},
isLoading: false,
Expand All @@ -64,12 +71,11 @@ export const useRecipeStore = create<RecipeStore>()(
}
},


loadRecipe: async (recipeId) => {
if (get().recipes[recipeId]) return;
const recJson = await getFirebaseRecipe(recipeId);
const recStr = jsonToString(recJson);
set(s => ({
set((s) => ({
recipes: {
...s.recipes,
[recipeId]: {
Expand All @@ -80,42 +86,48 @@ export const useRecipeStore = create<RecipeStore>()(
},
},
}));

},

loadAllRecipes: async () => {
const { inputOptions, recipes, loadRecipe } = get();

const ids = new Set<string>();
Object.values(inputOptions).forEach(opt => { if (opt?.recipe) ids.add(opt.recipe); });
const missing = [...ids].filter(id => !recipes[id]);
if (!missing.length) return;

Object.values(inputOptions).forEach((opt) => {
if (opt?.recipe) ids.add(opt.recipe);
});
const recipesToLoad = [...ids].filter((id) => !recipes[id]);
if (!recipesToLoad.length) return;
set({ isLoading: true });
try {
await Promise.all(missing.map(id => loadRecipe(id)));
await Promise.all(recipesToLoad.map((id) => loadRecipe(id)));
} finally {
set({ isLoading: false });
}
let recipeToLoad = INITIAL_RECIPE_ID;
if (!get().recipes[INITIAL_RECIPE_ID]) {
console.warn(
`Initial recipe ID ${INITIAL_RECIPE_ID} not found, selecting first available recipe.`
);
recipeToLoad = Object.keys(get().recipes)[0];
}
get().selectRecipe(recipeToLoad);
},

selectInput: async (inputName) => {
const sel = get().inputOptions[inputName];
selectRecipe: async (recipeId) => {
const sel = get().inputOptions[recipeId];
if (!sel) return;

set({
selectedInputName: inputName,
selectedRecipeId: sel.recipe ?? "",
selectedRecipeId: recipeId,
});

if (sel.recipe && !get().recipes[sel.recipe]) {
await get().loadRecipe(sel.recipe);
}
},


updateRecipeString: (recipeId, newString) => {
set(s => {
set((s) => {
const rec = s.recipes[recipeId];
if (!rec) return s;
return {
Expand All @@ -141,10 +153,13 @@ export const useRecipeStore = create<RecipeStore>()(
for (const [path, value] of Object.entries(updates)) {
lodashSet(obj, path, value);
}
get().updateRecipeString(recipeId, JSON.stringify(obj, null, 2));
get().updateRecipeString(
recipeId,
JSON.stringify(obj, null, 2)
);
} catch {
// TODO: better error handling
console.warn("Failed to update recipe object")
console.warn("Failed to update recipe object");
}
},

Expand All @@ -153,28 +168,28 @@ export const useRecipeStore = create<RecipeStore>()(
if (rec) get().updateRecipeString(recipeId, rec.originalString);
},


getCurrentValue: (path) => {
const { selectedRecipeId, recipes } = get();
const str = recipes[selectedRecipeId]?.currentString;
if (!str) return undefined;
try {
const obj = JSON.parse(str);
const v = lodashGet(obj, path);
return typeof v === "string" || typeof v === "number" ? v : undefined;
return typeof v === "string" || typeof v === "number"
? v
: undefined;
} catch {
console.warn("Failed to retrieve value.")
console.warn("Failed to retrieve value.");
return undefined;
}
},



startPacking: async (callback) => {
const s = get();
const input = s.inputOptions[s.selectedInputName];
const input = s.inputOptions[s.selectedRecipeId];
const configId = input?.config ?? "";
const recipeString = s.recipes[s.selectedRecipeId]?.currentString ?? "";
const recipeString =
s.recipes[s.selectedRecipeId]?.currentString ?? "";
set({ isPacking: true });
try {
await callback(s.selectedRecipeId, configId, recipeString);
Expand All @@ -190,37 +205,43 @@ export const useRecipeStore = create<RecipeStore>()(
try {
const obj = JSON.parse(str);
const v = lodashGet(obj, path);
return typeof v === "string" || typeof v === "number" ? v : undefined;
return typeof v === "string" || typeof v === "number"
? v
: undefined;
} catch {
console.warn("Failed to retrieve default value.")
console.warn("Failed to retrieve default value.");
return undefined;
}
},

})),


}))
);

// tiny helpers/selectors (all derived — not stored)
export const useSelectedRecipeId = () => useRecipeStore(s => s.selectedRecipeId);
export const useSelectedRecipeId = () =>
useRecipeStore((s) => s.selectedRecipeId);
export const useCurrentRecipeString = () =>
useRecipeStore(s => s.recipes[s.selectedRecipeId]?.currentString ?? "");
export const useInputOptions = () => useRecipeStore(s => s.inputOptions);
export const useIsLoading = () => useRecipeStore(s => s.isLoading);
export const useIsPacking = () => useRecipeStore(s => s.isPacking);
useRecipeStore((s) => s.recipes[s.selectedRecipeId]?.currentString ?? "");
export const useInputOptions = () => useRecipeStore((s) => s.inputOptions);
export const useIsLoading = () => useRecipeStore((s) => s.isLoading);
export const useIsPacking = () => useRecipeStore((s) => s.isPacking);
export const useFieldsToDisplay = () =>
useRecipeStore(s => s.inputOptions[s.selectedInputName]?.editable_fields);
useRecipeStore((s) => s.inputOptions[s.selectedRecipeId]?.editable_fields);
export const useIsCurrentRecipeModified = () =>
useRecipeStore(s => s.recipes[s.selectedRecipeId]?.isModified ?? false);
export const useGetOriginalValue = () => useRecipeStore(s => s.getOriginalValue);
useRecipeStore((s) => s.recipes[s.selectedRecipeId]?.isModified ?? false);
export const useGetOriginalValue = () =>
useRecipeStore((s) => s.getOriginalValue);

// action selectors (stable identities)
export const useLoadInputOptions = () => useRecipeStore(s => s.loadInputOptions);
export const useLoadAllRecipes = () => useRecipeStore(s => s.loadAllRecipes);
export const useSelectInput = () => useRecipeStore(s => s.selectInput);
export const useUpdateRecipeObj = () => useRecipeStore(s => s.updateRecipeObj);
export const useUpdateRecipeString = () => useRecipeStore(s => s.updateRecipeString);
export const useRestoreRecipeDefault = () => useRecipeStore(s => s.restoreRecipeDefault);
export const useStartPacking = () => useRecipeStore(s => s.startPacking);
export const useGetCurrentValue = () => useRecipeStore(s => s.getCurrentValue);
export const useLoadInputOptions = () =>
useRecipeStore((s) => s.loadInputOptions);
export const useLoadAllRecipes = () => useRecipeStore((s) => s.loadAllRecipes);
export const useSelectRecipe = () => useRecipeStore((s) => s.selectRecipe);
export const useUpdateRecipeObj = () =>
useRecipeStore((s) => s.updateRecipeObj);
export const useUpdateRecipeString = () =>
useRecipeStore((s) => s.updateRecipeString);
export const useRestoreRecipeDefault = () =>
useRecipeStore((s) => s.restoreRecipeDefault);
export const useStartPacking = () => useRecipeStore((s) => s.startPacking);
export const useGetCurrentValue = () =>
useRecipeStore((s) => s.getCurrentValue);
Loading