From 10b13ca8ce2023d584bb117bb810c28cc14f7bbc Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 1 Nov 2023 11:43:58 +0100 Subject: [PATCH 1/4] #273_c edit functionality added --- .../ExperimentDialogs/TagsAutocomplete.jsx | 6 +- .../common/ExperimentDialogs/TextEditor.jsx | 10 +- .../CreateUpdateExperimentDialog.tsx | 2 +- .../src/components/home/ExperimentCard.jsx | 28 +++- .../src/components/home/ExperimentList.jsx | 4 +- .../home/ExplorationSpinalCordDialog.tsx | 150 +++++++++++++++--- .../portal/frontend/src/pages/HomePage.tsx | 9 +- 7 files changed, 174 insertions(+), 35 deletions(-) diff --git a/applications/portal/frontend/src/components/common/ExperimentDialogs/TagsAutocomplete.jsx b/applications/portal/frontend/src/components/common/ExperimentDialogs/TagsAutocomplete.jsx index ce71d0e0..fae78eaa 100644 --- a/applications/portal/frontend/src/components/common/ExperimentDialogs/TagsAutocomplete.jsx +++ b/applications/portal/frontend/src/components/common/ExperimentDialogs/TagsAutocomplete.jsx @@ -25,7 +25,7 @@ const useStyles = makeStyles(() => ({ export const TagsAutocomplete = (props) => { const classes = useStyles(); - const { tags, onChange } = props; + const { tagsOptions, tags, onChange } = props; return ( { popupIcon={false} fullWidth id="tags-filled" - options={tags} - defaultValue={[]} + value={tags} + options={tagsOptions} freeSolo limitTags={3} renderTags={(value, getTagProps) => diff --git a/applications/portal/frontend/src/components/common/ExperimentDialogs/TextEditor.jsx b/applications/portal/frontend/src/components/common/ExperimentDialogs/TextEditor.jsx index 8cad2557..af2b2ad4 100644 --- a/applications/portal/frontend/src/components/common/ExperimentDialogs/TextEditor.jsx +++ b/applications/portal/frontend/src/components/common/ExperimentDialogs/TextEditor.jsx @@ -7,11 +7,11 @@ import UNORDERED from "../../../assets/images/icons/unordered_list.svg"; import ORDERED from "../../../assets/images/icons/ordered_list.svg"; import {Editor} from "react-draft-wysiwyg"; import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css"; -import {EditorState} from 'draft-js'; +import {EditorState, ContentState, convertFromHTML} from 'draft-js'; export const TextEditor = (props) => { const [editorState, setEditorState] = React.useState(EditorState.createEmpty()) - const {onChange, wrapperClassName} = props; + const {value, onChange, wrapperClassName} = props; const onEditorStateChange = (updatedEditorState) => { @@ -19,6 +19,12 @@ export const TextEditor = (props) => { onChange(updatedEditorState) } + React.useEffect(() => { + if (value) { + setEditorState(EditorState.createWithContent(ContentState.createFromBlockArray(convertFromHTML(value)))); + }; + }, []); + return ( Tags - t.name)} onChange={handleTagsChange}/> + t.name)} onChange={handleTagsChange}/> diff --git a/applications/portal/frontend/src/components/home/ExperimentCard.jsx b/applications/portal/frontend/src/components/home/ExperimentCard.jsx index 3c3767d4..7a31d43b 100644 --- a/applications/portal/frontend/src/components/home/ExperimentCard.jsx +++ b/applications/portal/frontend/src/components/home/ExperimentCard.jsx @@ -33,9 +33,11 @@ import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; import ArrowRightIcon from '@material-ui/icons/ArrowRight'; +import WorkspaceService from "../../service/WorkspaceService"; import { CircularProgress } from "@material-ui/core"; import { getDateFromDateTime } from "../../utils"; import { DeleteExperimentDialog } from "../DeleteExperimentDialog"; +import { ExplorationSpinalCordDialog } from "./ExplorationSpinalCordDialog"; const commonStyle = { @@ -275,7 +277,6 @@ const ExperimentCard = ({ experiment, type, handleDialogToggle, - handleExplorationDialogToggle, handleShareDialogToggle, handleShareMultipleDialogToggle, refreshExperimentList @@ -286,8 +287,15 @@ const ExperimentCard = ({ history.push(`/experiments/${experiment.id}`) } + const api = WorkspaceService.getApi(); const [experimentMenuEl, setExperimentMenuEl] = React.useState(null); const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false); + const [explorationDialogOpen, setExplorationDialogOpen] = React.useState(false); + const [tagsOptions, setTagsOptions] = React.useState([]); + + const handleExplorationDialogToggle = () => { + setExplorationDialogOpen((prevOpen) => !prevOpen); + }; const handleCardActions = (event) => { setExperimentMenuEl(event.currentTarget); @@ -301,6 +309,15 @@ const ExperimentCard = ({ closeFilter(); setOpenDeleteDialog(!openDeleteDialog); }; + + React.useEffect(() => { + const fetchTagOptions = async () => { + const res = await api.listTags() + setTagsOptions(res.data) + }; + fetchTagOptions().catch(console.error); + }, []); + return ( @@ -399,6 +416,15 @@ const ExperimentCard = ({ + setOpenDeleteDialog(false)} diff --git a/applications/portal/frontend/src/components/home/ExperimentList.jsx b/applications/portal/frontend/src/components/home/ExperimentList.jsx index cb4568f8..11d8edaa 100644 --- a/applications/portal/frontend/src/components/home/ExperimentList.jsx +++ b/applications/portal/frontend/src/components/home/ExperimentList.jsx @@ -198,7 +198,7 @@ const ExperimentList = (props) => { } const tags = ["Project A", "Tag X", "Label 1"]; - const { heading, description, type, infoIcon, handleDialogToggle, handleExplorationDialogToggle, handleShareMultipleDialogToggle, handleShareDialogToggle } = props; + const { heading, description, type, infoIcon, handleDialogToggle, handleShareMultipleDialogToggle, handleShareDialogToggle } = props; const sortOptions = ["Alphabetical", "Date created", "Last viewed"]; const orderOptions = ["Oldest first", "Newest first"]; @@ -295,7 +295,7 @@ const ExperimentList = (props) => { {experiments.map( exp => ( diff --git a/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx b/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx index 95935503..1d640515 100644 --- a/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx +++ b/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx @@ -5,22 +5,14 @@ import { Typography, TextField, } from "@material-ui/core"; +import * as Yup from 'yup'; import { headerButtonBorderColor } from "../../theme"; +// @ts-ignore +import WorkspaceService from "../../service/WorkspaceService"; +import { common } from "../header/ExperimentDialog/Common"; import Modal from "../common/BaseDialog"; import { TagsAutocomplete } from "../common/ExperimentDialogs/TagsAutocomplete"; import { TextEditor } from "../common/ExperimentDialogs/TextEditor"; -import { ExperimentTags } from "../../apiclient/workspaces"; - -const tags: ExperimentTags[] = [ - { name: 'Project A', id: 1 }, - { name: 'Label B', id: 2}, - { name: 'Label XYZ', id: 3}, - { name: 'Project C', id: 4 }, - { name: 'Project d', id: 5 }, - { name: 'Label e', id: 6}, - { name: 'Label XeYZ', id: 7}, - { name: 'Project Ce', id: 8}, -]; const useStyles = makeStyles(() => ({ formGroup: { @@ -36,35 +28,157 @@ const useStyles = makeStyles(() => ({ marginBottom: '0.75rem' }, }, + editorWrapper: { + '&:hover': { + borderColor: '#fff' + }, + '&:focus-within': { + borderColor: '#7b61ff' + }, + }, })); +const NAME_KEY = "name"; +const DESCRIPTION_KEY = "description"; + export const ExplorationSpinalCordDialog = (props: any) => { const classes = useStyles(); - const { open, handleClose } = props; + const commonClasses = common(); + const api = WorkspaceService.getApi(); + const { experimentId, experiment, tagsOptions, open, handleClose, refreshExperimentList, setExperimentMenuEl } = props; + + const [name, setName] = React.useState(experiment.name); + const [description, setDescription] = React.useState(experiment.description); + const [validationErrors, setValidationErrors] = React.useState(new Set([])); + const [initialTags, setInitialTags] = React.useState(experiment.tags.map((t: any) => t.name)); + const [tags, setTags] = React.useState(experiment.tags.map((t: any) => t.name)); + + const onTagsChange = (e: any, value: string[]) => { + setTags(value); + }; + + const validationSchema = experimentId + ? Yup.object().shape({ + [NAME_KEY]: Yup.string().required(), + [DESCRIPTION_KEY]: Yup.string().required(), + }) + : Yup.object().shape({}); + + const deleteTag = async (tag: string) => { + try { + await api.deleteTagExperiment(experiment.id.toString(), tag); + } catch (error) { + console.error(error); + } + }; + + const addTags = async (tags: string[]) => { + try { + await api.addTagsExperiment(experiment.id.toString(), tags); + } catch (error) { + console.error(error); + } + } + + const handleFormChange = (newValue: any, setState: any, errorKey: string) => { + validationErrors.delete(errorKey); + setValidationErrors(validationErrors); + setState(newValue); + }; + + const getCurrentFormDataObject = () => { + return { + name, + description, + tags, + } + } + + const getValidationErrors = async () => { + const errorsSet = new Set() + try { + await validationSchema.validate(getCurrentFormDataObject(), { strict: true, abortEarly: false }) + } catch (exception) { + for (const e of exception.inner) { + errorsSet.add(e.path) + } + } + + return errorsSet + } + + const handleAction = async () => { + + const deletedTags = initialTags.filter((tag: any) => !tags.includes(tag)); + const addedTags = tags.filter((tag: any) => !initialTags.includes(tag)); + + //API calls tags + if (deletedTags.length > 0) { + deletedTags.forEach((tag: any) => deleteTag(tag)); + } + if (addedTags.length > 0) { + addTags(addedTags); + } + + const errorsSet = await getValidationErrors() + if (errorsSet.size > 0) { + setValidationErrors(errorsSet) + return + } + + if(experimentId) { + try { + const res = await api.updateExperiment(experimentId.toString(), name, description); + if (res.status === 200) { + refreshExperimentList() + } + } catch (err) { + console.error(err) + } + } + setInitialTags(tags); + handleClose(); + setExperimentMenuEl(null) + }; return ( Name - + handleFormChange(e.target.value, setName, NAME_KEY)} + error={validationErrors.has(NAME_KEY)} + required={true} + /> Description - + + handleFormChange(editorState.getCurrentContent().getPlainText(), setDescription, DESCRIPTION_KEY)} + required={true} + /> Tags - + t.name)} onChange={onTagsChange} /> diff --git a/applications/portal/frontend/src/pages/HomePage.tsx b/applications/portal/frontend/src/pages/HomePage.tsx index 6fa0d182..8b0e9ecf 100644 --- a/applications/portal/frontend/src/pages/HomePage.tsx +++ b/applications/portal/frontend/src/pages/HomePage.tsx @@ -67,12 +67,6 @@ export default (props: any) => { setDialogOpen((prevOpen) => !prevOpen); }; - const [explorationDialogOpen, setExplorationDialogOpen] = React.useState(false); - - const handleExplorationDialogToggle = () => { - setExplorationDialogOpen((prevOpen) => !prevOpen); - }; - const [shareDialogOpen, setShareDialogOpen] = React.useState(false); const handleShareDialogToggle = () => { @@ -97,7 +91,7 @@ export default (props: any) => { @@ -118,7 +112,6 @@ export default (props: any) => { {/**/} - From 27471a26421ae92873f0633373dabf1f8d0596d5 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Wed, 1 Nov 2023 14:05:03 +0100 Subject: [PATCH 2/4] #273_c changes made --- .../home/ExplorationSpinalCordDialog.tsx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx b/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx index 1d640515..1b257f00 100644 --- a/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx +++ b/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx @@ -50,7 +50,6 @@ export const ExplorationSpinalCordDialog = (props: any) => { const [name, setName] = React.useState(experiment.name); const [description, setDescription] = React.useState(experiment.description); const [validationErrors, setValidationErrors] = React.useState(new Set([])); - const [initialTags, setInitialTags] = React.useState(experiment.tags.map((t: any) => t.name)); const [tags, setTags] = React.useState(experiment.tags.map((t: any) => t.name)); const onTagsChange = (e: any, value: string[]) => { @@ -64,21 +63,21 @@ export const ExplorationSpinalCordDialog = (props: any) => { }) : Yup.object().shape({}); - const deleteTag = async (tag: string) => { - try { - await api.deleteTagExperiment(experiment.id.toString(), tag); - } catch (error) { - console.error(error); - } - }; + const deleteTag = async (tag: any) => { + try { + await api.deleteTagExperiment(experiment.id.toString(), tag.name); + } catch (error) { + console.error(error); + } + }; - const addTags = async (tags: string[]) => { - try { - await api.addTagsExperiment(experiment.id.toString(), tags); - } catch (error) { - console.error(error); - } + const addTags = async (tags: string[]) => { + try { + await api.addTagsExperiment(experiment.id.toString(), tags); + } catch (error) { + console.error(error); } + } const handleFormChange = (newValue: any, setState: any, errorKey: string) => { validationErrors.delete(errorKey); @@ -109,9 +108,9 @@ export const ExplorationSpinalCordDialog = (props: any) => { const handleAction = async () => { - const deletedTags = initialTags.filter((tag: any) => !tags.includes(tag)); - const addedTags = tags.filter((tag: any) => !initialTags.includes(tag)); - + const deletedTags = experiment.tags.filter((tag: any) => !tags.includes(tag.name)); + const addedTags = tags.filter((tag: any) => !experiment.tags.some((expTag: any) => expTag.name === tag)); + //API calls tags if (deletedTags.length > 0) { deletedTags.forEach((tag: any) => deleteTag(tag)); @@ -136,7 +135,6 @@ export const ExplorationSpinalCordDialog = (props: any) => { console.error(err) } } - setInitialTags(tags); handleClose(); setExperimentMenuEl(null) }; From 36106dab9d52506ada6dd9ea4451c1617a8f4b0e Mon Sep 17 00:00:00 2001 From: afonso pinto Date: Wed, 1 Nov 2023 16:38:12 +0000 Subject: [PATCH 3/4] fix: Fix delete tag --- .../portal/backend/api/views/rest/experiment.py | 1 - .../home/ExplorationSpinalCordDialog.tsx | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/applications/portal/backend/api/views/rest/experiment.py b/applications/portal/backend/api/views/rest/experiment.py index 4c5da33e..e097eda7 100644 --- a/applications/portal/backend/api/views/rest/experiment.py +++ b/applications/portal/backend/api/views/rest/experiment.py @@ -122,7 +122,6 @@ def add_tags(self, request, pk): ) def delete_tag(self, request, tag_name, **kwargs): instance = self.get_object() - tag_name = request.data.get("name") delete_tag(instance, tag_name) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx b/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx index 1d640515..5ec903e0 100644 --- a/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx +++ b/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx @@ -64,17 +64,17 @@ export const ExplorationSpinalCordDialog = (props: any) => { }) : Yup.object().shape({}); - const deleteTag = async (tag: string) => { + const deleteTag = async (tag: string) => { try { await api.deleteTagExperiment(experiment.id.toString(), tag); } catch (error) { console.error(error); } }; - - const addTags = async (tags: string[]) => { + + const addTags = async (newTags: string[]) => { try { - await api.addTagsExperiment(experiment.id.toString(), tags); + await api.addTagsExperiment(experiment.id.toString(), newTags); } catch (error) { console.error(error); } @@ -112,7 +112,7 @@ export const ExplorationSpinalCordDialog = (props: any) => { const deletedTags = initialTags.filter((tag: any) => !tags.includes(tag)); const addedTags = tags.filter((tag: any) => !initialTags.includes(tag)); - //API calls tags + // API calls tags if (deletedTags.length > 0) { deletedTags.forEach((tag: any) => deleteTag(tag)); } @@ -126,7 +126,7 @@ export const ExplorationSpinalCordDialog = (props: any) => { return } - if(experimentId) { + if (experimentId) { try { const res = await api.updateExperiment(experimentId.toString(), name, description); if (res.status === 200) { @@ -154,7 +154,7 @@ export const ExplorationSpinalCordDialog = (props: any) => { Name - Date: Wed, 1 Nov 2023 16:39:39 +0000 Subject: [PATCH 4/4] chore: Fix linting errors --- .../home/ExplorationSpinalCordDialog.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx b/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx index 1b257f00..8559d526 100644 --- a/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx +++ b/applications/portal/frontend/src/components/home/ExplorationSpinalCordDialog.tsx @@ -70,10 +70,10 @@ export const ExplorationSpinalCordDialog = (props: any) => { console.error(error); } }; - - const addTags = async (tags: string[]) => { + + const addTags = async (newTags: string[]) => { try { - await api.addTagsExperiment(experiment.id.toString(), tags); + await api.addTagsExperiment(experiment.id.toString(), newTags); } catch (error) { console.error(error); } @@ -110,8 +110,8 @@ export const ExplorationSpinalCordDialog = (props: any) => { const deletedTags = experiment.tags.filter((tag: any) => !tags.includes(tag.name)); const addedTags = tags.filter((tag: any) => !experiment.tags.some((expTag: any) => expTag.name === tag)); - - //API calls tags + + // API calls tags if (deletedTags.length > 0) { deletedTags.forEach((tag: any) => deleteTag(tag)); } @@ -125,7 +125,7 @@ export const ExplorationSpinalCordDialog = (props: any) => { return } - if(experimentId) { + if (experimentId) { try { const res = await api.updateExperiment(experimentId.toString(), name, description); if (res.status === 200) { @@ -152,7 +152,7 @@ export const ExplorationSpinalCordDialog = (props: any) => { Name -