Skip to content

Commit

Permalink
Merge pull request #322 from WFrankishOCC/feature/296-element-identif…
Browse files Browse the repository at this point in the history
…iers

Add element identifiers
  • Loading branch information
nbarlowATI committed Dec 19, 2023
2 parents eba9b75 + 7763be9 commit b26aa1a
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 59 deletions.
153 changes: 150 additions & 3 deletions frontend/src/components/CaseContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ import CasePermissionsManager from "./CasePermissionsManager.js";
import MermaidChart from "./Mermaid";
import EditableText from "./EditableText.js";
import ItemViewer from "./ItemViewer.js";
import ItemEditor from "./ItemEditor.js";
import ItemEditor, { postItemUpdate } from "./ItemEditor.js";
import ItemCreator from "./ItemCreator.js";
import memoize from "memoize-one";

import { getBaseURL, jsonToMermaid, getSelfUser } from "./utils.js";
import {
getBaseURL,
jsonToMermaid,
getSelfUser,
visitCaseItem,
getParentPropertyClaims,
} from "./utils.js";
import configData from "../config.json";
import "./CaseContainer.css";

Expand All @@ -37,6 +43,8 @@ class CaseContainer extends Component {

constructMarkdownMemoised = memoize(jsonToMermaid);

getIdListMemoised = memoize(updateIdList);

constructor(props) {
super(props);
this.state = {
Expand All @@ -61,6 +69,8 @@ class CaseContainer extends Component {
/** @type {string[]} */
collapsedNodes: [],
metadata: null,
/** @type {Set<string>} */
identifiers: new Set(),
};

this.url = `${getBaseURL()}/cases/`;
Expand Down Expand Up @@ -228,7 +238,7 @@ class CaseContainer extends Component {
const id = this.props.params.caseSlug;
const oldId = prevProps.params.caseSlug;
if (id !== oldId) {
this.setState({ id: id }, this.updateView);
this.setState({ id: id, identifiers: new Set() }, this.updateView);
}
}

Expand Down Expand Up @@ -321,6 +331,125 @@ class CaseContainer extends Component {
this.setState({ showEditLayer: false, itemType: null, itemId: null });
}

/**
* @param {string} type
* @param {string} parentId
* @param {string} parentType
* @returns {string}
*/
getIdForNewElement(type, parentId, parentType) {
const newList = new Set([
...this.state.identifiers,
...this.getIdListMemoised(this.state.assurance_case),
]);

this.setState({ idList: newList });

let prefix = configData.navigation[type].db_name
.substring(0, 1)
.toUpperCase();

if (type === "PropertyClaim") {
const parents = getParentPropertyClaims(
this.state.assurance_case,
parentId,
parentType,
);
if (parents.length > 0) {
const parent = parents[parents.length - 1];
prefix = parent.name + ".";
}
}

let i = 1;
while (newList.has(prefix + i)) {
i++;
}

return prefix + i;
}

updateAllIdentifiers() {
this.setState({ loading: true });

const promises = [];

const identifiers = new Set();

const foundEvidence = new Set();

function updateItem(item, type, parents) {
let prefix = configData.navigation[type].db_name
.substring(0, 1)
.toUpperCase();

if (type === "PropertyClaim") {
const claimParents = parents.filter((t) => t.type === "PropertyClaim");
if (claimParents.length > 0) {
const parent = claimParents[claimParents.length - 1];
prefix = parent.name + ".";
}
}

let i = 1;
while (identifiers.has(prefix + i)) {
i++;
}

if (item.name === prefix + i) {
// don't need to post an update
identifiers.add(item.name);
return [item, type, parents];
}

const itemCopy = { ...item };
itemCopy.name = prefix + i;
identifiers.add(itemCopy.name);
promises.push(postItemUpdate(item.id, type, itemCopy));

return [itemCopy, type, parents];
}

// run breadth first search
/** @type [any, string, any[]] */
const caseItemQueue = this.state.assurance_case.goals.map((i) =>
updateItem(i, "TopLevelNormativeGoal", []),
);

while (caseItemQueue.length > 0) {
const [node, nodeType, parents] = caseItemQueue.shift();
const newParents = [...parents, node];

configData.navigation[nodeType]["children"].forEach((childName, j) => {
const childType = configData.navigation[nodeType]["children"][j];
const dbName = configData.navigation[childName]["db_name"];
if (Array.isArray(node[dbName])) {
node[dbName].forEach((child) => {
if (childType === "Evidence" && foundEvidence.has(child.id)) {
// already found this, skip
return;
}

caseItemQueue.push(updateItem(child, childType, newParents));
if (childType === "Evidence") {
foundEvidence.add(child.id);
}
});
}
});
}

this.setState({ identifiers });

if (promises.length === 0) {
this.setState({ loading: false });
} else {
Promise.all(promises).then(() => {
this.updateView();
});
}
}

hideCreateLayer() {
this.setState({
showCreateLayer: false,
Expand Down Expand Up @@ -407,6 +536,7 @@ class CaseContainer extends Component {
parentId={this.state.createItemParentId}
parentType={this.state.createItemParentType}
updateView={this.updateView.bind(this)}
getId={this.getIdForNewElement.bind(this)}
/>
</Box>
</Box>
Expand Down Expand Up @@ -502,6 +632,7 @@ class CaseContainer extends Component {
parentId={this.state.id}
parentType="AssuranceCase"
updateView={this.updateView.bind(this)}
getId={this.getIdForNewElement.bind(this)}
/>
}
/>
Expand Down Expand Up @@ -660,6 +791,11 @@ class CaseContainer extends Component {
secondary
onClick={this.exportCurrentCaseAsSVG.bind(this)}
/>
<Button
label="Reset names"
secondary
onClick={this.updateAllIdentifiers.bind(this)}
/>
</Box>
</Box>
<Grid
Expand Down Expand Up @@ -778,6 +914,17 @@ class CaseContainer extends Component {
}
}

/** @returns {string[]} */
function updateIdList(assuranceCase) {
const set = [];
assuranceCase.goals.forEach((goal) => {
visitCaseItem(goal, (item) => {
set.push(item.name);
});
});
return set;
}

// eslint-disable-next-line import/no-anonymous-default-export
export default (props) => (
<CaseContainer {...props} params={useParams()} navigate={useNavigate()} />
Expand Down
11 changes: 2 additions & 9 deletions frontend/src/components/ItemCreator.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { getBaseURL } from "./utils.js";
import configData from "../config.json";

function ItemCreator(props) {
const [name, setName] = useState("Name");
const [sdesc, setShortDesc] = useState("Short description");
const [ldesc, setLongDesc] = useState("Long description");
const [keywords, setKeywords] = useState("Keywords (comma-separated)");
Expand Down Expand Up @@ -44,7 +43,8 @@ function ItemCreator(props) {
}/`;

let request_body = {};
request_body["name"] = name;
const id = props.getId(props.type, props.parentId, props.parentType);
request_body["name"] = id;
request_body["short_description"] = sdesc;
request_body["long_description"] = ldesc;
request_body["keywords"] = keywords;
Expand Down Expand Up @@ -88,13 +88,6 @@ function ItemCreator(props) {
<Box className="dropdown" pad="small">
<Heading level={3}>Create a new {props.type}</Heading>
<Form onSubmit={handleSubmit}>
<FormField>
<TextInput
placeholder={name}
name="name"
onChange={(e) => setName(e.target.value)}
/>
</FormField>
<FormField>
<TextInput
placeholder={sdesc}
Expand Down
72 changes: 38 additions & 34 deletions frontend/src/components/ItemEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,32 +69,8 @@ function ItemEditor(props) {
editDBObject().then(() => props.updateView());
}

async function editDBObject() {
let backendURL = `${getBaseURL()}/${
configData.navigation[props.type]["api_name"]
}/${props.id}/`;

let request_body = {};
request_body["name"] = items.name;
request_body["short_description"] = items.short_description;
request_body["long_description"] = items.long_description;
request_body["keywords"] = items.keywords;
if (props.type === "PropertyClaim") {
request_body["claim_type"] = items.claim_type;
}
if (props.type === "Evidence") {
request_body["URL"] = items.URL;
}

const requestOptions = {
method: "PUT",
headers: {
Authorization: `Token ${localStorage.getItem("token")}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request_body),
};
return fetch(backendURL, requestOptions);
function editDBObject() {
return postItemUpdate(props.id, props.type, items);
}

async function submitAddParent(event) {
Expand Down Expand Up @@ -174,15 +150,8 @@ function ItemEditor(props) {
if (loading) return <Heading level={3}> Loading... </Heading>;
return (
<Box>
<Heading level={3}>Edit {props.type}</Heading>
<Heading level={3}>Edit {items.name}</Heading>
<Form onSubmit={handleSubmit}>
<FormField>
<TextInput
value={items.name}
name="name"
onChange={(e) => setItem("name", e.target.value)}
/>
</FormField>
<FormField>
<TextInput
value={items.short_description}
Expand Down Expand Up @@ -263,5 +232,40 @@ function ItemEditor(props) {
</Box>
);
}

/**
* @param {string} id
* @param {string} type
* @param {*} item
* @returns Promise<any>
*/
export async function postItemUpdate(id, type, item) {
let backendURL = `${getBaseURL()}/${
configData.navigation[type]["api_name"]
}/${id}/`;

let request_body = {};
request_body["name"] = item.name;
request_body["short_description"] = item.short_description;
request_body["long_description"] = item.long_description;
request_body["keywords"] = item.keywords;
if (type === "PropertyClaim") {
request_body["claim_type"] = item.claim_type;
}
if (type === "Evidence") {
request_body["URL"] = item.URL;
}

const requestOptions = {
method: "PUT",
headers: {
Authorization: `Token ${localStorage.getItem("token")}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request_body),
};
return fetch(backendURL, requestOptions);
}

// eslint-disable-next-line import/no-anonymous-default-export
export default (props) => <ItemEditor {...props} params={useParams()} />;
5 changes: 0 additions & 5 deletions frontend/src/components/tests/ItemCreator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,25 @@ test("renders item creator layer", () => {

test("renders input fields correctly", () => {
render(<ItemCreator type="TopLevelNormativeGoal" />);
const nameInput = screen.getByPlaceholderText("Name");
const sdescInput = screen.getByPlaceholderText("Short description");
const ldescInput = screen.getByPlaceholderText("Long description");
const keywordsInput = screen.getByPlaceholderText(
"Keywords (comma-separated)",
);

expect(nameInput).toBeInTheDocument();
expect(sdescInput).toBeInTheDocument();
expect(ldescInput).toBeInTheDocument();
expect(keywordsInput).toBeInTheDocument();
});

test("updates input fields on change", () => {
render(<ItemCreator type="TopLevelNormativeGoal" />);
const nameInput = screen.getByPlaceholderText("Name");
const sdescInput = screen.getByPlaceholderText("Short description");

fireEvent.change(nameInput, { target: { value: "Updated name" } });
fireEvent.change(sdescInput, {
target: { value: "Updated short description" },
});

expect(nameInput.value).toBe("Updated name");
expect(sdescInput.value).toBe("Updated short description");
});

Expand Down

0 comments on commit b26aa1a

Please sign in to comment.