1605 lines
54 KiB
TypeScript
1605 lines
54 KiB
TypeScript
import {
|
|
Button,
|
|
Checkbox,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Divider,
|
|
FormControl,
|
|
FormControlLabel,
|
|
FormHelperText,
|
|
FormLabel,
|
|
Grid2 as Grid,
|
|
IconButton,
|
|
InputAdornment,
|
|
MenuItem,
|
|
Switch,
|
|
TextField,
|
|
Theme,
|
|
Tooltip,
|
|
Typography,
|
|
useTheme
|
|
} from "@mui/material";
|
|
import AddIcon from "@mui/icons-material/AddCircle";
|
|
import RemoveIcon from "@mui/icons-material/RemoveCircle";
|
|
import classNames from "classnames";
|
|
import DeleteButton from "common/DeleteButton";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import { coalesce, or, capitalize } from "utils";
|
|
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
|
|
import { Component, Device, Interaction } from "models";
|
|
import moment from "moment-timezone";
|
|
import {
|
|
componentIDToString,
|
|
emptyComponentId,
|
|
getComponentIDString,
|
|
sameComponentID,
|
|
stringToComponentId
|
|
} from "pbHelpers/Component";
|
|
import {
|
|
extension,
|
|
getMeasurements,
|
|
GetNumNodes,
|
|
hasInteractionResultType,
|
|
isSource
|
|
} from "pbHelpers/ComponentType";
|
|
import { ComponentType, Measurement, Operator, Result } from "pbHelpers/Enums";
|
|
import {
|
|
getDefaultInteraction,
|
|
isInteractionValid,
|
|
isMeasurementTypeValid,
|
|
isSourceValid,
|
|
timeOfDayDescriptor
|
|
} from "pbHelpers/Interaction";
|
|
import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { quack } from "protobuf-ts/quack";
|
|
import { useGlobalState } from "providers";
|
|
import React, { useCallback, useEffect, useState } from "react";
|
|
import { hasDeviceFeature } from "services/feature/service";
|
|
import RemoveInteraction from "./RemoveInteraction";
|
|
import { makeStyles } from "@mui/styles";
|
|
import { green, red } from "@mui/material/colors";
|
|
import { LocalizationProvider, TimePicker } from "@mui/x-date-pickers";
|
|
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
|
// import dayjs from "dayjs";
|
|
import CancelSubmit from "common/CancelSubmit";
|
|
import { Sensor } from "ventilation/drawable/Sensor";
|
|
|
|
// import { Sensor } from "ventilation/drawable/Sensor";
|
|
|
|
const useStyles = makeStyles((theme: Theme) => {
|
|
return ({
|
|
dialogContent: {
|
|
marginTop: theme.spacing(1)
|
|
},
|
|
borderedContainer: {
|
|
padding: theme.spacing(1),
|
|
marginBottom: theme.spacing(2),
|
|
border: "1px solid rgba(255, 255, 255, 0.12)",
|
|
borderRadius: "4px"
|
|
},
|
|
greenButton: {
|
|
color: green["600"]
|
|
},
|
|
redButton: {
|
|
color: red["600"]
|
|
},
|
|
noPadding: {
|
|
padding: 0
|
|
},
|
|
forPrompt: {
|
|
fontSize: "0.875em",
|
|
marginTop: "10px"
|
|
},
|
|
timeRange: {
|
|
fontSize: "0.875em",
|
|
marginTop: "20px"
|
|
},
|
|
singleInput: {
|
|
minHeight: "48px"
|
|
}
|
|
})
|
|
});
|
|
|
|
interface Props {
|
|
device: Device;
|
|
initialComponent?: Component;
|
|
initialInteraction?: Interaction;
|
|
components: Component[];
|
|
mode?: string;
|
|
isDialogOpen: boolean;
|
|
closeDialogCallback: Function;
|
|
refreshCallback: Function;
|
|
canEdit?: boolean;
|
|
sensor?: Sensor;
|
|
}
|
|
|
|
export default function InteractionSettings(props: Props) {
|
|
const {
|
|
device,
|
|
initialComponent,
|
|
initialInteraction,
|
|
components,
|
|
mode,
|
|
isDialogOpen,
|
|
closeDialogCallback,
|
|
refreshCallback,
|
|
canEdit,
|
|
sensor
|
|
} = props;
|
|
const theme = useTheme();
|
|
const { success, error } = useSnackbar();
|
|
const prevInitialInteraction = usePrevious(initialInteraction);
|
|
const prevComponents = usePrevious(components);
|
|
const prevInitialComponent = usePrevious(initialComponent);
|
|
const classes = useStyles();
|
|
const [{ user, as }] = useGlobalState();
|
|
const interactionsAPI = useInteractionsAPI();
|
|
const [interaction, setInteraction] = useState<Interaction>(
|
|
Interaction.clone(initialInteraction)
|
|
);
|
|
const [isRemoveInteractionOpen, setIsRemoveInteractionOpen] = useState<boolean>(false);
|
|
const [mappedComponents, setMappedComponents] = useState<Map<string, Component>>(new Map());
|
|
const [numConditions, setNumConditions] = useState<number>(0);
|
|
const [rawConditionValues, setRawConditionValues] = useState<string[]>([]);
|
|
const [rawResultValue, setRawResultValue] = useState<string>("");
|
|
const [dutyCycleEnabled, setDutyCycleEnabled] = useState<boolean>(false);
|
|
const [dutyCycle, setDutyCycle] = useState<string>("");
|
|
const [timezone, setTimezone] = useState<string | undefined>(undefined);
|
|
//whether is any node(0), average of all nodes(1), single node(2), or node diff(3)
|
|
const [subtypeDropdown, setSubtypeDropdown] = useState<number>(0);
|
|
|
|
const initialConditions = useCallback(
|
|
(type: quack.ComponentType): pond.InteractionCondition[] => {
|
|
let measurementType = availableMeasurementTypes(type)[0];
|
|
let condition = pond.InteractionCondition.create({
|
|
measurementType: measurementType,
|
|
comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less,
|
|
value: 0
|
|
});
|
|
return [condition];
|
|
},
|
|
[]
|
|
);
|
|
|
|
const setDefaultState = useCallback(() => {
|
|
let interaction = getDefaultInteraction();
|
|
if (initialInteraction && mode === "update") {
|
|
interaction = initialInteraction;
|
|
if (interaction.settings.subtype === 0 || interaction.settings.subtype === 1) {
|
|
setSubtypeDropdown(interaction.settings.subtype);
|
|
} else {
|
|
if (interaction.settings.subtype >= 138) {
|
|
setSubtypeDropdown(4);
|
|
} else if (interaction.settings.subtype > 17) {
|
|
setSubtypeDropdown(3);
|
|
} else if (interaction.settings.subtype > 1) {
|
|
setSubtypeDropdown(2);
|
|
}
|
|
}
|
|
}
|
|
if (initialComponent && mode === "add") {
|
|
interaction.settings.result = pond.InteractionResult.create({
|
|
type: Result.report,
|
|
value: 0,
|
|
mode: 0
|
|
});
|
|
if (isSource(initialComponent.settings.type)) {
|
|
interaction.settings.source = initialComponent.location();
|
|
interaction.settings.conditions = initialConditions(initialComponent.settings.type);
|
|
}
|
|
}
|
|
let rawConditionValues = [] as Array<string>;
|
|
interaction.settings.conditions.forEach((condition: pond.InteractionCondition) => {
|
|
let value = describeMeasurement(
|
|
condition.measurementType,
|
|
or(initialComponent, Component.create()).settings.type,
|
|
or(initialComponent, Component.create()).settings.subtype
|
|
).toDisplay(condition.value);
|
|
rawConditionValues.push(value.toString());
|
|
});
|
|
let numConditions = interaction.settings.conditions.length;
|
|
let mappedComponents: Map<string, Component> = new Map();
|
|
components.forEach(component => mappedComponents.set(component.locationString(), component));
|
|
let interactionResult = pond.InteractionResult.create(
|
|
or(interaction.settings.result, undefined)
|
|
);
|
|
|
|
if (sensor) {
|
|
interaction.settings.subtype = 2;
|
|
interaction.settings.nodeOne = sensor.index;
|
|
interaction.settings.nodeTwo = sensor.index;
|
|
}
|
|
|
|
setIsRemoveInteractionOpen(false);
|
|
setNumConditions(numConditions);
|
|
setRawConditionValues(rawConditionValues);
|
|
setRawResultValue(interactionResult.value.toString());
|
|
setDutyCycleEnabled(interactionResult.dutyCycle > 0);
|
|
setDutyCycle(interactionResult.dutyCycle.toString());
|
|
setMappedComponents(mappedComponents);
|
|
setInteraction(interaction);
|
|
}, [components, initialComponent, initialConditions, initialInteraction, mode, /*sensor*/]);
|
|
|
|
const availableSources = () => {
|
|
return components.filter(component => isSource(component.settings.type));
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (user && user.settings.timezone) {
|
|
setTimezone(user.settings.timezone);
|
|
}
|
|
if (
|
|
prevInitialInteraction !== initialInteraction ||
|
|
(prevComponents && prevComponents.length !== components.length) ||
|
|
getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent)
|
|
) {
|
|
setDefaultState();
|
|
}
|
|
}, [
|
|
components.length,
|
|
initialComponent,
|
|
initialInteraction,
|
|
prevComponents,
|
|
prevInitialComponent,
|
|
prevInitialInteraction,
|
|
setDefaultState,
|
|
user
|
|
]);
|
|
|
|
const getAvailableSinks = () => {
|
|
let type = or(interaction.settings.result, pond.InteractionResult.create()).type;
|
|
return components.filter(component => hasInteractionResultType(component.settings.type, type));
|
|
};
|
|
|
|
const close = () => {
|
|
closeDialogCallback();
|
|
if (mode === "add") {
|
|
setDefaultState();
|
|
}
|
|
};
|
|
|
|
const submit = () => {
|
|
let settings = interaction.settings;
|
|
if (multiNodeSource() && (settings.nodeOne > 0 || settings.nodeTwo > 0)) {
|
|
if (subtypeDropdown === 3 || subtypeDropdown === 2) {
|
|
settings.subtype = interaction.subtypeFromNodes(settings.nodeOne, settings.nodeTwo);
|
|
if (
|
|
interaction.settings.nodeOne > interaction.settings.nodeTwo &&
|
|
interaction.settings.nodeTwo !== 0
|
|
) {
|
|
//flip operator and send negative comparitor to save
|
|
settings.conditions.forEach(condition => {
|
|
if (
|
|
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
|
) {
|
|
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
|
|
} else if (
|
|
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN
|
|
) {
|
|
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
|
|
}
|
|
condition.value = condition.value * -1;
|
|
});
|
|
}
|
|
} else if (subtypeDropdown === 4) {
|
|
settings.subtype = Interaction.upToSubtype(settings.nodeOne);
|
|
}
|
|
} else {
|
|
settings.subtype = subtypeDropdown;
|
|
}
|
|
|
|
let sync = true;
|
|
if (interaction)
|
|
Object.keys(interaction?.settings).forEach(key => {
|
|
// Check if the key value has changed
|
|
let k = key as keyof pond.InteractionSettings;
|
|
let initial = JSON.stringify(initialInteraction?.settings[k]);
|
|
let after = JSON.stringify(interaction.settings[k]);
|
|
if (initial !== after) {
|
|
if (key !== "sortPriority") {
|
|
sync = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
switch (mode) {
|
|
case "add":
|
|
interactionsAPI
|
|
.addInteraction(Number(device.settings.deviceId), settings, as)
|
|
.then(() => {
|
|
success("Interaction was successfully created");
|
|
refreshCallback();
|
|
})
|
|
.catch(() => error("Error occured when creating an interaction"))
|
|
.finally(() => close());
|
|
break;
|
|
default:
|
|
if (sync) {
|
|
let s = pond.InteractionPondSettings.create();
|
|
s.sortPriority = interaction.settings.sortPriority;
|
|
interactionsAPI
|
|
.updateInteractionPondSettings(Number(device.settings.deviceId), interaction.key(), s, as)
|
|
.then(() => {
|
|
success("Interaction pond settings were successfully updated");
|
|
refreshCallback();
|
|
})
|
|
.catch(() => error("Error when updating interaction pond settings"))
|
|
.finally(() => close());
|
|
} else {
|
|
interactionsAPI
|
|
.updateInteraction(Number(device.settings.deviceId), settings, as)
|
|
.then(() => {
|
|
success("Interaction was successfully updated");
|
|
refreshCallback();
|
|
})
|
|
.catch(() => error("Error occured when updating an interaction"))
|
|
.finally(() => close());
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
const openRemoveInteraction = () => {
|
|
setIsRemoveInteractionOpen(true);
|
|
};
|
|
|
|
const closeRemoveInteraction = () => {
|
|
setIsRemoveInteractionOpen(false);
|
|
close();
|
|
};
|
|
|
|
const interactionRemoved = () => {
|
|
refreshCallback();
|
|
};
|
|
|
|
const addCondition = () => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let updatedRawConditionValues = rawConditionValues;
|
|
if (numConditions < 2 && interaction.settings.source) {
|
|
let condition = pond.InteractionCondition.create();
|
|
condition.measurementType = getMeasurements(
|
|
interaction.settings.source.type
|
|
)[0].measurementType;
|
|
condition.comparison =
|
|
condition.measurementType === Measurement.boolean ? Operator.equals : Operator.less;
|
|
updatedRawConditionValues.push("");
|
|
updatedInteraction.settings.conditions.push(condition);
|
|
|
|
setInteraction(updatedInteraction);
|
|
setNumConditions(numConditions + 1);
|
|
setRawConditionValues(updatedRawConditionValues);
|
|
}
|
|
};
|
|
|
|
const removeCondition = (index: number) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let updatedRawConditionValues = rawConditionValues;
|
|
if (numConditions > 1 && index < numConditions) {
|
|
updatedInteraction.settings.conditions.splice(index, 1);
|
|
updatedRawConditionValues.splice(index, 1);
|
|
setInteraction(updatedInteraction);
|
|
setNumConditions(numConditions - 1);
|
|
setRawConditionValues(updatedRawConditionValues);
|
|
}
|
|
};
|
|
|
|
const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => {
|
|
return getMeasurements(type).map(m => m.measurementType);
|
|
};
|
|
|
|
//changing the source also resets the conditions
|
|
const changeSource = (event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let changed = event.target.value !== componentIDToString(interaction.settings.source);
|
|
if (!changed) return;
|
|
updatedInteraction.settings.source = stringToComponentId(event.target.value.toString());
|
|
updatedInteraction.settings.conditions = initialConditions(
|
|
or(interaction.settings.source, quack.ComponentID.create()).type
|
|
);
|
|
let result = pond.InteractionResult.create(or(interaction.settings.result, undefined));
|
|
if (result.type === Result.report) {
|
|
updatedInteraction.settings.sink = emptyComponentId();
|
|
}
|
|
updatedInteraction.settings.result = result;
|
|
setInteraction(updatedInteraction);
|
|
setNumConditions(1);
|
|
setRawConditionValues([""]);
|
|
};
|
|
|
|
const changePriority = (event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
updatedInteraction.settings.priority = Number(event.target.value);
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const changeSortPriority = (event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
updatedInteraction.settings.sortPriority = Number(event.target.value);
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => {
|
|
const source: Component = or(
|
|
mappedComponents.get(componentIDToString(interaction.settings.source)),
|
|
Component.create()
|
|
);
|
|
return describeMeasurement(measurementType, source.settings.type, source.settings.subtype);
|
|
};
|
|
|
|
const describeSink = (): MeasurementDescriber => {
|
|
const sink = mappedComponents.get(componentIDToString(interaction.settings.sink));
|
|
return describeMeasurement(
|
|
Measurement.boolean,
|
|
or(sink, Component.create()).settings.type,
|
|
or(sink, Component.create()).settings.subtype
|
|
);
|
|
};
|
|
|
|
const changeMeasurementType = (conditionIndex: number, event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let measurementType = interaction.settings.conditions[conditionIndex].measurementType;
|
|
let rawValue = rawConditionValues[conditionIndex] as any;
|
|
updatedInteraction.settings.conditions[conditionIndex].value = describeSource(
|
|
measurementType
|
|
).toStored(rawValue);
|
|
updatedInteraction.settings.conditions[conditionIndex].measurementType = event.target.value;
|
|
if (event.target.value === Measurement.boolean) {
|
|
updatedInteraction.settings.conditions[conditionIndex].comparison = Operator.equals;
|
|
}
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const changeComparison = (conditionIndex: number, event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
updatedInteraction.settings.conditions[conditionIndex].comparison = event.target.value;
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const changeValue = (conditionIndex: number, event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let updatedRawConditionValues = rawConditionValues;
|
|
let measurementType = interaction.settings.conditions[conditionIndex].measurementType;
|
|
updatedRawConditionValues[conditionIndex] = event.target.value;
|
|
updatedInteraction.settings.conditions[conditionIndex].value = describeSource(
|
|
measurementType
|
|
).toStored(event.target.value);
|
|
setInteraction(updatedInteraction);
|
|
setRawConditionValues(updatedRawConditionValues);
|
|
};
|
|
|
|
const changeResultValue = (event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let value = event.target.value;
|
|
if (event.target.type === "checkbox") {
|
|
value = event.target.checked ? 1 : 0;
|
|
}
|
|
(updatedInteraction.settings.result as pond.InteractionResult).value = isNaN(value)
|
|
? 0
|
|
: Number(value);
|
|
setInteraction(updatedInteraction);
|
|
setRawResultValue(value);
|
|
};
|
|
|
|
const changeSubtype = (event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let value = event.target.value;
|
|
if (!isNaN(value)) {
|
|
if (value < 2) {
|
|
updatedInteraction.settings.subtype = value;
|
|
}
|
|
setSubtypeDropdown(value);
|
|
updatedInteraction.settings.nodeOne = 0;
|
|
updatedInteraction.settings.nodeTwo = 0;
|
|
}
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const changeNode = (event: any, node: number) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let value = event.target.value;
|
|
if (!isNaN(value)) {
|
|
if (node === 1) {
|
|
updatedInteraction.settings.nodeOne = value;
|
|
} else if (node === 2) {
|
|
updatedInteraction.settings.nodeTwo = value;
|
|
}
|
|
}
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const changeResultType = (event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let result = interaction.settings.result as pond.InteractionResult;
|
|
result.type = event.target.value;
|
|
if (result.type === Result.report) {
|
|
updatedInteraction.settings.sink = null;
|
|
} else if (result.type === Result.toggle) {
|
|
let availableSinks = getAvailableSinks();
|
|
if (availableSinks.length > 0) {
|
|
let firstSink = availableSinks[0];
|
|
updatedInteraction.settings.sink = quack.ComponentID.fromObject({
|
|
type: firstSink.settings.type,
|
|
addressType: firstSink.settings.addressType,
|
|
address: firstSink.settings.address
|
|
});
|
|
}
|
|
}
|
|
updatedInteraction.settings.result = result;
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const changeResultMode = (event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let result = interaction.settings.result as pond.InteractionResult;
|
|
result.mode = event.target.value;
|
|
updatedInteraction.settings.result = result;
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const changeSink = (event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
updatedInteraction.settings.sink = stringToComponentId(event.target.value.toString());
|
|
if (
|
|
updatedInteraction.settings.sink.type === quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION
|
|
) {
|
|
if (
|
|
updatedInteraction.settings.sink.addressType ===
|
|
quack.AddressType.ADDRESS_TYPE_INTERNAL_SLEEP
|
|
) {
|
|
if (updatedInteraction.settings.result) {
|
|
updatedInteraction.settings.result.value = 1;
|
|
}
|
|
}
|
|
}
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const toggleReport = (_event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let notifications = or(
|
|
interaction.settings.notifications,
|
|
pond.InteractionNotifications.create()
|
|
);
|
|
notifications.reports = !notifications.reports;
|
|
updatedInteraction.settings.notifications = notifications;
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const toggleNotify = (_event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let notifications = or(
|
|
updatedInteraction.settings.notifications,
|
|
pond.InteractionNotifications.create()
|
|
);
|
|
notifications.notify = !notifications.notify;
|
|
updatedInteraction.settings.notifications = notifications;
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const toggleDaySelected = (day: string, event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let schedule = or(updatedInteraction.settings.schedule, pond.InteractionSchedule.create());
|
|
if (event.target.checked) {
|
|
if (!schedule.weekdays.includes(day)) {
|
|
schedule.weekdays.push(day);
|
|
}
|
|
} else {
|
|
schedule.weekdays.splice(schedule.weekdays.indexOf(day), 1);
|
|
}
|
|
updatedInteraction.settings.schedule = schedule;
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const setScheduleTime = (id: string, event: any) => {
|
|
let updatedInteraction = Interaction.clone(interaction);
|
|
let schedule = or(updatedInteraction.settings.schedule, pond.InteractionSchedule.create());
|
|
if (id === "shortcut") {
|
|
let value = event.target.value;
|
|
if (value === "allDay") {
|
|
schedule.timeOfDayStart = "00:00";
|
|
schedule.timeOfDayEnd = "24:00";
|
|
} else if (value === "before") {
|
|
schedule.timeOfDayStart = "00:00";
|
|
if (!schedule.timeOfDayEnd || schedule.timeOfDayEnd === "24:00") {
|
|
schedule.timeOfDayEnd = "23:59";
|
|
}
|
|
} else if (value === "after") {
|
|
schedule.timeOfDayEnd = "24:00";
|
|
if (schedule.timeOfDayStart === "00:00") {
|
|
schedule.timeOfDayStart = "00:01";
|
|
}
|
|
} else {
|
|
if (schedule.timeOfDayStart === "00:00") {
|
|
schedule.timeOfDayStart = "00:01";
|
|
}
|
|
if (schedule.timeOfDayEnd === "24:00") {
|
|
schedule.timeOfDayEnd = "23:59";
|
|
}
|
|
}
|
|
} else {
|
|
let timeOfDay =
|
|
event
|
|
.hour()
|
|
.toString()
|
|
.padStart(2, "0") +
|
|
":" +
|
|
event
|
|
.minute()
|
|
.toString()
|
|
.padStart(2, "0");
|
|
if (id === "start") {
|
|
schedule.timeOfDayStart = timeOfDay;
|
|
} else if (id === "end") {
|
|
if (timeOfDay === "00:00") {
|
|
timeOfDay = "24:00";
|
|
}
|
|
schedule.timeOfDayEnd = timeOfDay;
|
|
}
|
|
}
|
|
schedule.timezone = or(timezone, moment.tz.guess());
|
|
updatedInteraction.settings.schedule = schedule;
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const isInitialComponentTheSource = () => {
|
|
if (!interaction || !initialComponent) return false;
|
|
return sameComponentID(interaction.settings.source, initialComponent.location());
|
|
};
|
|
|
|
const isInitialComponentTheSink = () => {
|
|
if (!interaction || !initialComponent) return false;
|
|
let sinkID = JSON.stringify(quack.ComponentID.create(or(interaction.settings.sink, undefined)));
|
|
let component = initialComponent;
|
|
let componentID = JSON.stringify(
|
|
quack.ComponentID.create({
|
|
type: component.settings.type,
|
|
addressType: component.settings.addressType,
|
|
address: component.settings.address
|
|
})
|
|
);
|
|
return sinkID === componentID;
|
|
};
|
|
|
|
const invalidConditionValue = (index: number) => {
|
|
const conditions = interaction.settings.conditions;
|
|
if (conditions.length <= index) return false;
|
|
if (conditions[index].measurementType === Measurement.boolean) {
|
|
return false;
|
|
}
|
|
let value = rawConditionValues[index];
|
|
return value === "" || isNaN(Number(value));
|
|
};
|
|
|
|
const invalidConditionValues = () => {
|
|
let invalid = false;
|
|
interaction.settings.conditions.forEach((_, i) => {
|
|
if (invalidConditionValue(i)) {
|
|
invalid = true;
|
|
}
|
|
});
|
|
return invalid;
|
|
};
|
|
|
|
const invalidResultValue = () => {
|
|
return isNaN(Number(rawResultValue));
|
|
};
|
|
|
|
const getNodeOptions = (describer?: MeasurementDescriber) => {
|
|
let options = [];
|
|
let numNodes = 0;
|
|
if (initialComponent) {
|
|
if (initialComponent.lastMeasurement[0] && initialComponent.lastMeasurement[0].values[0]) {
|
|
numNodes = initialComponent.lastMeasurement[0].values[0].values.length; //use the new structure last measurement to determine the number of nodes
|
|
} else {
|
|
numNodes = GetNumNodes(
|
|
initialComponent.type(),
|
|
initialComponent.status.lastMeasurement?.measurement
|
|
);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i <= numNodes; i++) {
|
|
let label = i === 0 ? "None" : "Node " + i;
|
|
if (describer && i > 0) {
|
|
let details = describer.nodeDetails();
|
|
if (details && details.labels[i - 1]) {
|
|
label = details.labels[i - 1];
|
|
}
|
|
}
|
|
options.push(
|
|
<MenuItem key={i} value={i}>
|
|
{label}
|
|
</MenuItem>
|
|
);
|
|
}
|
|
return options;
|
|
};
|
|
|
|
//determine if the source component has nodes
|
|
const multiNodeSource = () => {
|
|
return getNodeOptions().length > 1;
|
|
};
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
const title = () => {
|
|
return (
|
|
<React.Fragment>
|
|
<Grid container justifyContent="space-between">
|
|
<Grid >
|
|
{mode && mode === "add" ? "Add Interaction" : "Interaction Settings"}
|
|
{/* <Typography variant="body2" color="textSecondary">
|
|
{sensor ? "Subnode: " + sensor.name() : device.name()}
|
|
</Typography> */}
|
|
</Grid>
|
|
</Grid>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const sourceInput = () => {
|
|
return (
|
|
<TextField
|
|
select
|
|
id="source"
|
|
label="Source"
|
|
value={componentIDToString(interaction.settings.source)}
|
|
onChange={changeSource}
|
|
fullWidth
|
|
autoFocus={false}
|
|
margin="normal"
|
|
variant="outlined"
|
|
InputLabelProps={{ shrink: true }}
|
|
disabled={isInitialComponentTheSource() || !canEdit || mode === "update"}
|
|
SelectProps={{ className: classes.singleInput }}>
|
|
{sourceMenuItems()}
|
|
</TextField>
|
|
);
|
|
};
|
|
|
|
const priorityInput = () => {
|
|
return (
|
|
<TextField
|
|
select
|
|
id="priority"
|
|
label="Priority"
|
|
value={interaction.settings.priority}
|
|
onChange={changePriority}
|
|
fullWidth
|
|
autoFocus={false}
|
|
margin="normal"
|
|
variant="outlined"
|
|
InputLabelProps={{ shrink: true }}
|
|
disabled={!canEdit}
|
|
SelectProps={{ className: classes.singleInput }}>
|
|
{priorityMenuItems()}
|
|
</TextField>
|
|
);
|
|
};
|
|
|
|
const sortingInput = () => {
|
|
return (
|
|
<TextField
|
|
select
|
|
id="sorting"
|
|
label="Sorting Priority"
|
|
value={interaction.settings.sortPriority}
|
|
onChange={changeSortPriority}
|
|
fullWidth
|
|
autoFocus={false}
|
|
margin="normal"
|
|
variant="outlined"
|
|
InputLabelProps={{ shrink: true }}
|
|
disabled={!canEdit}
|
|
SelectProps={{ className: classes.singleInput }}>
|
|
{sortingMenuItems()}
|
|
</TextField>
|
|
);
|
|
};
|
|
|
|
const measurementTypeMenuItems = () => {
|
|
const type = or(interaction.settings.source, quack.ComponentID.create()).type;
|
|
return availableMeasurementTypes(type).map(measurementType => (
|
|
<MenuItem key={measurementType} value={measurementType}>
|
|
{describeSource(measurementType).label()}
|
|
</MenuItem>
|
|
));
|
|
};
|
|
|
|
const sourceMenuItems = () => {
|
|
return availableSources().map(source => (
|
|
<MenuItem key={source.locationString()} value={source.locationString()}>
|
|
{source.name()}
|
|
</MenuItem>
|
|
));
|
|
};
|
|
|
|
const sinkMenuItems = () => {
|
|
let sinks = getAvailableSinks().map(sink => (
|
|
<MenuItem key={sink.locationString()} value={sink.locationString()}>
|
|
{sink.name()}
|
|
</MenuItem>
|
|
));
|
|
if (isResultType(Result.set)) {
|
|
sinks.push(
|
|
<MenuItem key="30-16-1" value="30-16-1">
|
|
Stay Awake
|
|
</MenuItem>
|
|
);
|
|
}
|
|
return sinks;
|
|
};
|
|
|
|
const priorityMenuItems = () => {
|
|
return ["Low", "Medium", "High"].map((name: string, i: number) => (
|
|
<MenuItem key={i} value={i}>
|
|
{name}
|
|
</MenuItem>
|
|
));
|
|
};
|
|
|
|
const sortingMenuItems = () => {
|
|
return ["None", "1", "2", "3", "4", "5"].map((name: string, i: number) => (
|
|
<MenuItem key={i} value={i}>
|
|
{name}
|
|
</MenuItem>
|
|
));
|
|
};
|
|
|
|
const isBooleanValue = (index: number) => {
|
|
if (interaction.settings.conditions.length <= index) return false;
|
|
return interaction.settings.conditions[index].measurementType === Measurement.boolean;
|
|
};
|
|
|
|
const conditionValue = (index: number, interaction: Interaction, values: string[]) => {
|
|
let condition = interaction.settings.conditions[index];
|
|
return isBooleanValue(index) ? condition.value.toString() : values[index];
|
|
};
|
|
|
|
const conditionGroup = (index: number) => {
|
|
if (index >= numConditions) return;
|
|
let measurementType = interaction.settings.conditions[index].measurementType;
|
|
let source = describeSource(measurementType);
|
|
let isBoolean = isBooleanValue(index);
|
|
return (
|
|
<React.Fragment key={index}>
|
|
<Grid
|
|
key={"interaction"}
|
|
container
|
|
direction="row"
|
|
justifyContent="center"
|
|
alignItems="center"
|
|
spacing={2}>
|
|
{index > 0 && (
|
|
<Grid size={{ xs: 12 }}>
|
|
<Typography align="center">AND</Typography>
|
|
</Grid>
|
|
)}
|
|
<Grid size={{ xs: 10, sm: 4 }}>
|
|
<TextField
|
|
select
|
|
id="measurementType"
|
|
required={true}
|
|
label=""
|
|
helperText="Measurement"
|
|
error={
|
|
!isMeasurementTypeValid(interaction.settings.conditions[index].measurementType)
|
|
}
|
|
disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
|
value={interaction.settings.conditions[index].measurementType}
|
|
onChange={event => changeMeasurementType(index, event)}
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
fullWidth
|
|
InputLabelProps={{ shrink: true }}>
|
|
{measurementTypeMenuItems()}
|
|
</TextField>
|
|
</Grid>
|
|
<Grid size={{ xs: 10, sm: 3 }}>
|
|
<TextField
|
|
select
|
|
id="comparison"
|
|
required={true}
|
|
label=""
|
|
helperText="Comparator"
|
|
disabled={!isSourceValid(interaction.settings.source) || isBoolean || !canEdit}
|
|
value={interaction.settings.conditions[index].comparison}
|
|
onChange={event => changeComparison(index, event)}
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
fullWidth
|
|
InputLabelProps={{ shrink: true }}>
|
|
{!isBoolean && [
|
|
<MenuItem key={2} value={Operator.less}>
|
|
{"is below"}
|
|
</MenuItem>,
|
|
<MenuItem key={3} value={Operator.greater}>
|
|
{"is above"}
|
|
</MenuItem>
|
|
]}
|
|
<MenuItem key={1} value={Operator.equals}>
|
|
{"is exactly"}
|
|
</MenuItem>
|
|
</TextField>
|
|
</Grid>
|
|
<Grid size={{ xs: 8, sm: 4 }}>
|
|
<TextField
|
|
select={isBoolean}
|
|
id="value"
|
|
required={true}
|
|
type={"text"}
|
|
label=""
|
|
helperText="Value"
|
|
error={invalidConditionValue(index)}
|
|
disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
|
value={conditionValue(index, interaction, rawConditionValues)}
|
|
onChange={event => changeValue(index, event)}
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
fullWidth
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">{source.unit()}</InputAdornment>
|
|
}}
|
|
InputLabelProps={{ shrink: true }}>
|
|
{isBoolean && [
|
|
<MenuItem key={0} value={0}>
|
|
{source.enumerations()[0]}
|
|
</MenuItem>,
|
|
<MenuItem key={1} value={1}>
|
|
{source.enumerations()[1]}
|
|
</MenuItem>
|
|
]}
|
|
</TextField>
|
|
</Grid>
|
|
<Grid size={{ xs: 2, sm: 1 }}>
|
|
{index === 0 ? (
|
|
<Tooltip title="Add another condition">
|
|
<IconButton
|
|
color="primary"
|
|
disabled={numConditions > 1 || !canEdit}
|
|
aria-label="Add another condition"
|
|
onClick={addCondition}
|
|
className={classNames(classes.greenButton, classes.noPadding)}>
|
|
<AddIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
) : (
|
|
<Tooltip title="Remove this condition">
|
|
<IconButton
|
|
color="default"
|
|
disabled={!canEdit}
|
|
aria-label="Remove condition"
|
|
onClick={() => removeCondition(index)}
|
|
className={classNames(classes.redButton, classes.noPadding)}>
|
|
<RemoveIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</Grid>
|
|
</Grid>
|
|
{multiNodeSource() && !sensor && (
|
|
<Grid
|
|
key={"nodes"}
|
|
container
|
|
direction="row"
|
|
justifyContent="center"
|
|
alignItems="center"
|
|
spacing={2}>
|
|
<Grid size={{ xs: 10, sm: 4 }}>
|
|
<TextField
|
|
select
|
|
id="subtype"
|
|
label="Subtype"
|
|
disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
|
value={subtypeDropdown}
|
|
onChange={event => changeSubtype(event)}
|
|
margin="dense"
|
|
variant="standard"
|
|
fullWidth
|
|
InputLabelProps={{ shrink: true }}>
|
|
<MenuItem key={0} value={0}>
|
|
Any
|
|
</MenuItem>
|
|
<MenuItem key={1} value={1}>
|
|
Average
|
|
</MenuItem>
|
|
<MenuItem key={2} value={2}>
|
|
Single Node
|
|
</MenuItem>
|
|
<MenuItem key={3} value={3}>
|
|
Node Diff
|
|
</MenuItem>
|
|
<MenuItem key={4} value={4}>
|
|
Up To
|
|
</MenuItem>
|
|
</TextField>
|
|
</Grid>
|
|
<Grid size={{ xs: 10, sm: 4 }}>
|
|
{(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && (
|
|
<TextField
|
|
select
|
|
id="interactionType"
|
|
label="Node 1"
|
|
disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
|
value={interaction.settings.nodeOne}
|
|
onChange={event => changeNode(event, 1)}
|
|
margin="dense"
|
|
variant="standard"
|
|
fullWidth
|
|
InputLabelProps={{ shrink: true }}>
|
|
{getNodeOptions(source)}
|
|
</TextField>
|
|
)}
|
|
</Grid>
|
|
<Grid size={{ xs: 10, sm: 4 }}>
|
|
{subtypeDropdown === 3 && (
|
|
<TextField
|
|
select
|
|
id="interactionType"
|
|
label="Node 2"
|
|
disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
|
value={interaction.settings.nodeTwo}
|
|
onChange={event => changeNode(event, 2)}
|
|
margin="dense"
|
|
variant="standard"
|
|
fullWidth
|
|
InputLabelProps={{ shrink: true }}>
|
|
{getNodeOptions(source)}
|
|
</TextField>
|
|
)}
|
|
</Grid>
|
|
</Grid>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const conditionInput = () => {
|
|
const conditionGroups = [];
|
|
for (let i = 0; i < numConditions; i++) {
|
|
conditionGroups[i] = conditionGroup(i);
|
|
}
|
|
return (
|
|
<FormControl
|
|
component={"fieldset" as "div"}
|
|
className={classes.borderedContainer}
|
|
fullWidth
|
|
disabled={!isSourceValid(interaction.settings.source) || !canEdit}>
|
|
<FormLabel component={"legend" as "caption"}>Conditions</FormLabel>
|
|
{isSourceValid(interaction.settings.source) ? (
|
|
<React.Fragment>{conditionGroups}</React.Fragment>
|
|
) : (
|
|
<FormHelperText>You must select a source before adding conditions</FormHelperText>
|
|
)}
|
|
</FormControl>
|
|
);
|
|
};
|
|
|
|
const isResultType = (type: quack.InteractionResultType): boolean => {
|
|
return or(interaction.settings.result, pond.InteractionResult.create()).type === type;
|
|
};
|
|
|
|
const reportingEnabled = () => {
|
|
return (
|
|
or(interaction.settings.notifications, pond.InteractionNotifications.create()).reports ||
|
|
or(interaction.settings.result, pond.InteractionResult.create()).type === Result.report
|
|
);
|
|
};
|
|
|
|
const toggleDutyCycle = (event: any) => {
|
|
let updatedInteraction = interaction;
|
|
if (!interaction.settings.result) {
|
|
updatedInteraction.settings.result = pond.InteractionResult.create();
|
|
}
|
|
if (!event.target.checked) {
|
|
if (!updatedInteraction.settings.result) {
|
|
updatedInteraction.settings.result = pond.InteractionResult.create({
|
|
dutyCycle: 0
|
|
});
|
|
} else {
|
|
updatedInteraction.settings.result.dutyCycle = 0;
|
|
}
|
|
}
|
|
setDutyCycleEnabled(event.target.checked);
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const changeDutyCycle = (event: any) => {
|
|
let dutyCycle = Number(event.target.value);
|
|
let updatedInteraction = interaction;
|
|
if (!isNaN(dutyCycle)) {
|
|
if (!updatedInteraction.settings.result) {
|
|
updatedInteraction.settings.result = pond.InteractionResult.create();
|
|
} else {
|
|
updatedInteraction.settings.result.dutyCycle = dutyCycle;
|
|
}
|
|
}
|
|
setDutyCycle(event.target.value);
|
|
setInteraction(updatedInteraction);
|
|
};
|
|
|
|
const dutyCycleInput = () => {
|
|
return (
|
|
<React.Fragment>
|
|
<Grid size={{ xs: 4, sm: 4 }}>
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
id="enableDutyCycle"
|
|
checked={dutyCycleEnabled}
|
|
onChange={toggleDutyCycle}
|
|
value="enableDutyCycle"
|
|
color="secondary"
|
|
/>
|
|
}
|
|
label="Once every"
|
|
disabled={!canEdit}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 8, sm: 8 }}>
|
|
<TextField
|
|
id="dutyCycle"
|
|
value={dutyCycle}
|
|
onChange={changeDutyCycle}
|
|
fullWidth
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
disabled={!canEdit || !dutyCycleEnabled}
|
|
error={isNaN(Number(dutyCycle)) || Number(dutyCycle) < 0}
|
|
InputLabelProps={{
|
|
shrink: true
|
|
}}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
|
|
}}></TextField>
|
|
</Grid>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const notificationInput = () => {
|
|
const report = reportingEnabled();
|
|
const notify = or(interaction.settings.notifications, pond.InteractionNotifications.create())
|
|
.notify;
|
|
return (
|
|
<React.Fragment>
|
|
{!isResultType(Result.report) && (
|
|
<Grid size={{xs:6, sm:3}}>
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
id="notificationReports"
|
|
checked={reportingEnabled()}
|
|
onChange={toggleReport}
|
|
value="notificationReports"
|
|
color="secondary"
|
|
/>
|
|
}
|
|
label="Report"
|
|
disabled={!canEdit}
|
|
/>
|
|
</Grid>
|
|
)}
|
|
<Grid size={{xs: isResultType(Result.toggle) ? 6 : 12, sm: 3}} >
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
id="interactionNotification"
|
|
checked={notify}
|
|
onChange={toggleNotify}
|
|
value="interactionNotification"
|
|
color="secondary"
|
|
/>
|
|
}
|
|
label="Notify"
|
|
disabled={!canEdit || !report}
|
|
/>
|
|
</Grid>
|
|
{notify && (
|
|
<Grid size={{ xs: 12, sm: 12 }}>
|
|
<Typography color="textSecondary" variant="subtitle1">
|
|
Everyone with notifications enabled for this device will receive an email and/or SMS
|
|
each time this result occurs
|
|
</Typography>
|
|
</Grid>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const sinkSelector = () => {
|
|
return (
|
|
<TextField
|
|
select
|
|
id="sink"
|
|
label=""
|
|
value={componentIDToString(interaction.settings.sink)}
|
|
onChange={changeSink}
|
|
fullWidth
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
InputLabelProps={{ shrink: true }}
|
|
disabled={isInitialComponentTheSink() || !canEdit || mode === "update"}>
|
|
{sinkMenuItems()}
|
|
</TextField>
|
|
);
|
|
};
|
|
|
|
const setMenuItems = () => {
|
|
let type = or(interaction.settings.sink, quack.ComponentID.create()).type;
|
|
let states = extension(type).states.map((state, i) => (
|
|
<MenuItem key={state} value={i}>
|
|
{state}
|
|
</MenuItem>
|
|
));
|
|
return states;
|
|
};
|
|
|
|
const resultInput = () => {
|
|
const sink = or(interaction.settings.sink, quack.ComponentID.create());
|
|
const result = or(
|
|
interaction.settings.result,
|
|
pond.InteractionResult.create({
|
|
type: Result.report
|
|
})
|
|
);
|
|
const isRun = result.type === Result.run;
|
|
const isMotor = sink.type === ComponentType.stepperMotor;
|
|
return (
|
|
<FormControl component={"fieldset" as "div"} className={classes.borderedContainer} fullWidth>
|
|
<FormLabel component={"legend" as "caption"}>Result</FormLabel>
|
|
<Grid container direction="row" spacing={2}>
|
|
<Grid size={{ xs: 12, sm: 3 }}>
|
|
<TextField
|
|
select
|
|
id="resultType"
|
|
required={true}
|
|
label=""
|
|
value={result.type}
|
|
onChange={changeResultType}
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
fullWidth
|
|
InputLabelProps={{ shrink: true }}
|
|
disabled={!canEdit}>
|
|
<MenuItem value={Result.report}>Report</MenuItem>
|
|
<MenuItem value={Result.set}>Set</MenuItem>
|
|
<MenuItem value={Result.toggle}>Toggle</MenuItem>
|
|
<MenuItem value={Result.run}>Run</MenuItem>
|
|
</TextField>
|
|
</Grid>
|
|
{isRun && (
|
|
<React.Fragment>
|
|
<Grid size={{ xs: 12, sm: isMotor ? 8 : 4 }}>
|
|
{sinkSelector()}
|
|
</Grid>
|
|
{isMotor && (
|
|
<Grid size={{ xs: 12, sm: 5 }} container justifyContent="center">
|
|
<TextField
|
|
select
|
|
id="mode"
|
|
label=""
|
|
value={result.mode}
|
|
onChange={changeResultMode}
|
|
fullWidth
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
InputLabelProps={{ shrink: true }}
|
|
disabled={!canEdit}>
|
|
<MenuItem key={"Clockwise"} value={0}>
|
|
Clockwise
|
|
</MenuItem>
|
|
<MenuItem key={"Counter Clockwise"} value={1}>
|
|
Counter Clockwise
|
|
</MenuItem>
|
|
</TextField>
|
|
</Grid>
|
|
)}
|
|
<Grid size={{ xs: 2, sm: 1 }}>
|
|
<Typography variant="subtitle1" color="textSecondary" className={classes.forPrompt}>
|
|
for
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 10, sm: isMotor ? 6 : 4 }} container justifyContent="center">
|
|
<TextField
|
|
id="runDuration"
|
|
required={true}
|
|
label=""
|
|
helperText=""
|
|
error={invalidResultValue()}
|
|
value={rawResultValue}
|
|
onChange={changeResultValue}
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
fullWidth
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">sec</InputAdornment>
|
|
}}
|
|
InputLabelProps={{ shrink: true }}
|
|
disabled={!canEdit}
|
|
/>
|
|
</Grid>
|
|
</React.Fragment>
|
|
)}
|
|
{isResultType(Result.set) && (
|
|
<React.Fragment>
|
|
<Grid size={{ xs: 12, sm: 4 }}>
|
|
{sinkSelector()}
|
|
</Grid>
|
|
{interaction.settings.sink?.type !==
|
|
quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && (
|
|
<Grid size={{ xs: 2, sm: 1 }}>
|
|
<Typography
|
|
variant="subtitle1"
|
|
color="textSecondary"
|
|
className={classes.forPrompt}>
|
|
to
|
|
</Typography>
|
|
</Grid>
|
|
)}
|
|
{interaction.settings.sink?.type !==
|
|
quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && (
|
|
<Grid size={{ xs: 10, sm: 3 }} container justifyContent="center">
|
|
<TextField
|
|
select
|
|
id="resultValue"
|
|
label=""
|
|
value={result.value}
|
|
onChange={changeResultValue}
|
|
fullWidth
|
|
autoFocus={false}
|
|
margin="dense"
|
|
variant="standard"
|
|
InputLabelProps={{ shrink: true }}
|
|
disabled={!canEdit}>
|
|
{setMenuItems()}
|
|
</TextField>
|
|
</Grid>
|
|
)}
|
|
</React.Fragment>
|
|
)}
|
|
{isResultType(Result.toggle) && (
|
|
<React.Fragment>
|
|
<Grid size={{ xs: 12, sm: 5 }}>
|
|
{sinkSelector()}
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 3 }} container justifyContent="center">
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
id="resultValue"
|
|
checked={result.value === 1}
|
|
onChange={changeResultValue}
|
|
value="toggleResultValue"
|
|
color="secondary"
|
|
/>
|
|
}
|
|
label={describeSink().enumerations()[result.value]}
|
|
disabled={!canEdit}
|
|
/>
|
|
</Grid>
|
|
</React.Fragment>
|
|
)}
|
|
{hasDeviceFeature(device.settings.upgradeChannel, "better-controls") &&
|
|
isRun &&
|
|
dutyCycleInput()}
|
|
{notificationInput()}
|
|
</Grid>
|
|
</FormControl>
|
|
);
|
|
};
|
|
|
|
const daySelector = (day: string, size: any) => {
|
|
const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create());
|
|
return (
|
|
<Grid container size={{ xs: size, sm: 1 }} justifyContent="center" alignItems="center">
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
id={day}
|
|
checked={schedule.weekdays.includes(day)}
|
|
onChange={event => toggleDaySelected(day, event)}
|
|
value={day}
|
|
color="secondary"
|
|
/>
|
|
}
|
|
label={capitalize(day.substr(0, 2))}
|
|
labelPlacement="bottom"
|
|
disabled={!canEdit}
|
|
/>
|
|
</Grid>
|
|
);
|
|
};
|
|
|
|
const scheduleInput = () => {
|
|
let schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create());
|
|
let timezone = coalesce(schedule.timezone, moment.tz.guess());
|
|
let todStart = coalesce(schedule.timeOfDayStart, "00:00").split(":");
|
|
let todEnd = coalesce(schedule.timeOfDayEnd, "23:59").split(":");
|
|
let start = moment
|
|
.tz(timezone)
|
|
.startOf("day")
|
|
.add(todStart[0], "hours")
|
|
.add(todStart[1], "minutes")
|
|
.local();
|
|
let end = moment
|
|
.tz(timezone)
|
|
.startOf("day")
|
|
.add(todEnd[0], "hours")
|
|
.add(todEnd[1], "minutes")
|
|
.local();
|
|
let descriptor = timeOfDayDescriptor(schedule);
|
|
let showStart = descriptor === "after" || descriptor === "from";
|
|
let showEnd = descriptor === "before" || descriptor === "from";
|
|
let showBoth = showStart && showEnd;
|
|
return (
|
|
<FormControl
|
|
component={"fieldset" as "div"}
|
|
className={classes.borderedContainer}
|
|
fullWidth
|
|
disabled={!canEdit}>
|
|
<FormLabel component={"legend" as "caption"}>Schedule</FormLabel>
|
|
<Grid container direction="row" spacing={0} justifyContent="flex-start">
|
|
{daySelector("sunday", 3)}
|
|
{daySelector("monday", 3)}
|
|
{daySelector("tuesday", 3)}
|
|
{daySelector("wednesday", 3)}
|
|
{daySelector("thursday", 4)}
|
|
{daySelector("friday", 4)}
|
|
{daySelector("saturday", 4)}
|
|
</Grid>
|
|
<Grid container direction="row" spacing={2} alignItems="center">
|
|
<Grid size={{ xs: 12, sm: 3 }}>
|
|
<TextField
|
|
select
|
|
id="timeOfDayShortcut"
|
|
value={descriptor}
|
|
onChange={(event: any) => setScheduleTime("shortcut", event)}
|
|
fullWidth
|
|
autoFocus={false}
|
|
margin="normal"
|
|
variant="standard"
|
|
InputLabelProps={{ shrink: true }}>
|
|
<MenuItem key="allDay" value="allDay">
|
|
All Day
|
|
</MenuItem>
|
|
<MenuItem key="before" value="before">
|
|
Before
|
|
</MenuItem>
|
|
<MenuItem key="after" value="after">
|
|
After
|
|
</MenuItem>
|
|
<MenuItem key="from" value="from">
|
|
From
|
|
</MenuItem>
|
|
</TextField>
|
|
</Grid>
|
|
{showStart && (
|
|
<Grid size={{ xs: 5, sm: 3 }}>
|
|
<LocalizationProvider dateAdapter={AdapterMoment}>
|
|
<TimePicker
|
|
value={start}
|
|
onChange={(event) => setScheduleTime("start", event)}
|
|
slotProps={{
|
|
textField: (props) => <TextField {...props} helperText="" />,
|
|
}}
|
|
/>
|
|
</LocalizationProvider>
|
|
</Grid>
|
|
)}
|
|
{showBoth && (
|
|
<Grid size={{ xs: 2, sm: 1 }}>
|
|
<Typography
|
|
variant="subtitle1"
|
|
color="textSecondary"
|
|
className={classes.timeRange}
|
|
align="right">
|
|
to
|
|
</Typography>
|
|
</Grid>
|
|
)}
|
|
{showEnd && (
|
|
<Grid size={{ xs: 5, sm: 3 }}>
|
|
<LocalizationProvider dateAdapter={AdapterMoment}>
|
|
<TimePicker
|
|
value={end}
|
|
onChange={(event: any) => setScheduleTime("end", event)}
|
|
slotProps={{
|
|
textField: (props) => <TextField {...props} helperText="" />,
|
|
}}
|
|
/>
|
|
</LocalizationProvider>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
</FormControl>
|
|
);
|
|
};
|
|
|
|
const content = () => {
|
|
return (
|
|
<Grid container direction="row" spacing={2}>
|
|
<Grid size={{ xs: 12 }}>
|
|
{sourceInput()}
|
|
</Grid>
|
|
{hasDeviceFeature(device.settings.upgradeChannel, "better-controls") && (
|
|
<Grid size={{ xs: 12 }}>
|
|
<div style={{ display: "flex", flexDirection: "row" }}>
|
|
{priorityInput()}
|
|
<div style={{ marginLeft: theme.spacing(2) }}></div>
|
|
{sortingInput()}
|
|
</div>
|
|
</Grid>
|
|
)}
|
|
<Grid size={{ xs: 12 }}>
|
|
{conditionInput()}
|
|
</Grid>
|
|
<Grid size={{ xs: 12 }}>
|
|
{resultInput()}
|
|
</Grid>
|
|
<Grid size={{ xs: 12 }} >
|
|
{scheduleInput()}
|
|
</Grid>
|
|
</Grid>
|
|
);
|
|
};
|
|
|
|
const actions = () => {
|
|
return (
|
|
<Grid container justifyContent="space-between" direction="row" width={"100%"}>
|
|
<Grid size={{ xs: 5 }}>
|
|
{mode === "update" && canEdit && (
|
|
<DeleteButton onClick={openRemoveInteraction}>Delete</DeleteButton>
|
|
)}
|
|
</Grid>
|
|
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
|
|
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
|
|
{/* <Button onClick={close} color="primary">
|
|
Cancel
|
|
</Button>
|
|
{canEdit && (
|
|
<Button
|
|
onClick={submit}
|
|
color="primary"
|
|
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
|
|
Submit
|
|
</Button>
|
|
)} */}
|
|
</Grid>
|
|
</Grid>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ResponsiveDialog
|
|
fullWidth
|
|
open={isDialogOpen}
|
|
onClose={close}
|
|
aria-labelledby="interaction-settings-dialog">
|
|
<DialogTitle id="interaction-settings-title">{title()}</DialogTitle>
|
|
<Divider />
|
|
<DialogContent className={classes.dialogContent}>{content()}</DialogContent>
|
|
<Divider />
|
|
<DialogActions>{actions()}</DialogActions>
|
|
{mode === "update" && (
|
|
<RemoveInteraction
|
|
device={device}
|
|
interaction={interaction}
|
|
isDialogOpen={isRemoveInteractionOpen}
|
|
closeDialogCallback={closeRemoveInteraction}
|
|
refreshCallback={interactionRemoved}
|
|
/>
|
|
)}
|
|
</ResponsiveDialog>
|
|
);
|
|
}
|