(existingRecipeId ?? 'pnw');
+
+ const handleChangeRecipeId = (nextId: string) => {
+ setRecipeId(nextId);
+ dispatch(updateRoundExtensionData(round.id, { ...(existingRoundConfig ?? {}), recipe: { id: nextId } }));
+ };
+
+ const handleRunRecipe = () => {
+ dispatch(runRecipeAction(round.id, recipeId));
+ };
+
if (roundActivities.length === 0) {
return (
@@ -98,6 +116,9 @@ const RoundContainer = ({ roundId, activityCode, eventId, round }: RoundContaine
activityCode={activityCode}
onConfigureAssignments={() => dialogs.configureAssignments.setOpen(true)}
onGenerateAssignments={handleGenerateAssignments}
+ recipeId={recipeId}
+ onChangeRecipeId={handleChangeRecipeId}
+ onRunRecipe={handleRunRecipe}
onConfigureStationNumbers={(code) =>
dialogs.configureStationNumbers.setActivityCode(code)
}
diff --git a/src/store/actions.ts b/src/store/actions.ts
index 729e486..60cb951 100644
--- a/src/store/actions.ts
+++ b/src/store/actions.ts
@@ -46,6 +46,7 @@ export const ActionType = {
PARTIAL_UPDATE_WCIF: 'partial_update_wcif',
RESET_ALL_GROUP_ASSIGNMENTS: 'reset_all_group_assignments',
GENERATE_ASSIGNMENTS: 'generate_assignments',
+ RUN_RECIPE: 'run_recipe',
EDIT_ACTIVITY: 'edit_activity',
UPDATE_GLOBAL_EXTENSION: 'update_global_extension',
ADD_PERSON: 'add_person',
@@ -365,6 +366,20 @@ export type GenerateAssignmentsPayload = {
* @param {ActivityCode} roundId
* @returns
*/
+
+export type RunRecipePayload = {
+ roundId: string;
+ recipeId: string;
+};
+export const runRecipe = (
+ roundId: string,
+ recipeId: string
+): ReduxAction => ({
+ type: ActionType.RUN_RECIPE,
+ roundId,
+ recipeId,
+});
+
export const generateAssignments = (
roundId: string,
options?: Partial
diff --git a/src/store/reducerHandlers.ts b/src/store/reducerHandlers.ts
index 44168f7..6f04ce5 100644
--- a/src/store/reducerHandlers.ts
+++ b/src/store/reducerHandlers.ts
@@ -170,6 +170,7 @@ export const reducers: Record = {
};
},
[ActionType.GENERATE_ASSIGNMENTS]: Reducers.generateAssignments,
+ [ActionType.RUN_RECIPE]: Reducers.runRecipe,
[ActionType.EDIT_ACTIVITY]: (state, action: EditActivityPayload) => {
if (!('where' in action && 'what' in action) || !state.wcif) return state;
const { where, what } = action;
diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts
index af3bc42..1c899a8 100644
--- a/src/store/reducers/index.ts
+++ b/src/store/reducers/index.ts
@@ -1,3 +1,5 @@
export * from './generateAssignments';
export * from './competitorAssignments';
export * from './persons';
+
+export * from './runRecipe';
diff --git a/src/store/reducers/runRecipe.ts b/src/store/reducers/runRecipe.ts
new file mode 100644
index 0000000..d65b8b5
--- /dev/null
+++ b/src/store/reducers/runRecipe.ts
@@ -0,0 +1,84 @@
+import { type Competition } from '@wca/helpers';
+import { Constraints, Generators } from 'wca-group-generators';
+import { findRoundActivitiesById } from '../../lib/wcif/activities';
+import { createGroupsAcrossStages } from '../../lib/wcif/groups';
+import { Recipes, fromRecipeDefinition, hydrateStep } from '../../lib/recipes';
+import { mapIn } from '../../lib/utils/utils';
+import { type AppState } from '../initialState';
+import type { RunRecipePayload } from '../actions';
+
+/**
+ * Run a built-in recipe to generate groups and/or assignments for a round.
+ * Restores the legacy "recipe" workflow based on wca-group-generators.
+ */
+export function runRecipe(state: AppState, action: RunRecipePayload): AppState {
+ if (!state.wcif) return state;
+
+ const wcif = state.wcif as unknown as Competition;
+ const recipeDef = Recipes.find((r) => r.id === action.recipeId);
+ if (!recipeDef) {
+ throw new Error(`Recipe ${action.recipeId} not found`);
+ }
+
+ const recipe = fromRecipeDefinition(recipeDef, { wcif, activityCode: action.roundId });
+
+ const updatedWcif = recipe.steps.reduce((accWcif, step) => {
+ if (step.type === 'assignments') {
+ const generator = (Generators as Record)[step.props.generator];
+ if (!generator) {
+ throw new Error(`Generator ${step.props.generator} not found`);
+ }
+
+ const hydratedStep = hydrateStep(accWcif, action.roundId, step);
+
+ const constraints =
+ hydratedStep.props.constraints?.map((c) => {
+ const constraintFn = (Constraints as Record)[c.constraint];
+ if (!constraintFn) {
+ throw new Error(`Constraint ${c.constraint} not found`);
+ }
+ return {
+ constraint: constraintFn,
+ weight: c.weight,
+ };
+ }) ?? [];
+
+ return generator.execute({
+ wcif: accWcif,
+ roundId: action.roundId,
+ ...hydratedStep.props,
+ constraints,
+ }) as Competition;
+ }
+
+ if (step.type === 'groups') {
+ const roundActivities = findRoundActivitiesById(accWcif, action.roundId);
+ const roundActivitiesWithGroups = createGroupsAcrossStages(accWcif, roundActivities, {
+ spreadGroupsAcrossAllStages: true,
+ groups: step.props.count,
+ });
+
+ return {
+ ...accWcif,
+ schedule: mapIn(accWcif.schedule, 'venues', (venue) =>
+ mapIn(venue, 'rooms', (room) =>
+ mapIn(room, 'activities', (activity) =>
+ roundActivitiesWithGroups.find((ra) => ra.id === activity.id)
+ ? (roundActivitiesWithGroups.find((ra) => ra.id === activity.id) as any)
+ : activity
+ )
+ )
+ ),
+ } as Competition;
+ }
+
+ return accWcif;
+ }, wcif);
+
+ return {
+ ...state,
+ needToSave: true,
+ changedKeys: new Set([...state.changedKeys, 'schedule', 'persons', 'events']),
+ wcif: updatedWcif,
+ };
+}