Skip to content

Commit

Permalink
fix(frontend): enhance property edition (#394)
Browse files Browse the repository at this point in the history
* fix(frontend): replace underscore by colons in property table

* refactor(frontend): convert jsx to tsx

* refactor(frontend): get rid of useless uuid

* feat(frontend): validate property name format

* feat(frontend): check if property name is unique when editing table

* fix(frontend): fix property name validation

---------

Co-authored-by: alice.juan <[email protected]>
  • Loading branch information
Piv94165 and alice.juan authored Feb 14, 2024
1 parent 5119baa commit 7bfcb71
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Alert, Box, Snackbar, Typography, Button } from "@mui/material";
import SaveIcon from "@mui/icons-material/Save";
import CircularProgress from "@mui/material/CircularProgress";
import { useMemo, useState } from "react";
import { useState } from "react";
import ListEntryParents from "./ListEntryParents";
import ListEntryChildren from "./ListEntryChildren";
import { ListTranslations } from "./ListTranslations";
Expand Down Expand Up @@ -59,28 +59,12 @@ const AccumulateAllComponents = ({ id, taxonomyName, branchName }) => {
: !equal(nodeObject, originalNodeObject) ||
!equal(updateChildren, previousUpdateChildren);

const createNodeObject = (node) => {
if (!node) return null;

const duplicateNode = { ...node };
// Adding UUIDs for properties
Object.keys(node).forEach((key) => {
if (key.startsWith("prop")) {
duplicateNode[key + "_uuid"] = [Math.random().toString()];
}
});

return duplicateNode;
};

const nodeObj = useMemo(() => createNodeObject(node), [node]);

if (
(!nodeObject && nodeObj) ||
(originalNodeObject && !equal(nodeObj, originalNodeObject))
(!nodeObject && node) ||
(originalNodeObject && !equal(node, originalNodeObject))
) {
setNodeObject(nodeObj);
setOriginalNodeObject(nodeObj);
setNodeObject(node);
setOriginalNodeObject(node);
}

// Displaying error messages if any
Expand Down
242 changes: 169 additions & 73 deletions taxonomy-editor-frontend/src/pages/editentry/ListAllEntryProperties.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,45 @@
import { Box, Grid, Paper, Typography } from "@mui/material";
import { Alert, Box, Grid, Paper, Snackbar, Typography } from "@mui/material";
import MaterialTable, { MTableToolbar } from "@material-table/core";
import { useState } from "react";
import ISO6391, { LanguageCode } from "iso-639-1";

interface RenderedProperties {
id: string;
type RowType = {
propertyName: string;
propertyValue: string;
}
};

type RenderedPropertyType = RowType & {
id: string;
};

const ListAllEntryProperties = ({ nodeObject, setNodeObject }) => {
const collectProperties = () => {
const renderedProperties: 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") &&
key.endsWith("uuid") &&
!key.endsWith("_comments_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("_");
function replaceAll(str: string, find: string, replace: string) {
return str.replace(new RegExp(find, "g"), replace);
}

const normalizeNameToFrontend = (name: string) => {
// Use the replace() method with a regular expression to replace underscores with colons
return replaceAll(name, "_", ":");
};

const normalizeNameToDb = (name: string) => {
// Use the replace() method with a regular expression to replace underscores with colons
return replaceAll(name, ":", "_");
};

// Properties have a prefix "prop_" followed by their name
// Getting key for accessing property value
const property_key = "prop_" + property_name;
const collectProperties = (): RenderedPropertyType[] => {
const renderedProperties: RenderedPropertyType[] = [];
Object.keys(nodeObject).forEach((key: string) => {
// Collecting properties (begin with prop_)
// Ex: prop_vegan_en
if (key.startsWith("prop") && !key.endsWith("_comments")) {
// Removing "prop_" prefix from key to render only the name
const property_name = key.replace(/^prop_/, "");

renderedProperties.push({
id: uuid,
propertyName: property_name,
propertyValue: nodeObject[property_key],
id: Math.random().toString(),
propertyName: normalizeNameToFrontend(property_name),
propertyValue: nodeObject[key],
});
}
});
Expand All @@ -42,7 +49,7 @@ const ListAllEntryProperties = ({ nodeObject, setNodeObject }) => {
const [data, setData] = useState(collectProperties());

// Helper function used for changing properties of node
const changePropertyData = (key, value) => {
const changePropertyData = (key: string, value: string) => {
setNodeObject((prevState) => {
const newNodeObject = { ...prevState };
newNodeObject["prop_" + key] = value;
Expand All @@ -51,74 +58,148 @@ const ListAllEntryProperties = ({ nodeObject, setNodeObject }) => {
};

// Helper function used for deleting properties of node
const deletePropertyData = (key) => {
const deletePropertyData = (key: string) => {
setNodeObject((prevState) => {
const keyToRemove = "prop_" + key;
const { [keyToRemove]: _propToRemove, ...newNodeObject } = prevState;
const toRemove = normalizeNameToDb("prop_" + key);
const { [toRemove]: _, ...newNodeObject } = prevState;
return newNodeObject;
});
};

const LanguageCodes = ISO6391.getAllCodes();

const validatePropertyName = (propertyName: string): boolean => {
// Every property name should be in the form property_name:lang_code
if (propertyName) {
const words = propertyName.split(":");
const langCode = words[words.length - 1];
if (!LanguageCodes.includes(langCode as LanguageCode)) {
return false;
}
// Property name should not include special caracters
for (const word of words) {
const pattern = /^[a-zA-Z0-9_]+$/;
if (!pattern.test(word)) {
return false;
}
}
return true;
}
return false;
};

const isPropertyNameUnique = (
propertyName: string,
otherProperties: RenderedPropertyType[]
): boolean => {
for (const prop of otherProperties) {
if (prop.propertyName === propertyName) return false;
}
return true;
};

const [errorMessage, setErrorMessage] = useState("");
const handleCloseErrorSnackbar = () => {
setErrorMessage("");
};

return (
<Box>
{/* Properties */}
<Box sx={{ width: "90%", ml: 4, maxWidth: "1000px", m: "auto", mb: 3 }}>
<MaterialTable
data={data}
columns={[
{ title: "Name", field: "propertyName" },
{
title: "Name",
field: "propertyName",
validate: (rowData) =>
validatePropertyName(rowData.propertyName)
? true
: {
isValid: false,
helperText:
"Property name should not contain special caracters and should follow the format : property_name:lang_code",
},
},
{ title: "Value", field: "propertyValue" },
]}
editable={{
onRowAdd: async (newRow) => {
// Add new property to rendered rows
const updatedRows = [
...data,
{ ...newRow, id: Math.random().toString() },
];
setData(updatedRows);

// Add new key-value pair of a property in nodeObject
changePropertyData(newRow.propertyName, newRow.propertyValue);
},
onRowDelete: async (selectedRow) => {
// Delete property from rendered rows
const updatedRows = [...data];
const index = parseInt(selectedRow.id);
updatedRows.splice(index, 1);
setData(updatedRows);

// Delete key-value pair of a property from nodeObject
deletePropertyData(selectedRow.propertyName);
},
onRowUpdate: async (updatedRow, oldRow) => {
// Update row in rendered rows
const updatedRows = data.map((el) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
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
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
updatedRow.propertyName !== oldRow.propertyName &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
deletePropertyData(oldRow.propertyName);
// Add new property to nodeObject
changePropertyData(
updatedRow.propertyName,
updatedRow.propertyValue
);
},
onRowAdd: (newRow: RowType) =>
new Promise<void>((resolve, reject) => {
if (!isPropertyNameUnique(newRow.propertyName, data)) {
setErrorMessage(`${newRow.propertyName} already exists`);
reject();
} else {
// Add new property to rendered rows
const updatedRows = [
...data,
{ id: Math.random().toString(), ...newRow },
];
setData(updatedRows);

// Add new key-value pair of a property in nodeObject
changePropertyData(
normalizeNameToDb(newRow.propertyName),
newRow.propertyValue
);
resolve();
}
}),
onRowDelete: (selectedRow: RenderedPropertyType) =>
new Promise<void>((resolve) => {
// Delete property from rendered rows
const updatedRows = [...data];
const index = updatedRows.findIndex(
(row) => row.id === selectedRow.id
);
updatedRows.splice(index, 1);
setData(updatedRows);
// Delete key-value pair of a property from nodeObject
deletePropertyData(selectedRow.propertyName);
resolve();
}),
onRowUpdate: (
updatedRow: RenderedPropertyType,
oldRow: RenderedPropertyType
) =>
new Promise<void>((resolve, reject) => {
const index = data.findIndex((row) => row.id === updatedRow.id);
const otherProperties = [...data];
otherProperties.splice(index, 1);
if (
!isPropertyNameUnique(
updatedRow.propertyName,
otherProperties
)
) {
setErrorMessage(`${updatedRow.propertyName} already exists`);
reject();
} else {
// 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(
normalizeNameToDb(updatedRow.propertyName),
updatedRow.propertyValue
);
resolve();
}
}),
}}
options={{
actionsColumnIndex: -1,
addRowPosition: "last",
tableLayout: "fixed",
paging: false,
showTitle: false,
}}
components={{
Toolbar: (props) => {
Expand All @@ -142,6 +223,21 @@ const ListAllEntryProperties = ({ nodeObject, setNodeObject }) => {
}}
/>
</Box>
<Snackbar
anchorOrigin={{ vertical: "top", horizontal: "right" }}
open={!!errorMessage}
autoHideDuration={3000}
onClose={handleCloseErrorSnackbar}
>
<Alert
elevation={6}
variant="filled"
onClose={handleCloseErrorSnackbar}
severity="error"
>
{errorMessage}
</Alert>
</Snackbar>
</Box>
);
};
Expand Down

0 comments on commit 7bfcb71

Please sign in to comment.