From ec6227640045ad131070dfb0d09a2fed8db8ced7 Mon Sep 17 00:00:00 2001
From: "I. A. Naval" <790279+ianonavy@users.noreply.github.com>
Date: Mon, 9 Mar 2026 21:41:53 -0400
Subject: [PATCH] feat: Auto choose team from robot and match number
See !108 for the original version of this feature. This PR adds a new
field type TBA-assigned-station and updates the default config to
include the new field.
We update the schema for TBA-team-and-robot robotPosition to be
constrained to the values generated by the current TBATeamAndRobotInput,
which is descended from the "Robot" field from previous year's default
config. These values are not end user configurable, so this should not
break any preexisting configs.
I also added a new pattern for defaulting choices for types that extend
select, including the labels. The gen schema script was not working for
me, so I switched it to npx tsx. If you'd prefer I keep that in a
separate PR, happy to do that.
There was some preexisting drift in the schema relating to "year" that I
decided to leave in. I can also revert that if it's an issue.
Testing methodology:
1. Reset to default config
2. Edit config to change to a team that attended a week 0 event (e.g.
3467)
3. Prefill match data from a past event
4. Select an assigned station; observe the Team and Robot is
auto-selected
5. Update the match number; observe the same Robot and new Team is
auto-selected
---
config/2026/config.json | 11 +-
package.json | 2 +-
public/schema.json | 114 +++++++++++++++++-
src/assets/schema.json | 114 +++++++++++++++++-
src/components/inputs/BaseInputProps.ts | 34 +++++-
src/components/inputs/ConfigurableInput.tsx | 2 +
.../inputs/TBATeamAndRobotInput.tsx | 22 ++--
7 files changed, 277 insertions(+), 22 deletions(-)
diff --git a/config/2026/config.json b/config/2026/config.json
index 9b0ef6306..914b74ec7 100644
--- a/config/2026/config.json
+++ b/config/2026/config.json
@@ -74,6 +74,14 @@
"formResetBehavior": "preserve",
"defaultValue": ""
},
+ {
+ "title": "Assigned Station",
+ "description": "The station assigned to the scout (e.g., R1, R2, R3, B1, B2, B3). Use this to automatically assign the team and robot position based on the match number and station.",
+ "type": "TBA-assigned-station",
+ "required": false,
+ "code": "assignedStation",
+ "formResetBehavior": "preserve"
+ },
{
"title": "Match Number",
"description": "Select the match number.",
@@ -90,7 +98,8 @@
"required": true,
"code": "robot",
"formResetBehavior": "preserve",
- "defaultValue": null
+ "defaultValue": null,
+ "autoAssignFromFieldCode": "assignedStation"
},
{
"title": "Starting Position",
diff --git a/package.json b/package.json
index 20fb5a02b..8f4e31b2f 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"dev": "vite",
"build": "tsc && vite build ",
"preview": "vite preview",
- "schema": "ts-node src/scripts/generateJsonSchema.ts src/assets/schema.json && cp src/assets/schema.json public/schema.json"
+ "schema": "npx tsx src/scripts/generateJsonSchema.ts src/assets/schema.json && cp src/assets/schema.json public/schema.json"
},
"dependencies": {
"@headlessui/react": "^1.7.19",
diff --git a/public/schema.json b/public/schema.json
index fa58f2281..84cc3413a 100644
--- a/public/schema.json
+++ b/public/schema.json
@@ -11,7 +11,7 @@
},
"year": {
"type": "number",
- "description": "The year this scouting config is relevant for."
+ "description": "The year this scouting config is relevant for. Defaults to the current year if not provided."
},
"delimiter": {
"type": "string",
@@ -294,6 +294,45 @@
],
"additionalProperties": false
},
+ {
+ "type": "object",
+ "properties": {
+ "title": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/title"
+ },
+ "description": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/description"
+ },
+ "type": {
+ "type": "string",
+ "const": "multi-counter"
+ },
+ "required": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/required"
+ },
+ "code": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/code"
+ },
+ "disabled": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/disabled"
+ },
+ "formResetBehavior": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/formResetBehavior"
+ },
+ "defaultValue": {
+ "type": "number",
+ "default": 0,
+ "description": "The default value"
+ }
+ },
+ "required": [
+ "title",
+ "type",
+ "required",
+ "code"
+ ],
+ "additionalProperties": false
+ },
{
"type": "object",
"properties": {
@@ -741,6 +780,10 @@
"timerDuration": {
"type": "number",
"description": "Expected duration in seconds (for UI reference, e.g., 15 for auto, 135 for teleop)"
+ },
+ "autoStopSeconds": {
+ "type": "number",
+ "description": "Automatically stop the timer after this many seconds. Useful to prevent the timer from running past the match phase duration."
}
},
"required": [
@@ -786,7 +829,16 @@
"type": "number"
},
"robotPosition": {
- "type": "string"
+ "type": "string",
+ "enum": [
+ "R1",
+ "R2",
+ "R3",
+ "B1",
+ "B2",
+ "B3"
+ ],
+ "description": "The robot position in the alliance. R = Red alliance, B = Blue alliance, 1/2/3 = position on the alliance."
}
},
"required": [
@@ -801,6 +853,10 @@
],
"default": null,
"description": "The default team and robot position"
+ },
+ "autoAssignFromFieldCode": {
+ "type": "string",
+ "description": "Optional code of another field to auto-assign"
}
},
"required": [
@@ -857,6 +913,59 @@
"code"
],
"additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "title": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/title"
+ },
+ "description": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/description"
+ },
+ "type": {
+ "type": "string",
+ "const": "TBA-assigned-station"
+ },
+ "required": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/required"
+ },
+ "code": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/code"
+ },
+ "disabled": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/disabled"
+ },
+ "formResetBehavior": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/formResetBehavior"
+ },
+ "defaultValue": {
+ "type": "string",
+ "default": "",
+ "description": "If assigning by driver station, the driver station to use for selecting team and robot position across matches"
+ },
+ "choices": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "default": {
+ "R1": "Red 1",
+ "R2": "Red 2",
+ "R3": "Red 3",
+ "B1": "Blue 1",
+ "B2": "Blue 2",
+ "B3": "Blue 3"
+ }
+ }
+ },
+ "required": [
+ "title",
+ "type",
+ "required",
+ "code"
+ ],
+ "additionalProperties": false
}
]
}
@@ -876,7 +985,6 @@
"required": [
"title",
"page_title",
- "year",
"delimiter",
"teamNumber",
"sections"
diff --git a/src/assets/schema.json b/src/assets/schema.json
index fa58f2281..84cc3413a 100644
--- a/src/assets/schema.json
+++ b/src/assets/schema.json
@@ -11,7 +11,7 @@
},
"year": {
"type": "number",
- "description": "The year this scouting config is relevant for."
+ "description": "The year this scouting config is relevant for. Defaults to the current year if not provided."
},
"delimiter": {
"type": "string",
@@ -294,6 +294,45 @@
],
"additionalProperties": false
},
+ {
+ "type": "object",
+ "properties": {
+ "title": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/title"
+ },
+ "description": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/description"
+ },
+ "type": {
+ "type": "string",
+ "const": "multi-counter"
+ },
+ "required": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/required"
+ },
+ "code": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/code"
+ },
+ "disabled": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/disabled"
+ },
+ "formResetBehavior": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/formResetBehavior"
+ },
+ "defaultValue": {
+ "type": "number",
+ "default": 0,
+ "description": "The default value"
+ }
+ },
+ "required": [
+ "title",
+ "type",
+ "required",
+ "code"
+ ],
+ "additionalProperties": false
+ },
{
"type": "object",
"properties": {
@@ -741,6 +780,10 @@
"timerDuration": {
"type": "number",
"description": "Expected duration in seconds (for UI reference, e.g., 15 for auto, 135 for teleop)"
+ },
+ "autoStopSeconds": {
+ "type": "number",
+ "description": "Automatically stop the timer after this many seconds. Useful to prevent the timer from running past the match phase duration."
}
},
"required": [
@@ -786,7 +829,16 @@
"type": "number"
},
"robotPosition": {
- "type": "string"
+ "type": "string",
+ "enum": [
+ "R1",
+ "R2",
+ "R3",
+ "B1",
+ "B2",
+ "B3"
+ ],
+ "description": "The robot position in the alliance. R = Red alliance, B = Blue alliance, 1/2/3 = position on the alliance."
}
},
"required": [
@@ -801,6 +853,10 @@
],
"default": null,
"description": "The default team and robot position"
+ },
+ "autoAssignFromFieldCode": {
+ "type": "string",
+ "description": "Optional code of another field to auto-assign"
}
},
"required": [
@@ -857,6 +913,59 @@
"code"
],
"additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "title": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/title"
+ },
+ "description": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/description"
+ },
+ "type": {
+ "type": "string",
+ "const": "TBA-assigned-station"
+ },
+ "required": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/required"
+ },
+ "code": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/code"
+ },
+ "disabled": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/disabled"
+ },
+ "formResetBehavior": {
+ "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/formResetBehavior"
+ },
+ "defaultValue": {
+ "type": "string",
+ "default": "",
+ "description": "If assigning by driver station, the driver station to use for selecting team and robot position across matches"
+ },
+ "choices": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "default": {
+ "R1": "Red 1",
+ "R2": "Red 2",
+ "R3": "Red 3",
+ "B1": "Blue 1",
+ "B2": "Blue 2",
+ "B3": "Blue 3"
+ }
+ }
+ },
+ "required": [
+ "title",
+ "type",
+ "required",
+ "code"
+ ],
+ "additionalProperties": false
}
]
}
@@ -876,7 +985,6 @@
"required": [
"title",
"page_title",
- "year",
"delimiter",
"teamNumber",
"sections"
diff --git a/src/components/inputs/BaseInputProps.ts b/src/components/inputs/BaseInputProps.ts
index f5a33d5fe..32f8dfa99 100644
--- a/src/components/inputs/BaseInputProps.ts
+++ b/src/components/inputs/BaseInputProps.ts
@@ -15,6 +15,7 @@ export const inputTypeSchema = z
'action-tracker',
'TBA-team-and-robot',
'TBA-match-number',
+ 'TBA-assigned-station',
])
.describe('The type of input');
@@ -150,16 +151,35 @@ export const actionTrackerInputSchema = inputBaseSchema.extend({
),
});
+export const robotPosition = z
+ .enum(['R1', 'R2', 'R3', 'B1', 'B2', 'B3'])
+ .describe(
+ 'The robot position in the alliance. R = Red alliance, B = Blue alliance, 1/2/3 = position on the alliance.',
+ );
+
+export const robotPositionLabels = {
+ R1: 'Red 1',
+ R2: 'Red 2',
+ R3: 'Red 3',
+ B1: 'Blue 1',
+ B2: 'Blue 2',
+ B3: 'Blue 3',
+};
+
export const tbaTeamAndRobotInputSchema = inputBaseSchema.extend({
type: z.literal('TBA-team-and-robot'),
defaultValue: z
.object({
teamNumber: z.number(),
- robotPosition: z.string(),
+ robotPosition,
})
.nullable()
.default(null)
.describe('The default team and robot position'),
+ autoAssignFromFieldCode: z
+ .string()
+ .optional()
+ .describe('Optional code of another field to auto-assign'),
});
export const tbaMatchNumberInputSchema = inputBaseSchema.extend({
@@ -169,6 +189,17 @@ export const tbaMatchNumberInputSchema = inputBaseSchema.extend({
defaultValue: z.number().default(0).describe('The default value'),
});
+export const tbaAssignedStationInputSchema = selectInputSchema.extend({
+ type: z.literal('TBA-assigned-station'),
+ choices: z.record(z.string()).default(robotPositionLabels),
+ defaultValue: z
+ .string()
+ .default('')
+ .describe(
+ 'If assigning by driver station, the driver station to use for selecting team and robot position across matches',
+ ),
+});
+
export const sectionSchema = z.object({
name: z.string(),
fields: z.array(
@@ -186,6 +217,7 @@ export const sectionSchema = z.object({
actionTrackerInputSchema,
tbaTeamAndRobotInputSchema,
tbaMatchNumberInputSchema,
+ tbaAssignedStationInputSchema,
]),
),
});
diff --git a/src/components/inputs/ConfigurableInput.tsx b/src/components/inputs/ConfigurableInput.tsx
index 9b4d4a07d..f5dad2656 100644
--- a/src/components/inputs/ConfigurableInput.tsx
+++ b/src/components/inputs/ConfigurableInput.tsx
@@ -46,5 +46,7 @@ export default function ConfigurableInput(props: ConfigurableInputProps) {
return