From 038a1a6df162e74969e41de4fd88f095f0ee01d9 Mon Sep 17 00:00:00 2001 From: Aadarsh A Date: Wed, 5 Oct 2022 19:09:04 +0530 Subject: [PATCH] feat: Add CRUD features for entries and root nodes (#41) * Initial commit: Front page + list of entries * Removed duplicate file * Change import * Changed buttons and image types * View parents and children in frontend * Added navbar + parents/children + Main language synonyms * Added all translations and properties * Fix stopwords/synonyms page + add preceding_lines * Change favicons and manifest * Add requested changes * Add requested changes (part 2) * Added submit button + update functionality * Add requested changes (part 3) * Change variable obj to value * Remove console.log * Changed div to Mui Box * Changes regarding variable nodeObject * Restructure AccumulateAllComponents.jsx and index.jsx * Change text css * Add/Delete Properties WIP * Properties CRUD WIP2 * Properties Table Edit WIP * Remove WIP Properties table implementation * Add comment * Add helper comments * Added CRUD for properties of an entry * Add useEffect * Implement CRUD for translations + Refactor previous code * Added CRUD for synonyms, stopwords * Previous PR changes WIP * Add a translation button * Added functionality to add translation language * Added deletion of a translation language * Add paths for updating children and add in frontend * Update children query changed * Added update in backend * Add node functionality completed * Completed delete root node functionality * Rename files and add previous PR changes * Change variable names + Remove backend changes * Add requested changes + fixes * Remove state from originalNodeObject * Change then/catch position * Change dialog to snackbar * Add requested changes * Remove whitespaces * Add newline * Add requested changes WIP * Change variable type * Add keyCode * Change to e.KeyCode * Change typo * Remove original nodeObject * Add early return and comments --- backend/editor/entries.py | 2 - taxonomy-editor-frontend/src/App.js | 6 +- taxonomy-editor-frontend/src/constants.js | 2 +- .../src/pages/allentries/index.jsx | 246 +++++++++--- .../editentry/AccumulateAllComponents.jsx | 76 +++- .../editentry/ListAllEntryProperties.jsx | 161 +++++--- .../pages/editentry/ListAllNonEntryInfo.jsx | 157 +++++--- .../src/pages/editentry/ListEntryChildren.jsx | 145 +++++++ ...Relationships.jsx => ListEntryParents.jsx} | 35 +- .../src/pages/editentry/ListTranslations.jsx | 364 +++++++++++++++--- .../src/pages/editentry/createURL.jsx | 70 ++-- .../src/pages/editentry/index.jsx | 102 ++++- 12 files changed, 1078 insertions(+), 288 deletions(-) create mode 100644 taxonomy-editor-frontend/src/pages/editentry/ListEntryChildren.jsx rename taxonomy-editor-frontend/src/pages/editentry/{ListAllEntryRelationships.jsx => ListEntryParents.jsx} (58%) diff --git a/backend/editor/entries.py b/backend/editor/entries.py index be5f2ac4..df85046c 100644 --- a/backend/editor/entries.py +++ b/backend/editor/entries.py @@ -93,10 +93,8 @@ def delete_node(label, entry): // Find node to be deleted using node ID MATCH (deleted_node:{label})-[:is_before]->(next_node) WHERE deleted_node.id = $id MATCH (previous_node)-[:is_before]->(deleted_node) - // Remove node DETACH DELETE (deleted_node) - // Rebuild relationships after deletion CREATE (previous_node)-[:is_before]->(next_node) """ diff --git a/taxonomy-editor-frontend/src/App.js b/taxonomy-editor-frontend/src/App.js index 019411fe..5fd570e2 100644 --- a/taxonomy-editor-frontend/src/App.js +++ b/taxonomy-editor-frontend/src/App.js @@ -8,6 +8,10 @@ import Home from './pages/home'; const theme = createTheme({ typography: { fontFamily : 'Plus Jakarta Sans', + button: { + fontFamily : 'Roboto, Helvetica, Arial, sans-serif', + color: '#808080' + }, }, }); @@ -29,4 +33,4 @@ function App() { ); } -export default App; +export default App; \ No newline at end of file diff --git a/taxonomy-editor-frontend/src/constants.js b/taxonomy-editor-frontend/src/constants.js index f263380d..470d2a87 100644 --- a/taxonomy-editor-frontend/src/constants.js +++ b/taxonomy-editor-frontend/src/constants.js @@ -1,2 +1,2 @@ // FIXME we should find a way that works for production build -export const API_URL = process.env.REACT_APP_API_URL || "http://localhost:80/" +export const API_URL = process.env.REACT_APP_API_URL || "http://localhost:80/" \ No newline at end of file diff --git a/taxonomy-editor-frontend/src/pages/allentries/index.jsx b/taxonomy-editor-frontend/src/pages/allentries/index.jsx index 3db9463a..b79dd103 100644 --- a/taxonomy-editor-frontend/src/pages/allentries/index.jsx +++ b/taxonomy-editor-frontend/src/pages/allentries/index.jsx @@ -1,83 +1,211 @@ -import { Typography, Button, Box } from "@mui/material"; +import useFetch from "../../components/useFetch"; +import { useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { API_URL } from "../../constants"; +import { Typography, Box, TextField, Stack, Button, IconButton, Paper, FormControl, InputLabel } from "@mui/material"; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; import EditIcon from '@mui/icons-material/Edit'; -import { Link } from "react-router-dom"; -import useFetch from "../../components/useFetch"; -import { API_URL } from "../../constants"; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import Select from '@mui/material/Select'; +import ISO6391 from 'iso-639-1'; const Entry = () => { - const { data: nodes, isPending, isError, isSuccess, errorMessage } = useFetch(API_URL+'nodes'); + const url = API_URL+'nodes'; const title = "Test"; - if (isError) { + const {data: nodes, isPending, errorMessage} = useFetch(url); + const [nodeType, setNodeType] = useState('entry'); // Used for storing node type + const [newLanguageCode, setNewLanguageCode] = useState(null); // Used for storing new Language Code + const [newNode, setnewNode] = useState(null); // Used for storing canonical tag + const [isValidLC, setisValidLC] = useState(false); // Used for validating a new LC + const [openAddDialog, setOpenAddDialog] = useState(false); + const [openSuccessDialog, setOpenSuccessDialog] = useState(false); + const [btnDisabled, setBtnDisabled] = useState(true); + const navigate = useNavigate(); + + // Handler function for button clicks + const handleClick = (event, id) => { + event.preventDefault(); + navigate('/entry/'+id); + } + // Helper functions for Dialog component + function handleCloseAddDialog() { setOpenAddDialog(false); } + function handleOpenAddDialog() { setOpenAddDialog(true); } + function handleCloseSuccessDialog() { setOpenSuccessDialog(false); } + function handleOpenSuccessDialog() { setOpenSuccessDialog(true); } + + function handleAddNode() { + const newNodeID = newLanguageCode + ':' + newNode // Reconstructing node ID + const data = {"id": newNodeID, "main_language": newLanguageCode}; + fetch(url, { + method : 'POST', + headers: {"Content-Type" : "application/json"}, + body: JSON.stringify(data) + }).then(() => { + handleCloseAddDialog(); + nodes.push([{"id" : newNodeID, ...data}]) // Not required after "all nodes" table removal + handleOpenSuccessDialog(); + }).catch((errorMessage) => { + console.log(errorMessage); + }) + } + + if (errorMessage) { return ( -
- {isError &&
{errorMessage}
} -
+ + {errorMessage} + ) } if (isPending) { return ( -
- {isPending &&
Loading...
} -
+ + Loading.. + ) } return ( -
- - - List of nodes in {title} Taxonomy: - - - Number of nodes in taxonomy: {nodes.length} - - {/* Table for listing all nodes in taxonomy */} - - - - - - - - Nodes - + + + List of nodes in {title} Taxonomy: + + + Number of nodes in taxonomy: {nodes.length} + + {/* Table for listing all nodes in taxonomy */} + +
+ + + + + + Nodes + + + + + + + + + Action + + + + + + {nodes.map((node) => ( + + + + {node[0].id} + - - - Action - + + handleClick(event, node[0].id) } aria-label="edit"> + + - - - {nodes.map((node) => ( - - - - {node[0].id} - - - - - - - ))} - -
-
-
-
-
+ ))} + + + + {/* Dialog box for adding nodes */} + + Add a node + + + Type of node + + Type + + + + + Main Language + { + setNewLanguageCode(e.target.value); + const validateBool = ISO6391.validate(e.target.value); + validateBool ? setisValidLC(true) : setisValidLC(false); + validateBool ? setBtnDisabled(false) : setBtnDisabled(true); + }} + label="Language Code" + error={!isValidLC} + sx={{width : 150, ml: 4.5}} + size="small" + variant="outlined" + /> + + { + nodeType === 'entry' && + + Node ID + { + setnewNode(e.target.value); + }} + label="Node ID" + sx={{width : 150, ml: 11}} + size="small" + variant="outlined" + /> + + } + + + + + + + {/* Dialog box for acknowledgement of addition of node */} + + + {"Your edits have been saved!"} + + + + The node {newNode} has been successfully added. + + + + + + + ); } diff --git a/taxonomy-editor-frontend/src/pages/editentry/AccumulateAllComponents.jsx b/taxonomy-editor-frontend/src/pages/editentry/AccumulateAllComponents.jsx index d57c9b38..a18a6bda 100644 --- a/taxonomy-editor-frontend/src/pages/editentry/AccumulateAllComponents.jsx +++ b/taxonomy-editor-frontend/src/pages/editentry/AccumulateAllComponents.jsx @@ -1,11 +1,12 @@ -import { Box, Typography } from "@mui/material"; +import { Alert, Box, Button, Snackbar, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import useFetch from "../../components/useFetch"; -import { createURL, getIdType } from "./createURL"; -import ListAllNonEntryInfo from "./ListAllNonEntryInfo"; -import ListAllEntryProperties from "./ListAllEntryProperties"; +import ListEntryParents from "./ListEntryParents"; +import ListEntryChildren from "./ListEntryChildren"; import ListTranslations from "./ListTranslations"; -import ListAllEntryRelationships from "./ListAllEntryRelationships"; +import ListAllEntryProperties from "./ListAllEntryProperties"; +import ListAllNonEntryInfo from "./ListAllNonEntryInfo"; +import { createURL, getIdType } from "./createURL"; /** * Component used for rendering node information @@ -14,46 +15,79 @@ import ListAllEntryRelationships from "./ListAllEntryRelationships"; * If node is "header/footer": Comments are rendered */ const AccumulateAllComponents = ({ id }) => { + + // Finding URL to send requests const url = createURL(id); const isEntry = getIdType(id) === 'entry'; - const [nodeObject, setNodeObject] = useState(null); // Storing node information + const { data: node, isPending, isError, isSuccess, errorMessage } = useFetch(url); + const [nodeObject, setNodeObject] = useState(null); // Storing updates to node + const [updateChildren, setUpdateChildren] = useState([]); // Storing updates of children in node + const [open, setOpen] = useState(false); // Used for Dialog component // Setting state of node after fetch useEffect(() => { setNodeObject(node?.[0]); }, [node]) - // Check error in fetch + // Displaying error messages if any if (isError) { - return ( - - {errorMessage} - - ) + return ({errorMessage}) } + + // Loading... if (isPending) { - return ( - - Loading.. - - ) + return (Loading..) + } + + // Helper functions for Dialog component + const handleClose = () => {setOpen(false)}; + + // Function handling updation of node + const handleSubmit = () => { + if (!nodeObject) return + const {id, ...data} = nodeObject // ID not allowed in POST + let allUrlsAndData = [[url, data]] + if (isEntry) { + allUrlsAndData.push([url+'children/', updateChildren]) + } + Promise.all(allUrlsAndData.map(([url, data]) => { + return fetch(url, { + method : 'POST', + headers: {"Content-Type" : "application/json"}, + body: JSON.stringify(data) + }) + })).then(() => { + setOpen(true); + }).catch(() => {}) } return ( - + {/* Based on isEntry, respective components are rendered */} { isEntry ? { !!nodeObject && - <> - + <> + } : <> } + {/* Button for submitting edits */} + + {/* Snackbar for acknowledgment of update */} + + + The node has been successfully updated! + + ); } - export default AccumulateAllComponents; \ No newline at end of file diff --git a/taxonomy-editor-frontend/src/pages/editentry/ListAllEntryProperties.jsx b/taxonomy-editor-frontend/src/pages/editentry/ListAllEntryProperties.jsx index 926ac4f3..59c16c42 100644 --- a/taxonomy-editor-frontend/src/pages/editentry/ListAllEntryProperties.jsx +++ b/taxonomy-editor-frontend/src/pages/editentry/ListAllEntryProperties.jsx @@ -1,26 +1,62 @@ -import { Typography, TextField, Box } from "@mui/material"; +import { Box, Grid, Paper, TextField, Typography } from "@mui/material"; +import MaterialTable, { MTableToolbar } from '@material-table/core'; +import { useEffect, useState } from "react"; +import * as uuid from "uuid"; -/** - * Sub-component used for rendering comments and properties of a node with ID = "entry" -*/ const ListAllEntryProperties = ({ nodeObject, setNodeObject }) => { - let renderedProperties = {} - Object.keys(nodeObject).forEach((key) => { + const [data, setData] = useState([]); - // Collecting keys of properties - // Properties have a prefix "prop_" followed by their name - // Ex: prop_vegan_en + // Changes the properties to be rendered + // Dependent on changes occuring in "nodeObject" + useEffect(() => { + let renderedProperties = [] + Object.keys(nodeObject).forEach((key) => { + // Collecting uuids of properties + // UUID of properties will have a "_uuid" suffix + // Ex: prop_vegan_en_uuid - if (key.startsWith('prop')) { - renderedProperties[key] = nodeObject[key] + if (key.startsWith('prop') && key.endsWith('uuid')) { + const uuid = nodeObject[key][0]; // UUID + // Removing "prop_" prefix from key to render only the name + const property_name = key.split('_').slice(1, -1).join('_'); + + // Properties have a prefix "prop_" followed by their name + // Getting key for accessing property value + const property_key = "prop_" + property_name + + renderedProperties.push({ + 'id': uuid, + 'propertyName': property_name, + 'propertyValue': nodeObject[property_key] + }) } - }); + }); + setData(renderedProperties); + }, [nodeObject]) + + // Helper function used for changing comments from node + function changeCommentData(value) { + const newNodeObject = {...nodeObject}; + newNodeObject['preceding_lines'] = value; + setNodeObject(newNodeObject); + } + + // Helper function used for changing properties of node + function changePropertyData(key, value) { + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + newNodeObject["prop_"+key] = value; + return newNodeObject + }) + } - // Helper function used for changing state of properties - function changeData(key, value) { - const duplicateData = {...nodeObject}; - duplicateData[key] = value; - setNodeObject(duplicateData); + // Helper function used for deleting properties of node + function deletePropertyData(key) { + setNodeObject(prevState => { + const toRemove = "prop_"+key; + const {[toRemove]: _, ...newNodeObject} = prevState; + return newNodeObject + }) } return ( @@ -32,37 +68,76 @@ const ListAllEntryProperties = ({ nodeObject, setNodeObject }) => { minRows={4} multiline onChange = {event => { - changeData('preceding_lines', event.target.value.split('\n')) + changeCommentData(event.target.value.split('\n')) }} - value={nodeObject.preceding_lines.join('\n')} + value={nodeObject?.preceding_lines.join('\n')} variant="outlined" /> {/* Properties */} - Properties - { Object.keys(renderedProperties).length === 0 ? - None : - Object.entries(renderedProperties).map(([property, value]) => { - // Removing "prop_" prefix from key to render only the name - const property_name = property.split('_').slice(1).join('_'); - return ( - - - {property_name}: - - { - changeData(property, event.target.value) - }} - value={value} - variant="outlined" /> - - ) - }) - } + + new Promise((resolve, reject) => { + // Add new property to rendered rows + const updatedRows = [...data, { id: uuid.v4(), ...newRow }] + setData(updatedRows); + + // Add new key-value pair of a property in nodeObject + changePropertyData(newRow.propertyName, newRow.propertyValue); + resolve() + }), + onRowDelete: selectedRow => new Promise((resolve, reject) => { + // Delete property from rendered rows + const index = selectedRow.tableData.id; + const updatedRows = data.filter(obj => !(index === obj.id)) + setData(updatedRows); + + // Delete key-value pair of a property from nodeObject + deletePropertyData(selectedRow.propertyName); + resolve(); + }), + onRowUpdate: (updatedRow, oldRow) => new Promise((resolve, reject) => { + // Update row in rendered rows + const updatedRows = data.map(el => (el.id === oldRow.id) ? updatedRow : el); + setData(updatedRows); + + // Updation takes place by deletion + addition + // If property name has been changed, previous key should be removed from nodeObject + (updatedRow.propertyName !== oldRow.propertyName) && deletePropertyData(oldRow.propertyName); + // Add new property to nodeObject + changePropertyData(updatedRow.propertyName, updatedRow.propertyValue) + resolve(); + }) + }} + options={{ + actionsColumnIndex: -1, addRowPosition: "last" + }} + components={{ + Toolbar: props => { + // Used for custom title and margins + const propsCopy = { ...props }; + propsCopy.showTitle = false; + return ( + + + Properties + + + + + + ); + }, + Container: props => + }} + /> + ); } - export default ListAllEntryProperties; \ No newline at end of file diff --git a/taxonomy-editor-frontend/src/pages/editentry/ListAllNonEntryInfo.jsx b/taxonomy-editor-frontend/src/pages/editentry/ListAllNonEntryInfo.jsx index 7932b06c..08772e25 100644 --- a/taxonomy-editor-frontend/src/pages/editentry/ListAllNonEntryInfo.jsx +++ b/taxonomy-editor-frontend/src/pages/editentry/ListAllNonEntryInfo.jsx @@ -1,5 +1,9 @@ -import { Box, Paper, Stack, TextField, Typography } from "@mui/material"; +import { Box, Paper, Stack, TextField, Typography, Button } from "@mui/material"; +import { useState, useEffect } from "react"; import { getIdType } from "./createURL"; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import * as uuid from "uuid"; import ISO6391 from 'iso-639-1'; /** @@ -10,30 +14,44 @@ import ISO6391 from 'iso-639-1'; const ListAllNonEntryInfo = ({ nodeObject, id, setNodeObject }) => { - // TODO: Change variables to state variables and wrap Object.keys() inside a useEffect() - // Stores ID type of node object - let IDType = getIdType(id); + const IDType = getIdType(id); // Stores 2 letter language code (LC) of the tags - let languageCode = ''; - // Storing keys and values that needs to be rendered for editing - let renderedNonEntryInfo = {} + const [languageCode, setLanguageCode] = useState(''); + // Storing tags that need to be rendered for editing + const [renderedNonEntryInfo, setRenderedNonEntryInfo] = useState([]) - if (nodeObject) { - Object.keys(nodeObject).forEach((key) => { - - // Get all tags and its corresponding language code - // Tagids need to be recomputed, so shouldn't be rendered - // Eg: tags_fr - - if (key.startsWith('tags') && - !key.includes('ids')) { - languageCode = key.slice(-2); - renderedNonEntryInfo[languageCode] = nodeObject[key] - } - }) - } + useEffect(() => { + const tagsExtracted = [] + let extractedLanguageCode = '' + if (nodeObject) { + Object.keys(nodeObject).forEach((key) => { + + // Get all tag UUIDs + // Ex: tags_en_uuid + if (key.startsWith('tags') && !key.includes('ids')) { + if (key.endsWith('uuid')) { + // Get all tags and its corresponding language code + // Tagids need to be recomputed, so it shouldn't be rendered + // Eg: tags_fr + extractedLanguageCode = key.split('_').slice(1,-1)[0] + const uuids = nodeObject[key] + const tagsKey = key.split('_').slice(0,-1).join('_') + nodeObject[tagsKey].map((tag, index) => ( + tagsExtracted.push({ + 'index' : uuids[index], + 'tag' : tag + }) + )) + } + } + }) + } + setLanguageCode(extractedLanguageCode) + setRenderedNonEntryInfo(tagsExtracted) + }, [nodeObject]) + // Helper function used for changing state of "preceding_lines" function changeDataComment(value) { const newNodeObject = {...nodeObject}; @@ -42,16 +60,51 @@ const ListAllNonEntryInfo = ({ nodeObject, id, setNodeObject }) => { } // Helper function used for changing state of properties - function changeData(key, index, value) { - const newNodeObject = {...nodeObject}; - newNodeObject[key][index] = value; - setNodeObject(newNodeObject); + function changeData(index, value) { + const updatedTagObject = {'index' : index, 'tag' : value} + const newRenderedNonEntryInfo = renderedNonEntryInfo.map(obj => (obj.index === index) ? updatedTagObject : obj) + setRenderedNonEntryInfo(newRenderedNonEntryInfo); // Set state + + // Updated tags assigned for later use + const tagsToBeInserted = newRenderedNonEntryInfo.map(el => (el.tag)) + + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + newNodeObject['tags_'+languageCode] = tagsToBeInserted; + return newNodeObject + }) + } + + function handleAdd() { + const newRenderedNonEntryInfo = [...renderedNonEntryInfo, {'index': uuid.v4(), 'tag' : ''}]; + setRenderedNonEntryInfo(newRenderedNonEntryInfo); // Set state + + // Updated tags assigned for later use + const tagsToBeInserted = newRenderedNonEntryInfo.map(el => (el.tag)) + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + newNodeObject['tags_'+languageCode] = tagsToBeInserted; + return newNodeObject + }) + } + + function handleDelete(index) { + const newRenderedNonEntryInfo = renderedNonEntryInfo.filter(obj => !(obj.index === index)) + setRenderedNonEntryInfo(newRenderedNonEntryInfo); // Set state + + // Updated tags assigned for later use + const tagsToBeInserted = newRenderedNonEntryInfo.map(el => (el.tag)) + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + newNodeObject['tags_'+languageCode] = tagsToBeInserted; + return newNodeObject + }) } return ( {/* Comments */} - Comments + Comments { nodeObject && { Language - {languageCode && ISO6391.getName(languageCode)} + {ISO6391.getName(languageCode)} - } + } {/* Stopwords or Synonyms */} - { (IDType ==='Synonyms' || IDType === 'Stopwords') && + { (IDType === 'Synonyms' || IDType === 'Stopwords') && - - { IDType } - + + + { IDType } + + + {/* Render all tags */} - - { nodeObject && renderedNonEntryInfo[languageCode].map((tag, index) => { - return ( - { - changeData('tags_'+languageCode, index, event.target.value) - }} - value={tag} - variant="outlined" /> - )}) - } - + { renderedNonEntryInfo.map((tagObj) => { + const index = tagObj.index; + const tag = tagObj.tag; + return ( + + + { + changeData(index, event.target.value) + }} + value={tag} + variant="outlined" /> + + + + )})} } diff --git a/taxonomy-editor-frontend/src/pages/editentry/ListEntryChildren.jsx b/taxonomy-editor-frontend/src/pages/editentry/ListEntryChildren.jsx new file mode 100644 index 00000000..366d589c --- /dev/null +++ b/taxonomy-editor-frontend/src/pages/editentry/ListEntryChildren.jsx @@ -0,0 +1,145 @@ +import useFetch from "../../components/useFetch"; +import { Typography, TextField, Stack, Button, IconButton, Box } from "@mui/material"; +import { Link } from "react-router-dom"; +import { useEffect, useState } from "react"; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import * as uuid from "uuid"; +import ISO6391 from 'iso-639-1'; + +const ListEntryChildren = ({url, setUpdateNodeChildren}) => { + const [relations, setRelations] = useState(null); + const [newChild, setNewChild] = useState(null); + const [newLanguageCode, setNewLanguageCode] = useState(null); + const [open, setOpen] = useState(false); // Used for Dialog component + const [btnDisabled, setBtnDisabled] = useState(true) // For enabling or disabling Dialog button + const [isValidLC, setisValidLC] = useState(false); // Used for validating a new LC + + const { data: incomingData, isPending, isError, isSuccess, errorMessage } = useFetch(url); + + useEffect(() => { + if (incomingData) { + setUpdateNodeChildren(incomingData.map(el => el?.[0])); + const arrayData = []; + incomingData.map((el) => + arrayData.push( + {"index" : uuid.v4(), "child" : el?.[0]}) + ); + setRelations(arrayData); + } + }, [incomingData, setUpdateNodeChildren]) + + // Helper functions for Dialog component + function handleClose() { setOpen(false); } + function handleOpen() { setOpen(true); } + + function handleAddChild() { + const newChildID = newLanguageCode + ':' + newChild; // Reconstructing node ID + setRelations([...relations, {"index" : uuid.v4(), "child": newChildID}]); + setUpdateNodeChildren(prevState => { + const duplicateData = [...prevState]; + duplicateData.push(newChildID); + return duplicateData + }); + setOpen(false); + } + + function handleDeleteChild(index) { + const newRelations = relations.filter(obj => !(index === obj.index)) + setRelations(newRelations); + // Updated tags assigned for later use + const tagsToBeInserted = newRelations.map(el => (el.child)) + setUpdateNodeChildren(tagsToBeInserted); + } + + // Check error in fetch + if (isError) { + return ({errorMessage}) + } + if (isPending) { + return (Loading..) + } + return ( + + + Children + + + + + {/* Renders parents or children of the node */} + {relations && relations.map(relationObject => ( + + + + {relationObject['child']} + + + handleDeleteChild(relationObject['index'], e)}> + + + + )) } + + {/* When no parents or children are present */} + {relations && relations.length === 0 && None } + + {/* Dialog box for adding translations */} + + Add a child + + + Enter the name of the child in the format "LC:child_tag_id" + + + Example - en:yogurts + + + { (e.keyCode === 13) && handleAddChild(e) }} + onChange={(e) => { + setNewLanguageCode(e.target.value); + const validateBool = ISO6391.validate(e.target.value); + validateBool ? setisValidLC(true) : setisValidLC(false); + validateBool ? setBtnDisabled(false) : setBtnDisabled(true); + }} + label="Language Code" + error={!isValidLC} + sx={{width : 250, marginRight: 1}} + size="small" + variant="outlined" + /> + : + { (e.keyCode === 13) && handleAddChild(e) }} + onChange={(e) => { + setNewChild(e.target.value); + }} + label="Child" + sx={{marginLeft: 1}} + size="small" + fullWidth + variant="outlined" + /> + + + + + + + + + ); +} + +export default ListEntryChildren; \ No newline at end of file diff --git a/taxonomy-editor-frontend/src/pages/editentry/ListAllEntryRelationships.jsx b/taxonomy-editor-frontend/src/pages/editentry/ListEntryParents.jsx similarity index 58% rename from taxonomy-editor-frontend/src/pages/editentry/ListAllEntryRelationships.jsx rename to taxonomy-editor-frontend/src/pages/editentry/ListEntryParents.jsx index 41a8130f..61e00bfd 100644 --- a/taxonomy-editor-frontend/src/pages/editentry/ListAllEntryRelationships.jsx +++ b/taxonomy-editor-frontend/src/pages/editentry/ListEntryParents.jsx @@ -1,43 +1,42 @@ import useFetch from "../../components/useFetch"; import { Box, Typography } from "@mui/material"; import { Link } from "react-router-dom"; +import { useEffect, useState } from "react"; + +const ListEntryParents = ({url}) => { + const [relations, setRelations] = useState(null); + const { data: incomingData, isPending, isError, isSuccess, errorMessage } = useFetch(url); + + useEffect(() => { + setRelations(incomingData) + }, [incomingData]) -const ListAllEntryRelationships = ({url, title}) => { - const { data: relations, isPending, isError, isSuccess, errorMessage } = useFetch(url); // Check error in fetch if (isError) { - return ( - - {errorMessage} - - ) + return ({errorMessage}) } if (isPending) { - return ( - - Loading.. - - ) + return (Loading..) } return ( - {title} - - {/* When no parents or children are present */} - {relations && relations.length === 0 && None } + {Parents} {/* Renders parents or children of the node */} {relations && relations.map(relation => ( - + {relation} )) } + + {/* When no parents or children are present */} + {relations && relations.length === 0 && None } ); } -export default ListAllEntryRelationships; \ No newline at end of file +export default ListEntryParents; \ No newline at end of file diff --git a/taxonomy-editor-frontend/src/pages/editentry/ListTranslations.jsx b/taxonomy-editor-frontend/src/pages/editentry/ListTranslations.jsx index f0c2b032..40817e82 100644 --- a/taxonomy-editor-frontend/src/pages/editentry/ListTranslations.jsx +++ b/taxonomy-editor-frontend/src/pages/editentry/ListTranslations.jsx @@ -1,78 +1,301 @@ -import { Typography, Paper, TextField, Stack, Box } from "@mui/material"; +import { Typography, TextField, Stack, Button, IconButton, Box } from "@mui/material"; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import { useEffect, useState } from "react"; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import * as uuid from "uuid"; import ISO6391 from 'iso-639-1'; /** * Sub-component for rendering translation of an "entry" */ const ListTranslations = ({ nodeObject, setNodeObject }) => { - let renderedTranslations = {} - Object.keys(nodeObject).forEach((key) => { + const [renderedTranslations, setRenderedTranslations] = useState([]) // Stores state of all tags + const [mainLangRenderedTranslations, setMainLangRenderedTranslations] = useState([]) // Stores state of main language's tags + const [openDialog, setOpen] = useState(false); // Used for Dialog component + const [newLanguageCode, setNewLanguageCode] = useState(''); // Used for storing new LC from Dialog + const [isValidLanguageCode, setisValidLanguageCode] = useState(false); // Used for validating a new LC + + // Helper functions for Dialog component + function handleClose() { setOpen(false); } + function handleOpen() { setOpen(true); } + + // Used for addition of a translation language + function handleAddTranslation(key) { + const newRenderedTranslations = [...renderedTranslations, {'languageCode' : key, 'tags' : []}] + setRenderedTranslations(newRenderedTranslations); + key = 'tags_' + key; // LC must have a prefix "tags_" + const uuidKey = key + '_uuid' // Format for the uuid - // Get all tags and its corresponding language code - // Tagids need to be recomputed, so shouldn't be rendered - // Main language isn't considered, since it's rendered separately - - if (key.startsWith('tags') && - !key.endsWith(nodeObject.main_language) && - !key.includes('ids')) { + // Make changes to the parent NodeObject + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + newNodeObject[key] = []; + newNodeObject[uuidKey] = [uuid.v4()]; + return newNodeObject + }) + setOpen(false); + } + + // Used for deleting a translation language + function handleDeleteTranslation(key) { + const newRenderedTranslations = renderedTranslations.filter(obj => !(key === obj.languageCode)) + setRenderedTranslations(newRenderedTranslations); + key = 'tags_' + key; // LC must have a prefix "tags_" + const uuidKey = key + '_uuid' // Format for the uuid + + // Make changes to the parent NodeObject + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + delete newNodeObject[key]; + delete newNodeObject[uuidKey]; + return newNodeObject + }) + setOpen(false); + } + + // Changes the translations to be rendered + // Dependent on changes occuring in "nodeObject" + useEffect(() => { - // Slice the language code - let languageCode = key.slice(-2); - renderedTranslations[languageCode] = nodeObject[key] + // Main langauge tags are considered separately, since they have to be rendered first + const mainLangTags = [] + const otherLangTags = [] + Object.keys(nodeObject).forEach((key) => { + + // Get all tags and its corresponding language code + // Tagids need to be recomputed, so shouldn't be rendered + // Eg: tags_fr + + if (key.startsWith('tags') && !key.includes('ids') && !key.includes('str')) { + if (key.endsWith('uuid')) { + const uuids = nodeObject[key] + // If tags are for main language, add them to mainLangTags + if (key.includes(nodeObject.main_language)) { + nodeObject["tags_"+nodeObject['main_language']].forEach((tag, index) => ( + mainLangTags.push({ + 'index' : uuids[index], + 'tag' : tag + }) + )) + } + + // If tags are not for the main language, add them to otherLangTags + else { + // Slice the language code + const languageCode = key.split('_').slice(1,-1)[0] + // General format for storing tags for different lc's + const tobeInsertedObj = {'languageCode' : languageCode, 'tags' : []} + const tagsKey = key.split('_').slice(0,-1).join('_') + nodeObject[tagsKey].forEach((tag, index) => ( + tobeInsertedObj['tags'].push({ + 'index' : uuids[index], // Give a unique identifier for each tag + 'tag' : tag + }) + )) + otherLangTags.push(tobeInsertedObj); + } + } } - }) - + }) + // Set states + setMainLangRenderedTranslations(mainLangTags); + setRenderedTranslations(otherLangTags); + }, [nodeObject]); + // Helper function used for changing state function changeData(key, index, value) { - key = 'tags_' + key; - const duplicateData = {...nodeObject}; - duplicateData[key][index] = value; - setNodeObject(duplicateData); + let updatedTags = [] // Stores all the tags of a language code + + if (key === nodeObject['main_language']) { + // Update state in correct format after duplication + const updatedObj = {'index' : index, 'tag' : value} + updatedTags = mainLangRenderedTranslations.map(el => (el.index === index) ? updatedObj : el) + setMainLangRenderedTranslations(updatedTags); + } + + else { + let duplicateOtherTags = []; // Stores the updated state for "renderedTranslations" + const updatedObj = {'index' : index, 'tag' : value} + renderedTranslations.forEach((allTagsObj) => { + + // Check if update LC and current element LC are same + // If LC is same, the element's tags needs to be updated + if (allTagsObj['languageCode'] === key) { + const newTags = allTagsObj['tags'].map(el => (el.index === index) ? updatedObj : el) + + // Append according to format used in "renderedTranslations" + duplicateOtherTags.push({ + 'languageCode' : key, + 'tags' : newTags + }) + updatedTags = [...newTags] // Assign to updatedTags for later use + } + // If LC is not the same, element doesn't require any changes + else { + duplicateOtherTags.push(allTagsObj) + } + }) + // Set state + setRenderedTranslations(duplicateOtherTags) + } + + let tagsToBeInserted = updatedTags.map(el => (el.tag)) // Removes unique idenitifer from each tag + + key = 'tags_' + key; // LC must have a prefix "tags_" + + // Make changes to the parent NodeObject + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + newNodeObject[key] = tagsToBeInserted; + return newNodeObject + }) + } + + // Helper function for adding a translation for a LC + function handleAdd(key) { + let tagsToBeInserted = []; + const newUUID = uuid.v4(); + // State of "MainLangRenderedTranslations" is updated according to format used + if (key === nodeObject.main_language) { + const duplicateMainLangRenderedTranslations = [...mainLangRenderedTranslations, {'index': newUUID, 'tag' : ''}]; + setMainLangRenderedTranslations(duplicateMainLangRenderedTranslations); // Set state + + // Updated tags assigned for later use + tagsToBeInserted = duplicateMainLangRenderedTranslations.map(el => (el.tag)) + } + // State of "renderedTranslations" is updated according to format used + else { + const newRenderedTranslations = [...renderedTranslations]; + newRenderedTranslations.map((allTagsObj) => (allTagsObj['languageCode'] === key) ? + ( + allTagsObj['tags'].push({'index': newUUID, 'tag' : ''}), + // Updated tags assigned for later use + tagsToBeInserted = allTagsObj['tags'].map(el => (el.tag)) + ) : allTagsObj + ) + setRenderedTranslations(newRenderedTranslations) // Set state + + } + // Set state of main NodeObject + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + newNodeObject['tags_'+key] = tagsToBeInserted; + newNodeObject['tags_'+key+'_uuid'].push(newUUID); + return newNodeObject + }) } - return ( - + function handleDelete(key, index) { + let tagsToBeInserted = [] + // State of "MainLangRenderedTranslations" is updated according to format used + if (key === nodeObject.main_language) { + const duplicateMainLangRenderedTranslations = mainLangRenderedTranslations.filter(obj => !(index === obj.index)) + setMainLangRenderedTranslations(duplicateMainLangRenderedTranslations); // Set state + + // Updated tags assigned for later use + tagsToBeInserted = duplicateMainLangRenderedTranslations.map(el => (el.tag)) + } + // State of "renderedTranslations" is updated according to format used + else { + const newRenderedTranslations = [] + renderedTranslations.forEach((allTagsObj) => { + if (allTagsObj['languageCode'] === key) { + const unDeletedTags = allTagsObj['tags'].filter((tagObj) => !(tagObj.index === index)); + newRenderedTranslations.push({ + 'languageCode' : key, + 'tags' : unDeletedTags + }) + // Updated tags assigned for later use + tagsToBeInserted = [...unDeletedTags].map(el => (el.tag)); + } + else { + newRenderedTranslations.push(allTagsObj) + } + }) + setRenderedTranslations(newRenderedTranslations) // Set state + } + // Set state of main NodeObject + setNodeObject(prevState => { + const newNodeObject = {...prevState}; + newNodeObject['tags_'+key] = tagsToBeInserted; + newNodeObject['tags_'+key+'_uuid'] = newNodeObject['tags_'+key+'_uuid'].filter(currIndex => !(currIndex === index)) + return newNodeObject + }) + } + + return ( + {/* Title */} - Translations + + Translations + + + + + {/* Main Language */} - - { ISO6391.getName(nodeObject.main_language) } - + + + { nodeObject && ISO6391.getName(nodeObject.main_language) } + + handleAdd(nodeObject.main_language)}> + + + + {/* Render main language tags */} - - { - nodeObject["tags_"+nodeObject['main_language']].map((tag, index) => { - return ( - // TODO: Key to be replaced by a UUID - - { - changeData(nodeObject['main_language'], index, event.target.value) - }} - value={tag} - variant="outlined" /> - - ) - }) - } - + { nodeObject && + mainLangRenderedTranslations.map(({index, tag}) => { + return ( + + { + changeData(nodeObject['main_language'], index, event.target.value) + }} + value={tag} + variant="outlined" /> + + handleDelete(nodeObject.main_language, index)}> + + + + ) + }) + } {/* All other languages */} { - Object.entries(renderedTranslations).map( ([lang, value]) => { + renderedTranslations.map( (allTagsObj) => { + const lang = allTagsObj['languageCode'] + const tagValue = allTagsObj['tags'] return ( - - - {ISO6391.getName(lang)} - + + + + {ISO6391.getName(lang)} + + handleAdd(lang)}> + + + handleDeleteTranslation(lang)}> + + + {/* Render all related tags */} { - value.map((tag, index) => { + tagValue.map((tagObj) => { + const index = tagObj['index'] + const tag = tagObj['tag'] return ( - + { }} value={tag} variant="outlined" /> - + handleDelete(lang, index)}> + + + ) }) } @@ -89,6 +315,40 @@ const ListTranslations = ({ nodeObject, setNodeObject }) => { ) } ) } + {/* Dialog box for adding translations */} + + Add a language + + + Enter the two letter language code for the language to be added. + + { (e.keyCode === 13) && isValidLanguageCode && handleAddTranslation(newLanguageCode, e) }} + onChange={(e) => { + setNewLanguageCode(e.target.value); + const validateBool = ISO6391.validate(e.target.value); + const ifDuplicateBool = renderedTranslations.some(el => (el.languageCode === e.target.value)) || + nodeObject.main_language === e.target.value + if (validateBool && !ifDuplicateBool) {setisValidLanguageCode(true)} + else {setisValidLanguageCode(false)} + }} + helperText={!isValidLanguageCode ? "Enter a correct language code!" : ""} + error={!isValidLanguageCode} + fullWidth + variant="standard" + /> + + + + + + ); } diff --git a/taxonomy-editor-frontend/src/pages/editentry/createURL.jsx b/taxonomy-editor-frontend/src/pages/editentry/createURL.jsx index 1db938a6..602358bc 100644 --- a/taxonomy-editor-frontend/src/pages/editentry/createURL.jsx +++ b/taxonomy-editor-frontend/src/pages/editentry/createURL.jsx @@ -1,35 +1,35 @@ -import { API_URL } from "../../constants.js" - -/** - * Finds type of ID - * Eg: Stopword, Synonym, Entry, Header or Footer -*/ -export function getIdType(id) { - let idType = ''; - - if (id.startsWith('__header__')) { idType = 'Header' } - else if (id.startsWith('__footer__')) { idType = 'Footer' } - else if (id.startsWith('synonym')) { idType = 'Synonyms' } - else if (id.startsWith('stopword')) { idType = 'Stopwords' } - else { idType = 'entry' } - - return idType -} - -/** - * Finding ID-specific URL for server requests -*/ -export function createURL(id) { - let url = API_URL; - - // ID's can look like: __header__, __footer__, synomym:0, stopword:0 - // For an entry, id looks like en:yogurts - - if (getIdType(id) === 'Header') { url += 'header/' } - else if (getIdType(id) === 'Footer') { url += 'footer/' } - else if (getIdType(id) === 'Synonyms') { url += `synonym/${id}/` } - else if (getIdType(id) === 'Stopwords') { url += `stopword/${id}/` } - else { url += `entry/${id}/` } - - return url -} +import { API_URL } from "../../constants.js" + +/** + * Finds type of ID + * Eg: Stopword, Synonym, Entry, Header or Footer +*/ +export function getIdType(id) { + let idType = ''; + + if (id.startsWith('__header__')) { idType = 'Header' } + else if (id.startsWith('__footer__')) { idType = 'Footer' } + else if (id.startsWith('synonym')) { idType = 'Synonyms' } + else if (id.startsWith('stopword')) { idType = 'Stopwords' } + else { idType = 'entry' } + + return idType +} + +/** + * Finding ID-specific URL for server requests +*/ +export function createURL(id) { + let url = API_URL; + + // ID's can look like: __header__, __footer__, synomym:0, stopword:0 + // For an entry, id looks like en:yogurts + + if (getIdType(id) === 'Header') { url += 'header/' } + else if (getIdType(id) === 'Footer') { url += 'footer/' } + else if (getIdType(id) === 'Synonyms') { url += `synonym/${id}/` } + else if (getIdType(id) === 'Stopwords') { url += `stopword/${id}/` } + else { url += `entry/${id}/` } + + return url +} \ No newline at end of file diff --git a/taxonomy-editor-frontend/src/pages/editentry/index.jsx b/taxonomy-editor-frontend/src/pages/editentry/index.jsx index 3500a058..e4175e3a 100644 --- a/taxonomy-editor-frontend/src/pages/editentry/index.jsx +++ b/taxonomy-editor-frontend/src/pages/editentry/index.jsx @@ -1,18 +1,102 @@ -import { Typography } from "@mui/material"; -import { useParams } from "react-router-dom"; +import { Typography, Stack, IconButton, Button, Box } from "@mui/material"; +import { useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; import AccumulateAllComponents from "./AccumulateAllComponents"; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import { API_URL } from "../../constants"; const EditEntry = () => { const { id } = useParams(); + const url = API_URL+'nodes'; + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [openSuccessDialog, setOpenSuccessDialog] = useState(false); + const navigate = useNavigate(); + + // Handler function for button clicks + const handleClick = (event) => { + event.preventDefault(); + navigate('/entry'); + } + + // Helper functions for Dialog component + function handleCloseDeleteDialog() { setOpenDeleteDialog(false); } + function handleOpenDeleteDialog() { setOpenDeleteDialog(true); } + function handleOpenSuccessDialog() { setOpenSuccessDialog(true); } + + function handleDeleteNode() { + const data = {"id" : id} + fetch(url, { + method : 'DELETE', + headers: {"Content-Type" : "application/json"}, + body: JSON.stringify(data) + }).then(() => { + handleCloseDeleteDialog(); + handleOpenSuccessDialog(); + }).catch((errorMessage) => { + console.log(errorMessage); + }) + } + return ( -
-
- - You are now editing "{id}" - -
+ + {/* Renders id of current node */} + + + + You are now editing "{id}" + + + + + + + {/* Renders node info based on id */} -
+ {/* Dialog box for confirmation of deletion of node */} + + Delete a node + + + Are you sure you want to delete this node? + + + + + + + + {/* Dialog box for acknowledgement of deletion of node */} + + + {"Your edits have been saved!"} + + + + The node {id} has been successfully deleted. + + + + + + +
); }