PorterFormContextProvider.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import React, { createContext, useReducer } from "react";
  2. import {
  3. PorterFormData,
  4. PorterFormState,
  5. PorterFormAction,
  6. PorterFormVariableList,
  7. PorterFormValidationInfo,
  8. GetFinalVariablesFunction,
  9. } from "./types";
  10. import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
  11. import { getFinalVariablesForStringInput } from "./field-components/Input";
  12. import { getFinalVariablesForKeyValueArray } from "./field-components/KeyValueArray";
  13. interface Props {
  14. rawFormData: PorterFormData;
  15. onSubmit: (vars: PorterFormVariableList) => void;
  16. initialVariables?: PorterFormVariableList;
  17. overrideVariables?: PorterFormVariableList;
  18. isReadOnly?: boolean;
  19. }
  20. interface ContextProps {
  21. formData: PorterFormData;
  22. formState: PorterFormState;
  23. onSubmit: () => void;
  24. dispatchAction: (event: PorterFormAction) => void;
  25. validationInfo: PorterFormValidationInfo;
  26. isReadOnly?: boolean;
  27. }
  28. export const PorterFormContext = createContext<ContextProps | undefined>(
  29. undefined!
  30. );
  31. const { Provider } = PorterFormContext;
  32. export const PorterFormContextProvider: React.FC<Props> = (props) => {
  33. const handleAction = (
  34. state: PorterFormState,
  35. action: PorterFormAction
  36. ): PorterFormState => {
  37. switch (action.type) {
  38. case "init-field":
  39. if (!(action.id in state.components)) {
  40. return {
  41. ...state,
  42. variables: {
  43. ...state.variables,
  44. ...action.initVars,
  45. },
  46. components: {
  47. ...state.components,
  48. [action.id]: {
  49. state: action.initValue,
  50. validation: {
  51. ...{
  52. validated: false,
  53. },
  54. ...action.initValidation,
  55. },
  56. },
  57. },
  58. };
  59. }
  60. break;
  61. case "update-field":
  62. return {
  63. ...state,
  64. components: {
  65. ...state.components,
  66. [action.id]: {
  67. ...state.components[action.id],
  68. state: {
  69. ...state.components[action.id].state,
  70. ...action.updateFunc(state.components[action.id].state),
  71. },
  72. },
  73. },
  74. };
  75. case "update-validation":
  76. return {
  77. ...state,
  78. components: {
  79. ...state.components,
  80. [action.id]: {
  81. ...state.components[action.id],
  82. validation: action.updateFunc(
  83. state.components[action.id].validation
  84. ),
  85. },
  86. },
  87. };
  88. case "mutate-vars":
  89. return {
  90. ...state,
  91. variables: {
  92. ...action.mutateFunc(state.variables),
  93. ...props.overrideVariables,
  94. },
  95. };
  96. }
  97. return state;
  98. };
  99. const [state, dispatch] = useReducer(handleAction, {
  100. components: {},
  101. variables: props.initialVariables || {},
  102. });
  103. const evalShowIf = (
  104. vals: ShowIf,
  105. variables: PorterFormVariableList
  106. ): boolean => {
  107. if (!vals) {
  108. return false;
  109. }
  110. if (typeof vals == "string") {
  111. return !!variables[vals];
  112. }
  113. if ((vals as ShowIfOr).or) {
  114. vals = vals as ShowIfOr;
  115. for (let i = 0; i < vals.or.length; i++) {
  116. if (evalShowIf(vals.or[i], variables)) {
  117. return true;
  118. }
  119. }
  120. return false;
  121. }
  122. if ((vals as ShowIfAnd).and) {
  123. vals = vals as ShowIfAnd;
  124. for (let i = 0; i < vals.and.length; i++) {
  125. if (!evalShowIf(vals.and[i], variables)) {
  126. return false;
  127. }
  128. }
  129. return true;
  130. }
  131. if ((vals as ShowIfNot).not) {
  132. vals = vals as ShowIfNot;
  133. return !evalShowIf(vals.not, variables);
  134. }
  135. return false;
  136. };
  137. /*
  138. Takes in old form data and changes it to use newer fields
  139. For example, number-input becomes input with a setting that makes it
  140. a number input
  141. */
  142. const restructureToNewFields = (data: PorterFormData) => {
  143. return {
  144. ...data,
  145. tabs: data.tabs.map((tab) => {
  146. return {
  147. ...tab,
  148. sections: tab.sections.map((section) => {
  149. return {
  150. ...section,
  151. contents: section.contents.map((field: any) => {
  152. if (field.type == "number-input") {
  153. return {
  154. ...field,
  155. type: "input",
  156. settings: {
  157. ...field.settings,
  158. type: "number",
  159. },
  160. };
  161. }
  162. if (field.type == "string-input") {
  163. return {
  164. ...field,
  165. type: "input",
  166. settings: {
  167. ...field.settings,
  168. type: "string",
  169. },
  170. };
  171. }
  172. if (field.type == "string-input-password") {
  173. return {
  174. ...field,
  175. type: "input",
  176. settings: {
  177. ...field.settings,
  178. type: "password",
  179. },
  180. };
  181. }
  182. return field;
  183. }),
  184. };
  185. }),
  186. };
  187. }),
  188. };
  189. };
  190. /*
  191. We don't want to have the actual <PorterForm> component to do as little form
  192. logic as possible, so this structures the form object based on show_if statements
  193. and assigns a unique id to each field
  194. This computed structure also later lets us figure out which fields should be required
  195. */
  196. const computeFormStructure = (
  197. data: PorterFormData,
  198. variables: PorterFormVariableList
  199. ) => {
  200. return {
  201. ...data,
  202. tabs: data.tabs.map((tab, i) => {
  203. return {
  204. ...tab,
  205. sections: tab.sections
  206. .map((section, j) => {
  207. return {
  208. ...section,
  209. contents: section.contents.map((field, k) => {
  210. return {
  211. ...field,
  212. id: `${i}-${j}-${k}`,
  213. };
  214. }),
  215. };
  216. })
  217. .filter((section) => {
  218. return !section.show_if || evalShowIf(section.show_if, variables);
  219. }),
  220. };
  221. }),
  222. };
  223. };
  224. /*
  225. compute a list of field ids who's input is required and a map from a variable value
  226. to a list of fields that set it
  227. */
  228. const computeRequiredVariables = (
  229. data: PorterFormData
  230. ): [string[], Record<string, string[]>] => {
  231. const requiredIds: string[] = [];
  232. const mapping: Record<string, string[]> = {};
  233. data.tabs.map((tab) =>
  234. tab.sections.map((section) =>
  235. section.contents.map((field) => {
  236. if (field.type == "heading" || field.type == "subtitle") return;
  237. if (field.required) {
  238. requiredIds.push(field.id);
  239. }
  240. if (!mapping[field.variable]) {
  241. mapping[field.variable] = [];
  242. }
  243. mapping[field.variable].push(field.id);
  244. })
  245. )
  246. );
  247. return [requiredIds, mapping];
  248. };
  249. /*
  250. Validate the form based on a list of required ids
  251. */
  252. const doValidation = (requiredIds: string[]) =>
  253. requiredIds
  254. .map((id) => state.components[id]?.validation.validated)
  255. .every((x) => x);
  256. const formData = computeFormStructure(
  257. restructureToNewFields(props.rawFormData),
  258. state.variables
  259. );
  260. const [requiredIds, varMapping] = computeRequiredVariables(formData);
  261. const isValidated = doValidation(requiredIds);
  262. /*
  263. Handle submit
  264. This involves going through all the (currently active) fields in the form and
  265. using functions for each input to finalize the variables
  266. This can take care of things like appending units to strings
  267. */
  268. const onSubmitWrapper = () => {
  269. // we start off with a base list of the current variables for fields
  270. // that don't need any processing on top (for example: checkbox)
  271. // the assign here is important because that way state.variable isn't mutated
  272. const varList: PorterFormVariableList[] = [
  273. Object.assign({}, state.variables),
  274. ];
  275. const finalFunctions: Record<string, GetFinalVariablesFunction> = {
  276. "string-input": getFinalVariablesForStringInput,
  277. "key-value-array": getFinalVariablesForKeyValueArray,
  278. };
  279. formData.tabs.map((tab) =>
  280. tab.sections.map((section) =>
  281. section.contents.map((field) => {
  282. if (finalFunctions[field.type])
  283. varList.push(
  284. finalFunctions[field.type](
  285. state.variables,
  286. field,
  287. state.components[field.id].state
  288. )
  289. );
  290. })
  291. )
  292. );
  293. props.onSubmit(Object.assign.apply({}, varList));
  294. };
  295. console.group("Validation Info:");
  296. console.log(requiredIds);
  297. console.log(varMapping);
  298. console.log(isValidated);
  299. console.groupEnd();
  300. return (
  301. <Provider
  302. value={{
  303. formData: formData,
  304. formState: state,
  305. dispatchAction: dispatch,
  306. isReadOnly: props.isReadOnly,
  307. validationInfo: {
  308. validated: isValidated,
  309. error: isValidated ? null : "Missing required fields",
  310. },
  311. onSubmit: onSubmitWrapper,
  312. }}
  313. >
  314. {props.children}
  315. </Provider>
  316. );
  317. };