diff --git a/api-editor/gui/src/app/App.tsx b/api-editor/gui/src/app/App.tsx index 0ef7f8d4b..cb89ebc14 100644 --- a/api-editor/gui/src/app/App.tsx +++ b/api-editor/gui/src/app/App.tsx @@ -67,7 +67,9 @@ import { SaveFilterDialog } from '../features/filter/SaveFilterDialog'; import { StatisticsView } from '../features/statistics/StatisticsView'; import { useAnnotationToasts } from '../features/achievements/AnnotationToast'; import { ValueForm } from '../features/annotations/forms/ValueForm'; -import { AnnotationStore } from '../features/annotations/versioning/AnnotationStoreV2'; +import { AnnotationStore, CalledAfterTarget } from '../features/annotations/versioning/AnnotationStoreV2'; +import { RemoveForm } from '../features/annotations/forms/RemoveForm'; +import { PureForm } from '../features/annotations/forms/PureForm'; export const App: React.FC = function () { useIndexedDB(); @@ -122,7 +124,14 @@ export const App: React.FC = function () { )} {currentUserAction.type === 'calledAfter' && userActionTarget instanceof PythonFunction && ( - + )} {currentUserAction.type === 'description' && (userActionTarget instanceof PythonClass || @@ -143,6 +152,10 @@ export const App: React.FC = function () { )} {currentUserAction.type === 'move' && } {currentUserAction.type === 'none' && } + {currentUserAction.type === 'pure' && } + {currentUserAction.type === 'remove' && ( + + )} {currentUserAction.type === 'rename' && ( )} diff --git a/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx b/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx index 5e7980a60..90d30e207 100644 --- a/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx @@ -2,7 +2,7 @@ import { Box, Button, Icon, Menu, MenuButton, MenuGroup, MenuItem, MenuList } fr import React from 'react'; import { FaChevronDown } from 'react-icons/fa'; import { useAppDispatch, useAppSelector } from '../../app/hooks'; -import { addPureAnnotation, addRemoveAnnotation, selectComplete, selectUsernameIsValid } from './annotationSlice'; +import { selectComplete, selectUsernameIsValid } from './annotationSlice'; import { showBoundaryAnnotationForm, showCalledAfterAnnotationForm, @@ -10,6 +10,8 @@ import { showEnumAnnotationForm, showGroupAnnotationForm, showMoveAnnotationForm, + showPureAnnotationForm, + showRemoveAnnotationForm, showRenameAnnotationForm, showTodoAnnotationForm, showValueAnnotationForm, @@ -133,12 +135,12 @@ export const AnnotationDropdown: React.FC = function ({ )} {showPure && ( - dispatch(addPureAnnotation({ target }))} paddingLeft={8}> + dispatch(showPureAnnotationForm(target))} paddingLeft={8}> @pure )} {showRemove && ( - dispatch(addRemoveAnnotation({ target }))} paddingLeft={8}> + dispatch(showRemoveAnnotationForm(target))} paddingLeft={8}> @remove )} diff --git a/api-editor/gui/src/features/annotations/AnnotationView.tsx b/api-editor/gui/src/features/annotations/AnnotationView.tsx index 9e9d709ee..5232e9683 100644 --- a/api-editor/gui/src/features/annotations/AnnotationView.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationView.tsx @@ -42,10 +42,13 @@ import { hideAnnotationForm, selectCurrentUserAction, showBoundaryAnnotationForm, + showCalledAfterAnnotationForm, showDescriptionAnnotationForm, showEnumAnnotationForm, showGroupAnnotationForm, showMoveAnnotationForm, + showPureAnnotationForm, + showRemoveAnnotationForm, showRenameAnnotationForm, showTodoAnnotationForm, showValueAnnotationForm, @@ -111,6 +114,7 @@ export const AnnotationView: React.FC = function ({ target name={calledAfterName} key={calledAfterName} annotation={calledAfterAnnotation[calledAfterName]} + onEdit={() => dispatch(showCalledAfterAnnotationForm({ target, calledAfterName }))} onDelete={() => dispatch(removeCalledAfterAnnotation({ target, calledAfterName }))} onReview={() => dispatch(reviewCalledAfterAnnotation({ target, calledAfterName }))} /> @@ -166,6 +170,7 @@ export const AnnotationView: React.FC = function ({ target dispatch(showPureAnnotationForm(target))} onDelete={() => dispatch(removePureAnnotation(target))} onReview={() => dispatch(reviewPureAnnotation(target))} /> @@ -174,6 +179,7 @@ export const AnnotationView: React.FC = function ({ target dispatch(showRemoveAnnotationForm(target))} onDelete={() => dispatch(removeRemoveAnnotation(target))} onReview={() => dispatch(reviewRemoveAnnotation(target))} reportable @@ -271,7 +277,7 @@ interface AnnotationTagProps { type: string; name?: string; annotation: Annotation; - onEdit?: () => void; + onEdit: () => void; onDelete: () => void; onReview: () => void; reportable?: boolean; diff --git a/api-editor/gui/src/features/annotations/annotationSlice.ts b/api-editor/gui/src/features/annotations/annotationSlice.ts index 0ba981767..628204825 100644 --- a/api-editor/gui/src/features/annotations/annotationSlice.ts +++ b/api-editor/gui/src/features/annotations/annotationSlice.ts @@ -189,22 +189,32 @@ const annotationsSlice = createSlice({ updateQueue(state); }, - upsertCalledAfterAnnotation(state, action: PayloadAction) { - if (!state.annotations.calledAfterAnnotations[action.payload.target]) { - state.annotations.calledAfterAnnotations[action.payload.target] = {}; + upsertCalledAfterAnnotation( + state, + action: PayloadAction<{ annotation: CalledAfterAnnotation; previousCalledAfterName?: string }>, + ) { + const oldAnnotation = + state.annotations.calledAfterAnnotations[action.payload.annotation.target][ + action.payload.previousCalledAfterName ?? '' + ]; + + if (!state.annotations.calledAfterAnnotations[action.payload.annotation.target]) { + state.annotations.calledAfterAnnotations[action.payload.annotation.target] = {}; } - updateCreationOrChangedCount( - state, - state.annotations.calledAfterAnnotations[action.payload.target][action.payload.calledAfterName], - ); + updateCreationOrChangedCount(state, oldAnnotation); + + state.annotations.calledAfterAnnotations[action.payload.annotation.target][ + action.payload.annotation.calledAfterName + ] = withAuthorAndReviewers(oldAnnotation, action.payload.annotation, state.username); + + // Delete old annotation + if (action.payload.previousCalledAfterName !== action.payload.annotation.calledAfterName) { + delete state.annotations.calledAfterAnnotations[action.payload.annotation.target][ + action.payload.previousCalledAfterName ?? '' + ]; + } - state.annotations.calledAfterAnnotations[action.payload.target][action.payload.calledAfterName] = - withAuthorAndReviewers( - state.annotations.calledAfterAnnotations[action.payload.target][action.payload.calledAfterName], - action.payload, - state.username, - ); updateQueue(state); }, removeCalledAfterAnnotation(state, action: PayloadAction) { @@ -310,19 +320,27 @@ const annotationsSlice = createSlice({ updateQueue(state); }, - upsertGroupAnnotation(state, action: PayloadAction) { - if (!state.annotations.groupAnnotations[action.payload.target]) { - state.annotations.groupAnnotations[action.payload.target] = {}; + upsertGroupAnnotation( + state, + action: PayloadAction<{ previousGroupName?: string; annotation: GroupAnnotation }>, + ) { + const oldAnnotation = + state.annotations.groupAnnotations[action.payload.annotation.target][ + action.payload.previousGroupName ?? '' + ]; + + if (!state.annotations.groupAnnotations[action.payload.annotation.target]) { + state.annotations.groupAnnotations[action.payload.annotation.target] = {}; } else { - const targetGroups = state.annotations.groupAnnotations[action.payload.target]; + const targetGroups = state.annotations.groupAnnotations[action.payload.annotation.target]; const otherGroupNames = Object.values(targetGroups) - .filter((group) => group.groupName !== action.payload.groupName) + .filter((group) => group.groupName !== action.payload.annotation.groupName) .map((group) => group.groupName); for (const nameOfGroup of otherGroupNames) { let needsChange = false; const group = targetGroups[nameOfGroup]; - const currentAnnotationParameter = action.payload.parameters; + const currentAnnotationParameter = action.payload.annotation.parameters; const currentGroupParameter = [...group.parameters]; for (const parameter of currentAnnotationParameter) { const index = currentGroupParameter.indexOf(parameter); @@ -333,7 +351,7 @@ const annotationsSlice = createSlice({ } if (currentGroupParameter.length < 1) { removeGroupAnnotation({ - target: action.payload.target, + target: action.payload.annotation.target, groupName: group.groupName, }); } else if (needsChange) { @@ -350,17 +368,17 @@ const annotationsSlice = createSlice({ } } - updateCreationOrChangedCount( - state, - state.annotations.groupAnnotations[action.payload.target][action.payload.groupName], - ); + updateCreationOrChangedCount(state, oldAnnotation); - state.annotations.groupAnnotations[action.payload.target][action.payload.groupName] = - withAuthorAndReviewers( - state.annotations.groupAnnotations[action.payload.target][action.payload.groupName], - action.payload, - state.username, - ); + state.annotations.groupAnnotations[action.payload.annotation.target][action.payload.annotation.groupName] = + withAuthorAndReviewers(oldAnnotation, action.payload.annotation, state.username); + + // Delete old annotation + if (action.payload.previousGroupName !== action.payload.annotation.groupName) { + delete state.annotations.groupAnnotations[action.payload.annotation.target][ + action.payload.previousGroupName ?? '' + ]; + } updateQueue(state); }, @@ -424,7 +442,7 @@ const annotationsSlice = createSlice({ updateQueue(state); }, - addPureAnnotation(state, action: PayloadAction) { + upsertPureAnnotation(state, action: PayloadAction) { updateCreationOrChangedCount(state, state.annotations.pureAnnotations[action.payload.target]); state.annotations.pureAnnotations[action.payload.target] = withAuthorAndReviewers( @@ -448,7 +466,7 @@ const annotationsSlice = createSlice({ updateQueue(state); }, - addRemoveAnnotation(state, action: PayloadAction) { + upsertRemoveAnnotation(state, action: PayloadAction) { updateCreationOrChangedCount(state, state.annotations.removeAnnotations[action.payload.target]); state.annotations.removeAnnotations[action.payload.target] = withAuthorAndReviewers( @@ -459,7 +477,7 @@ const annotationsSlice = createSlice({ updateQueue(state); }, - addRemoveAnnotations(state, action: PayloadAction) { + upsertRemoveAnnotations(state, action: PayloadAction) { action.payload.forEach((annotation) => { updateCreationOrChangedCount(state, state.annotations.removeAnnotations[annotation.target]); @@ -697,11 +715,11 @@ export const { upsertMoveAnnotations, removeMoveAnnotation, reviewMoveAnnotation, - addPureAnnotation, + upsertPureAnnotation, removePureAnnotation, reviewPureAnnotation, - addRemoveAnnotation, - addRemoveAnnotations, + upsertRemoveAnnotation, + upsertRemoveAnnotations, removeRemoveAnnotation, reviewRemoveAnnotation, upsertRenameAnnotation, diff --git a/api-editor/gui/src/features/annotations/batchforms/DestinationBatchForm.tsx b/api-editor/gui/src/features/annotations/batchforms/DestinationBatchForm.tsx deleted file mode 100644 index a7384dfba..000000000 --- a/api-editor/gui/src/features/annotations/batchforms/DestinationBatchForm.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { FormControl, FormErrorIcon, FormErrorMessage, FormLabel, Input, Text as ChakraText } from '@chakra-ui/react'; -import React, { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { useAppDispatch } from '../../../app/hooks'; -import { PythonDeclaration } from '../../packageData/model/PythonDeclaration'; -import { AnnotationBatchForm } from './AnnotationBatchForm'; -import { hideAnnotationForm } from '../../ui/uiSlice'; -import { ConfirmAnnotations } from './ConfirmAnnotations'; - -interface DestinationBatchFormProps { - targets: PythonDeclaration[]; - annotationType: string; - description: string; - onUpsertAnnotation: (data: DestinationBatchFormState) => void; -} - -export interface DestinationBatchFormState { - destination: string; -} - -export const DestinationBatchForm: React.FC = function ({ - targets, - annotationType, - description, - onUpsertAnnotation, -}) { - const dispatch = useAppDispatch(); - - const { - handleSubmit, - register, - formState: { errors }, - } = useForm({ - defaultValues: { - destination: '', - }, - }); - - let [confirmWindowVisible, setConfirmWindowVisible] = useState(false); - let [data, setData] = useState({ destination: '' }); - - // Event handlers ---------------------------------------------------------- - - const handleSave = (annotationData: DestinationBatchFormState) => { - onUpsertAnnotation({ ...annotationData }); - - setConfirmWindowVisible(false); - dispatch(hideAnnotationForm()); - }; - - const handleConfirm = (newData: DestinationBatchFormState) => { - setData(newData); - setConfirmWindowVisible(true); - }; - - const handleCancel = () => { - dispatch(hideAnnotationForm()); - }; - // Rendering ------------------------------------------------------------------------------------------------------- - - return ( - <> - - - Destination module: - - - {errors.destination?.message} - - - - This will annotate classes and global functions. - - {confirmWindowVisible && ( - handleSave(data)} - setConfirmVisible={setConfirmWindowVisible} - /> - )} - - ); -}; diff --git a/api-editor/gui/src/features/annotations/batchforms/EmptyBatchForm.tsx b/api-editor/gui/src/features/annotations/batchforms/EmptyBatchForm.tsx deleted file mode 100644 index 79dd5211a..000000000 --- a/api-editor/gui/src/features/annotations/batchforms/EmptyBatchForm.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { FormLabel } from '@chakra-ui/react'; -import React, { useState } from 'react'; -import { useAppDispatch } from '../../../app/hooks'; -import { PythonDeclaration } from '../../packageData/model/PythonDeclaration'; -import { AnnotationBatchForm } from './AnnotationBatchForm'; -import { hideAnnotationForm } from '../../ui/uiSlice'; -import { ConfirmAnnotations } from './ConfirmAnnotations'; - -interface EmptyBatchFormProps { - targets: PythonDeclaration[]; - annotationType: string; - description: string; - onUpsertAnnotation: () => void; - targetLabel: string; -} - -export const EmptyBatchForm: React.FC = function ({ - targets, - annotationType, - description, - onUpsertAnnotation, - targetLabel, -}) { - const dispatch = useAppDispatch(); - - let [confirmWindowVisible, setConfirmWindowVisible] = useState(false); - - // Event handlers ---------------------------------------------------------- - - const handleSave = () => { - onUpsertAnnotation(); - - setConfirmWindowVisible(false); - dispatch(hideAnnotationForm()); - }; - - const handleConfirm = () => { - setConfirmWindowVisible(true); - }; - - const handleCancel = () => { - dispatch(hideAnnotationForm()); - }; - // Rendering ------------------------------------------------------------------------------------------------------- - - return ( - <> - - {targetLabel} - - {confirmWindowVisible && ( - handleSave()} - setConfirmVisible={setConfirmWindowVisible} - /> - )} - - ); -}; diff --git a/api-editor/gui/src/features/annotations/batchforms/MoveBatchForm.tsx b/api-editor/gui/src/features/annotations/batchforms/MoveBatchForm.tsx index 0e4f98e86..301c9006a 100644 --- a/api-editor/gui/src/features/annotations/batchforms/MoveBatchForm.tsx +++ b/api-editor/gui/src/features/annotations/batchforms/MoveBatchForm.tsx @@ -1,11 +1,23 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useAppDispatch } from '../../../app/hooks'; import { PythonDeclaration } from '../../packageData/model/PythonDeclaration'; import { PythonClass } from '../../packageData/model/PythonClass'; -import { DestinationBatchForm, DestinationBatchFormState } from './DestinationBatchForm'; import { PythonFunction } from '../../packageData/model/PythonFunction'; import { MoveAnnotation } from '../versioning/AnnotationStoreV2'; import { upsertMoveAnnotations } from '../annotationSlice'; +import { hideAnnotationForm } from '../../ui/uiSlice'; +import { AnnotationBatchForm } from './AnnotationBatchForm'; +import { ConfirmAnnotations } from './ConfirmAnnotations'; +import { + FormControl, + FormErrorIcon, + FormErrorMessage, + FormLabel, + Input, + Text as ChakraText, + Textarea, +} from '@chakra-ui/react'; +import { useForm } from 'react-hook-form'; interface MoveBatchFormProps { targets: PythonDeclaration[]; @@ -28,6 +40,7 @@ export const MoveBatchForm: React.FC = function ({ targets } all.push({ target: targetPath, destination: data.destination, + comment: data.comment, }); }); dispatch(upsertMoveAnnotations(all)); @@ -44,3 +57,94 @@ export const MoveBatchForm: React.FC = function ({ targets } /> ); }; + +interface DestinationBatchFormProps { + targets: PythonDeclaration[]; + annotationType: string; + description: string; + onUpsertAnnotation: (data: DestinationBatchFormState) => void; +} + +export interface DestinationBatchFormState { + destination: string; + comment: string; +} + +export const DestinationBatchForm: React.FC = function ({ + targets, + annotationType, + description, + onUpsertAnnotation, +}) { + const dispatch = useAppDispatch(); + + const { + handleSubmit, + register, + formState: { errors }, + } = useForm({ + defaultValues: { + destination: '', + comment: '', + }, + }); + + let [confirmWindowVisible, setConfirmWindowVisible] = useState(false); + let [data, setData] = useState({ destination: '', comment: '' }); + + // Event handlers ---------------------------------------------------------- + + const handleSave = (annotationData: DestinationBatchFormState) => { + onUpsertAnnotation({ ...annotationData }); + + setConfirmWindowVisible(false); + dispatch(hideAnnotationForm()); + }; + + const handleConfirm = (newData: DestinationBatchFormState) => { + setData(newData); + setConfirmWindowVisible(true); + }; + + const handleCancel = () => { + dispatch(hideAnnotationForm()); + }; + // Rendering ------------------------------------------------------------------------------------------------------- + + return ( + <> + + + Destination module: + + + {errors.destination?.message} + + + + + Comment: +