Skip to content

Commit

Permalink
Merge pull request #20 from bcgov/dev
Browse files Browse the repository at this point in the history
Releasing dev to production
  • Loading branch information
timwekkenbc authored Jul 30, 2024
2 parents 732edcd + 91d6959 commit 0cd98cd
Show file tree
Hide file tree
Showing 35 changed files with 2,452 additions and 1,485 deletions.
5 changes: 3 additions & 2 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ export default function Admin() {
title: "GoRules JSON Filename",
dataIndex: "goRulesJSONFilename",
render: renderInputField("goRulesJSONFilename"),
width: "260px",
},
{
dataIndex: "delete",
width: "60px",
render: (value: string, _: RuleInfo, index: number) => (
<Button danger onClick={() => deleteRule(index)}>
Delete
Expand All @@ -119,6 +119,7 @@ export default function Admin() {
},
{
dataIndex: "view",
width: "60px",
render: (_: string, { _id }: RuleInfo) => (
<Link href={`/rule/${_id}`}>
<Button>View</Button>
Expand All @@ -135,7 +136,7 @@ export default function Admin() {
</Link>
<h1>Admin</h1>
{!isLoading && (
<Button type="primary" size="large" onClick={saveAllRuleUpdates}>
<Button type="primary" danger onClick={saveAllRuleUpdates}>
Save Changes
</Button>
)}
Expand Down
206 changes: 206 additions & 0 deletions app/components/InputStyler/ArrayFormatter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { Input, Radio, AutoComplete, InputNumber, Button } from "antd";
import { Scenario } from "@/app/types/scenario";
import { getAutoCompleteOptions, rawDataProps } from "./InputStyler";

interface TemplateObject {
[key: string]: any;
}

interface ParsedSchema {
arrayName: string;
objectTemplate: TemplateObject;
}

export const parseSchemaTemplate = (template: string): ParsedSchema | null => {
if (!template) return null;
const match = template.match(/(\w+)\[\{(.*)\}\]/);
if (!match) {
return null;
}

const arrayName = match[1];
const properties = match[2].split(",").map((prop) => prop.trim());

const objectTemplate: TemplateObject = {};
properties.forEach((prop) => {
const [propertyName, propertyType] = prop.split(":");
switch (propertyType.toLowerCase()) {
case "string":
objectTemplate[propertyName] = "";
break;
case "boolean":
objectTemplate[propertyName] = false;
break;
case "number":
objectTemplate[propertyName] = 0;
break;
default:
objectTemplate[propertyName] = undefined;
break;
}
});

return { arrayName, objectTemplate };
};

export const generateArrayFromSchema = (template: string, initialSize: number = 1): TemplateObject[] | null => {
if (!template || typeof template !== "string") return null;
const { objectTemplate } = parseSchemaTemplate(template) ?? {};
if (!objectTemplate) return null;

const array: TemplateObject[] = [];
for (let i = 0; i < initialSize; i++) {
array.push({ ...objectTemplate });
}

return array;
};

export default function ArrayFormatter(
value: any,
property: string,
editable: boolean,
scenarios: Scenario[] = [],
rawData: rawDataProps | null | undefined,
setRawData: any
) {
const valuesArray = getAutoCompleteOptions(property, scenarios);
let type = typeof value;
if (valuesArray.length > 0) {
type = typeof valuesArray[0].value;
}

const handleArrayInputItemChange = (arrayName: string, index: number, key: string, newValue: any) => {
const queryValue = newValue;

const updatedData: rawDataProps = { ...rawData };
if (updatedData) {
if (!updatedData[arrayName]) {
updatedData[arrayName] = [];
}
if (!updatedData[arrayName][index]) {
updatedData[arrayName][index] = {};
}
updatedData[arrayName][index][key] = queryValue;
setRawData(updatedData);
}
};

const handleArrayItemChange = (arrayName: string, index: number, key: string, newValue: any) => {
let queryValue: any = newValue;
if (typeof newValue === "string") {
if (newValue.toLowerCase() === "true") {
queryValue = true;
} else if (newValue.toLowerCase() === "false") {
queryValue = false;
} else if (!isNaN(Number(newValue))) {
queryValue = Number(newValue);
}
}

const updatedData: rawDataProps = { ...rawData };
if (updatedData) {
if (!updatedData[arrayName]) {
updatedData[arrayName] = [];
}
if (!updatedData[arrayName][index]) {
updatedData[arrayName][index] = {};
}
updatedData[arrayName][index][key] = queryValue;
setRawData(updatedData);
}
};

const parsedValue = generateArrayFromSchema(property);
const parsedSchema = parseSchemaTemplate(property);
const parsedPropertyName = parsedSchema?.arrayName || property;

// Utility function to get value from a nested object using a path
const getValueFromPath = (property: string, path: { [key: string]: any }) => {
if (path.hasOwnProperty(property)) {
return path[property];
} else {
return null;
}
};

// Utility function to set value at a path in a nested object
const setValueAtPath = (obj: any, path: (string | number)[], value: any): any => {
if (path.length === 0) return value;
const [first, ...rest] = path;
const newObj = typeof first === "number" ? [] : {};
return {
...obj,
[first]: rest.length ? setValueAtPath(obj[first] || newObj, rest, value) : value,
};
};

// Utility function to add a copy of an object in an array
const addCopyInArray = (arrayPath: string, parsedValue: any) => {
const currentArray = rawData ? getValueFromPath(arrayPath, rawData) || [] : [];
const newItem = generateArrayFromSchema(property)?.[0] ?? parsedValue; // Adjusting for potential null or undefined
if (newItem !== null && newItem !== undefined) {
currentArray.push(newItem);
}
const newData = { ...rawData, [arrayPath]: currentArray };
setRawData(newData);
};

if (editable) {
if (Array.isArray(parsedValue)) {
const customName = (parsedPropertyName.charAt(0).toUpperCase() + parsedPropertyName.slice(1)).slice(0, -1);
return (
<div>
<Button onClick={() => addCopyInArray(parsedPropertyName, parsedValue[0])}>Add {customName}</Button>
{(rawData?.[parsedPropertyName] || []).map(
(item: { [s: string]: unknown } | ArrayLike<unknown>, index: number) => (
<div key={index}>
<h4>
{customName} {index ? index + 1 : "1"}
</h4>
{Object.entries(item).map(([key, val]) => (
<div key={key}>
<label className="labelsmall">
{key}:
{typeof val === "boolean" ? (
<Radio.Group
onChange={(e) => handleArrayInputItemChange(parsedPropertyName, index, key, e.target.value)}
value={val}
>
<Radio value={true}>Yes</Radio>
<Radio value={false}>No</Radio>
</Radio.Group>
) : typeof val === "number" ? (
<InputNumber
value={val}
onBlur={(e) => handleArrayItemChange(parsedPropertyName, index, key, e.target.value)}
onChange={(newVal) => handleArrayInputItemChange(parsedPropertyName, index, key, newVal)}
/>
) : typeof val === "string" ? (
<AutoComplete
options={getAutoCompleteOptions(key)}
value={val}
onBlur={(e) =>
handleArrayItemChange(parsedPropertyName, index, key, (e.target as HTMLInputElement).value)
}
style={{ width: 200 }}
onChange={(newVal) => handleArrayInputItemChange(parsedPropertyName, index, key, newVal)}
/>
) : (
<Input onBlur={(e) => handleArrayItemChange(parsedPropertyName, index, key, e.target.value)} />
)}
</label>
</div>
))}
</div>
)
)}
</div>
);
} else {
return <></>;
}
} else {
return <></>;
}
}
73 changes: 57 additions & 16 deletions app/components/InputStyler/InputStyler.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { Tag, Input, Radio, AutoComplete, InputNumber, Flex } from "antd";
import ArrayFormatter, { parseSchemaTemplate, generateArrayFromSchema } from "./ArrayFormatter";
import { Scenario } from "@/app/types/scenario";

export default function inputStyler(
export interface rawDataProps {
[key: string]: any;
rulemap?: boolean;
}

export const getAutoCompleteOptions = (property: string, scenarios: Scenario[] = []) => {
if (!scenarios) return [];
const optionsSet = new Set<string>();

scenarios.forEach((scenario) => {
scenario.variables
.filter((variable) => variable.name === property)
.forEach((variable) => optionsSet.add(variable.value));
});

return Array.from(optionsSet).map((value) => ({ value, type: typeof value }));
};

export default function InputStyler(
value: any,
property: string,
editable: boolean,
scenarios: Scenario[] = [],
rawData: object = {},
rawData: rawDataProps | null | undefined,
setRawData: any
) {
const getAutoCompleteOptions = (property: string) => {
if (!scenarios) return [];
const optionsSet = new Set<string>();

scenarios.forEach((scenario) => {
scenario.variables
.filter((variable) => variable.name === property)
.forEach((variable) => optionsSet.add(variable.value));
});

return Array.from(optionsSet).map((value) => ({ value, type: typeof value }));
};

const handleValueChange = (value: any, property: string) => {
let queryValue: any = value;
if (typeof value === "string") {
Expand Down Expand Up @@ -50,13 +56,23 @@ export default function inputStyler(
}
};

const valuesArray = getAutoCompleteOptions(property);
const valuesArray = getAutoCompleteOptions(property, scenarios);
let type = typeof value;
if (valuesArray.length > 0) {
type = typeof valuesArray[0].value;
}

const parsedValue = generateArrayFromSchema(property);
const parsedSchema = parseSchemaTemplate(property);
const parsedPropertyName = parsedSchema?.arrayName || property;

if (editable) {
if (Array.isArray(parsedValue)) {
return ArrayFormatter(value, property, editable, scenarios, rawData, setRawData);
}
if (typeof value === "object" && value !== null && !Array.isArray(property) && property !== null) {
return <div>{Object.keys(value).length}</div>;
}
if (type === "boolean" || typeof value === "boolean") {
return (
<Flex gap={"small"} align="center" vertical>
Expand Down Expand Up @@ -108,6 +124,31 @@ export default function inputStyler(
);
}
} else {
if (value !== null && Array.isArray(value)) {
const customName = (parsedPropertyName.charAt(0).toUpperCase() + parsedPropertyName.slice(1)).slice(0, -1);
return (
<div>
{(rawData?.[parsedPropertyName] || []).map(
(item: { [s: string]: unknown } | ArrayLike<unknown>, index: number) => (
<div key={index}>
<h4>
{customName} {index + 1}
</h4>
{Object.entries(item).map(([key, val]) => (
<div key={key}>
<label className="labelsmall">
{key}

{InputStyler(val, key, false, scenarios, rawData, setRawData)}
</label>
</div>
))}
</div>
)
)}
</div>
);
}
if (type === "boolean" || typeof value === "boolean") {
return (
<Radio.Group onChange={() => null} value={value}>
Expand Down
14 changes: 9 additions & 5 deletions app/components/RuleHeader/RuleHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import { updateRuleData } from "@/app/utils/api";
import styles from "./RuleHeader.module.css";

export default function RuleHeader({ ruleInfo }: { ruleInfo: RuleInfo }) {
const { title, goRulesJSONFilename } = ruleInfo;

const [savedTitle, setSavedTitle] = useState(title || goRulesJSONFilename);
const [savedTitle, setSavedTitle] = useState("");
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [currTitle, setCurrTitle] = useState(savedTitle);
const [currTitle, setCurrTitle] = useState("");
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
const { title, goRulesJSONFilename } = ruleInfo;
setSavedTitle(title || goRulesJSONFilename);
setCurrTitle(title || goRulesJSONFilename);
}, [ruleInfo]);

useEffect(() => {
if (isEditingTitle) {
inputRef.current?.focus();
Expand All @@ -39,7 +43,7 @@ export default function RuleHeader({ ruleInfo }: { ruleInfo: RuleInfo }) {
setSavedTitle(currTitle);
} catch (e) {
// If updating fails, revert to previous title name
setCurrTitle(title || goRulesJSONFilename);
setCurrTitle(savedTitle);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.ruleSelect {
min-width: 350px;
margin-bottom: 16px;
}

.linkDrawer {
width: 85% !important;
}
Loading

0 comments on commit 0cd98cd

Please sign in to comment.