adding control emails to control component settings form

This commit is contained in:
Carter 2026-06-16 12:32:43 -06:00
parent 7dd7504415
commit c4999a0ba5
4 changed files with 275 additions and 38 deletions

4
package-lock.json generated
View file

@ -43,7 +43,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#control_emails",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
@ -11198,7 +11198,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#29d9765ce93c04ad70d795b80e9873fc2fc5f600",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#4011e9e7a75148a195e7311c41a4e6c8d6886d7b",
"dependencies": {
"protobufjs": "^6.8.8"
}

View file

@ -56,7 +56,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#control_emails",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",

View file

@ -21,7 +21,8 @@ import {
} from "@mui/material";
import {
ExpandMore,
Close as CloseIcon
Close as CloseIcon,
Remove as RemoveIcon
} from "@mui/icons-material";
import PeriodSelect from "common/time/PeriodSelect";
import SearchSelect, { Option } from "common/SearchSelect";
@ -97,6 +98,7 @@ interface Props {
component: Component;
componentChanged: (component: Component, formValid: boolean) => void;
canEdit: boolean;
hideControllerFields?: boolean;
}
interface ComponentData {
@ -119,7 +121,7 @@ export default function ComponentForm(props: Props) {
const classes = useStyles();
const grainOptions = GrainOptions();
const [{ user }] = useGlobalState();
const { device, component, componentChanged, canEdit } = props;
const { device, component, componentChanged, canEdit, hideControllerFields } = props;
const [reportExpanded, setReportExpanded] = useState(false);
const [dataUsageWarningDismissed, setDataUsageWarningDismissed] = useState(true);
const [form, setForm] = useState<ComponentData>({
@ -137,7 +139,8 @@ export default function ComponentForm(props: Props) {
sensorDistance: "0",
});
const [compMode, setCompMode] = useState<any>();
const [useCustomGrain, setUseCustomGrain] = useState<boolean>(false)
const [useCustomGrain, setUseCustomGrain] = useState<boolean>(false);
const [controlEmail, setControlEmail] = useState("");
//const [numCalibrations, setNumCalibrations] = useState(0)
useEffect(() => {
@ -356,6 +359,21 @@ export default function ComponentForm(props: Props) {
setForm(f);
};
const addControlEmail = () => {
const trimmed = controlEmail.trim().toLowerCase();
if (trimmed === "" || form.component.settings.controlEmails.includes(trimmed)) return;
let f = cloneDeep(form);
f.component.settings.controlEmails.push(trimmed);
setForm(f);
setControlEmail("");
};
const removeControlEmail = (index: number) => {
let f = cloneDeep(form);
f.component.settings.controlEmails.splice(index, 1);
setForm(f);
};
const toggleMeasure = (event: any) => {
let f = cloneDeep(form);
f.measure = event.target.checked;
@ -1341,7 +1359,7 @@ export default function ComponentForm(props: Props) {
}}
/>
)}
{isController(component.settings.type) && (
{isController(component.settings.type) && !hideControllerFields && (
<React.Fragment>
<Grid size={{ xs: 12 }}>
<TextField
@ -1382,6 +1400,62 @@ export default function ComponentForm(props: Props) {
/>
</FormControl>
</Grid>
<Grid size={{ xs: 12 }}>
<Typography variant="subtitle2" style={{ marginTop: 16, marginBottom: 8 }}>
Authorized Control Emails
</Typography>
<Grid container spacing={1} alignItems="center">
<Grid size={{ xs: 9 }}>
<TextField
label="Email address"
type="email"
fullWidth
variant="outlined"
size="small"
value={controlEmail}
onChange={e => setControlEmail(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter") {
e.preventDefault();
addControlEmail();
}
}}
disabled={!canEdit}
/>
</Grid>
<Grid size={{ xs: 3 }}>
<Button
fullWidth
variant="outlined"
color="primary"
onClick={addControlEmail}
disabled={!canEdit || controlEmail.trim() === ""}>
Add
</Button>
</Grid>
</Grid>
{component.settings.controlEmails.map((email, i) => (
<Grid
container
key={i}
alignItems="center"
justifyContent="space-between"
style={{ marginTop: 4 }}>
<Grid>
<Typography variant="body2">{email}</Typography>
</Grid>
<Grid>
<IconButton
size="small"
onClick={() => removeControlEmail(i)}
disabled={!canEdit}
className={classes.redButton}>
<RemoveIcon />
</IconButton>
</Grid>
</Grid>
))}
</Grid>
</React.Fragment>
)}
{showDataUsageWarning && (

View file

@ -8,6 +8,7 @@ import {
DialogContentText,
DialogTitle,
Divider,
FormControl,
FormControlLabel,
Grid2 as Grid,
IconButton,
@ -41,8 +42,10 @@ import {
GetComponentTypeOptions,
getFriendlyName,
getMeasurements,
isController
isController,
} from "pbHelpers/ComponentType";
import PeriodSelect from "common/time/PeriodSelect";
import { bestUnit, milliToX } from "common/time/duration";
import {
ComponentAvailabilityMap,
DeviceAvailabilityMap,
@ -177,6 +180,7 @@ export default function ComponentSettings(props: Props) {
const [formValid, setFormValid] = useState(false);
const [excludedNodes, setExcludedNodes] = useState<number[]>([]);
const [nodeToExclude, setNodeToExclude] = useState(0);
const [controlEmail, setControlEmail] = useState("");
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setTabVal(newValue);
@ -498,6 +502,7 @@ export default function ComponentSettings(props: Props) {
component={formComponent}
device={device}
canEdit={canEdit}
hideControllerFields={isController(formComponent.settings.type)}
componentChanged={(formComp, isValid) => {
formComponent.settings = formComp.settings;
setFormValid(isValid);
@ -867,6 +872,145 @@ export default function ComponentSettings(props: Props) {
);
};
const addControlEmail = () => {
const trimmed = controlEmail.trim().toLowerCase();
if (trimmed === "" || formComponent.settings.controlEmails.includes(trimmed)) return;
let f = cloneDeep(formComponent);
f.settings.controlEmails.push(trimmed);
setFormComponent(f);
setControlEmail("");
};
const removeControlEmail = (index: number) => {
let f = cloneDeep(formComponent);
f.settings.controlEmails.splice(index, 1);
setFormComponent(f);
};
const handleOutputModeChanged = (event: any) => {
let f = cloneDeep(formComponent);
f.settings.defaultOutputState = event.target.value;
setFormComponent(f);
};
const handleMinCycleTimeChanged = (ms: number) => {
let f = cloneDeep(formComponent);
f.settings.minCycleTimeMs = Number(ms);
setFormComponent(f);
};
const isMinCycleTimeValid = () => {
const ext = extension(formComponent.settings.type, formComponent.settings.subtype);
if (!ext.isController) return true;
let min = Math.max(0, ext.minCycleTimeMs ? ext.minCycleTimeMs : 0);
return formComponent.settings.minCycleTimeMs >= min;
};
const minCycleTimeDescription = () => {
const ext = extension(formComponent.settings.type, formComponent.settings.subtype);
if (!ext.isController) return "";
const min = Math.max(0, ext.minCycleTimeMs ? ext.minCycleTimeMs : 0);
const unit = bestUnit(min);
const value = milliToX(min, unit).toString();
return "Must be at least " + value + " " + unit;
};
const controlContent = () => {
return (
<DialogContent>
<TextField
id="output-mode"
name="outputMode"
select
label="Output Mode"
value={formComponent.settings.defaultOutputState}
onChange={handleOutputModeChanged}
margin="normal"
fullWidth
variant="outlined"
disabled={!canEdit}>
<MenuItem value={0}>Auto</MenuItem>
<MenuItem value={1}>On</MenuItem>
<MenuItem value={2}>Off</MenuItem>
</TextField>
<FormControl fullWidth>
<PeriodSelect
id="minCycleTimeMs"
label="Minimum cycle time"
units={["seconds", "minutes"]}
isDisabled={!canEdit}
isError={!isMinCycleTimeValid()}
initialMs={
formComponent.settings.minCycleTimeMs
? formComponent.settings.minCycleTimeMs
: undefined
}
onChange={handleMinCycleTimeChanged}
helperText={
isMinCycleTimeValid()
? "The minimum amount of time that the component spends in a state"
: minCycleTimeDescription()
}
/>
</FormControl>
<Typography variant="subtitle2" style={{ marginTop: 16, marginBottom: 8 }}>
Authorized Control Emails
</Typography>
<Grid container spacing={1} alignItems="center">
<Grid size={{ xs: 9 }}>
<TextField
label="Email address"
type="email"
fullWidth
variant="outlined"
size="small"
value={controlEmail}
onChange={e => setControlEmail(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter") {
e.preventDefault();
addControlEmail();
}
}}
disabled={!canEdit}
/>
</Grid>
<Grid size={{ xs: 3 }}>
<Button
fullWidth
variant="outlined"
color="primary"
onClick={addControlEmail}
disabled={!canEdit || controlEmail.trim() === ""}>
Add
</Button>
</Grid>
</Grid>
{formComponent.settings.controlEmails.map((email, i) => (
<Grid
container
key={i}
alignItems="center"
justifyContent="space-between"
style={{ marginTop: 4 }}>
<Grid>
<Typography variant="body2">{email}</Typography>
</Grid>
<Grid>
<IconButton
size="small"
onClick={() => removeControlEmail(i)}
disabled={!canEdit}
className={classNames(classes.redButton)}>
<RemoveIcon />
</IconButton>
</Grid>
</Grid>
))}
</DialogContent>
);
};
const actions = () => {
return (
<DialogActions>
@ -1042,7 +1186,25 @@ export default function ComponentSettings(props: Props) {
scroll="paper">
{title()}
<Divider />
{!isController(formComponent.settings.type) && (
{isController(formComponent.settings.type) ? (
<React.Fragment>
<Tabs
value={tabVal}
indicatorColor="primary"
textColor="primary"
onChange={handleChange}>
<Tab label="General" />
<Tab label="Control" />
</Tabs>
<TabPanelMine value={tabVal} index={0}>
{content()}
</TabPanelMine>
<TabPanelMine value={tabVal} index={1}>
{controlContent()}
</TabPanelMine>
</React.Fragment>
) : (
<React.Fragment>
<Tabs
value={tabVal}
indicatorColor="primary"
@ -1052,7 +1214,6 @@ export default function ComponentSettings(props: Props) {
<Tab label="Overlays" />
<Tab label="Nodes" disabled={!prevComponent} />
</Tabs>
)}
<TabPanelMine value={tabVal} index={0}>
{content()}
</TabPanelMine>
@ -1072,6 +1233,8 @@ export default function ComponentSettings(props: Props) {
<TabPanelMine value={tabVal} index={2}>
{componentPrefs()}
</TabPanelMine>
</React.Fragment>
)}
{actions()}
</ResponsiveDialog>
)}