From 9ec9ed3db82d3be013e9498cb8999c1d62a003ab Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Fri, 3 Jun 2022 11:42:54 +0200 Subject: [PATCH 01/18] feat: process description annotations in backend --- .../api_editor/model/editorAnnotations.kt | 16 ++- .../DescriptionAnnotationProcessor.kt | 34 +++++++ .../DescriptionAnnotationProcessorTest.kt | 99 +++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/DescriptionAnnotationProcessor.kt create mode 100644 api-editor/backend/src/test/kotlin/com/larsreimann/api_editor/transformation/DescriptionAnnotationProcessorTest.kt diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt index 59d11c181..456b42346 100644 --- a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt @@ -60,6 +60,13 @@ data class ConstantAnnotation(val defaultValue: DefaultValue) : EditorAnnotation override val validTargets = PARAMETERS } +@Serializable +data class DescriptionAnnotation(val newDescription: String): EditorAnnotation() { + + @Transient + override val validTargets = ANY_DECLARATION +} + @Serializable data class EnumAnnotation(val enumName: String, val pairs: List) : EditorAnnotation() { @@ -112,7 +119,7 @@ object RemoveAnnotation : EditorAnnotation() { @Serializable data class RenameAnnotation(val newName: String) : EditorAnnotation() { @Transient - override val validTargets = CLASSES.union(FUNCTIONS).union(PARAMETERS) + override val validTargets = ANY_DECLARATION } @Serializable @@ -164,6 +171,13 @@ enum class AnnotationTarget(private val target: String) { } } +val ANY_DECLARATION = setOf( + CLASS, + GLOBAL_FUNCTION, + METHOD, + CONSTRUCTOR_PARAMETER, + FUNCTION_PARAMETER +) val GLOBAL_DECLARATIONS = setOf(CLASS, GLOBAL_FUNCTION) val CLASSES = setOf(CLASS) val FUNCTIONS = setOf(GLOBAL_FUNCTION, METHOD) diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/DescriptionAnnotationProcessor.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/DescriptionAnnotationProcessor.kt new file mode 100644 index 000000000..03bc5b121 --- /dev/null +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/DescriptionAnnotationProcessor.kt @@ -0,0 +1,34 @@ +package com.larsreimann.api_editor.transformation + +import com.larsreimann.api_editor.model.DescriptionAnnotation +import com.larsreimann.api_editor.mutable_model.PythonClass +import com.larsreimann.api_editor.mutable_model.PythonDeclaration +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.modeling.descendants + +/** + * Processes and removes `@description` annotations. + */ +fun PythonPackage.processDescriptionAnnotations() { + this.descendants() + .filterIsInstance() + .forEach { it.processDescriptionAnnotations() } +} + +private fun PythonDeclaration.processDescriptionAnnotations() { + this.annotations + .filterIsInstance() + .forEach { + when (this) { + is PythonClass -> this.description = it.newDescription + is PythonFunction -> this.description = it.newDescription + is PythonParameter -> this.description = it.newDescription + else -> { + // Do nothing + } + } + this.annotations.remove(it) + } +} diff --git a/api-editor/backend/src/test/kotlin/com/larsreimann/api_editor/transformation/DescriptionAnnotationProcessorTest.kt b/api-editor/backend/src/test/kotlin/com/larsreimann/api_editor/transformation/DescriptionAnnotationProcessorTest.kt new file mode 100644 index 000000000..57a5ca978 --- /dev/null +++ b/api-editor/backend/src/test/kotlin/com/larsreimann/api_editor/transformation/DescriptionAnnotationProcessorTest.kt @@ -0,0 +1,99 @@ +package com.larsreimann.api_editor.transformation + +import com.larsreimann.api_editor.model.DescriptionAnnotation +import com.larsreimann.api_editor.mutable_model.PythonClass +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class DescriptionAnnotationProcessorTest { + private lateinit var testClass: PythonClass + private lateinit var testFunction: PythonFunction + private lateinit var testParameter: PythonParameter + private lateinit var testPackage: PythonPackage + + @BeforeEach + fun reset() { + testClass = PythonClass( + name = "TestClass", + description = "Lorem ipsum", + annotations = mutableListOf(DescriptionAnnotation("Important class")) + ) + testParameter = PythonParameter( + name = "testParameter", + description = "Lorem ipsum", + annotations = mutableListOf(DescriptionAnnotation("Important parameter")) + ) + testFunction = PythonFunction( + name = "testFunction", + description = "Lorem ipsum", + annotations = mutableListOf(DescriptionAnnotation("Important function")), + parameters = mutableListOf(testParameter) + ) + testPackage = PythonPackage( + distribution = "testPackage", + name = "testPackage", + version = "1.0.0", + modules = listOf( + PythonModule( + name = "testModule", + classes = listOf(testClass), + functions = listOf(testFunction) + ) + ) + ) + } + + @Test + fun `should process DescriptionAnnotation of classes`() { + testPackage.processDescriptionAnnotations() + + testClass.description shouldBe "Important class" + } + + @Test + fun `should remove DescriptionAnnotation of classes`() { + testPackage.processDescriptionAnnotations() + + testClass.annotations + .filterIsInstance() + .shouldBeEmpty() + } + + @Test + fun `should process DescriptionAnnotation of functions`() { + testPackage.processDescriptionAnnotations() + + testFunction.description shouldBe "Important function" + } + + @Test + fun `should remove DescriptionAnnotation of functions`() { + testPackage.processDescriptionAnnotations() + + testFunction.annotations + .filterIsInstance() + .shouldBeEmpty() + } + + @Test + fun `should process DescriptionAnnotation of parameters`() { + testPackage.processDescriptionAnnotations() + + testParameter.description shouldBe "Important parameter" + } + + @Test + fun `should remove DescriptionAnnotation of parameters`() { + testPackage.processDescriptionAnnotations() + + testParameter.annotations + .filterIsInstance() + .shouldBeEmpty() + } +} From 2d185156df163d646e89fc636a2b7717ecfeb83b Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Fri, 3 Jun 2022 11:43:33 +0200 Subject: [PATCH 02/18] feat: update validation in backend --- .../validation/AnnotationValidator.kt | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt index 5ba835376..25e117179 100644 --- a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt @@ -181,15 +181,28 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython companion object { private var possibleCombinations = buildMap> { - this["Attribute"] = mutableSetOf("Rename") - this["Boundary"] = mutableSetOf("Group", "Optional", "Rename", "Required") - this["CalledAfter"] = mutableSetOf("CalledAfter", "Group", "Move", "Pure", "Rename") + this["Attribute"] = mutableSetOf("Description", "Rename") + this["Boundary"] = mutableSetOf("Description", "Group", "Optional", "Rename", "Required") + this["CalledAfter"] = mutableSetOf("CalledAfter", "Description", "Group", "Move", "Pure", "Rename") this["Constant"] = mutableSetOf() - this["Enum"] = mutableSetOf("Group", "Rename", "Required") + this["Description"] = mutableSetOf( + "Attribute", + "Boundary", + "CalledAfter", + "Enum", + "Group", + "Move", + "Optional", + "Pure", + "Rename", + "Required" + ) + this["Enum"] = mutableSetOf("Description", "Group", "Rename", "Required") this["Group"] = mutableSetOf( "Boundary", "CalledAfter", + "Description", "Enum", "Group", "Move", @@ -198,14 +211,15 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython "Rename", "Required" ) - this["Move"] = mutableSetOf("CalledAfter", "Group", "Pure", "Rename") - this["Optional"] = mutableSetOf("Boundary", "Group", "Rename") - this["Pure"] = mutableSetOf("CalledAfter", "Group", "Move", "Rename") + this["Move"] = mutableSetOf("CalledAfter", "Description", "Group", "Pure", "Rename") + this["Optional"] = mutableSetOf("Boundary", "Description", "Group", "Rename") + this["Pure"] = mutableSetOf("CalledAfter", "Description", "Group", "Move", "Rename") this["Remove"] = mutableSetOf() this["Rename"] = mutableSetOf( "Attribute", "Boundary", "CalledAfter", + "Description", "Enum", "Group", "Move", @@ -213,7 +227,7 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython "Pure", "Required" ) - this["Required"] = mutableSetOf("Boundary", "Enum", "Group", "Rename") + this["Required"] = mutableSetOf("Boundary", "Description", "Enum", "Group", "Rename") } } } From e12901197d15f00be3d75d9ae08727051bfb7734 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Fri, 3 Jun 2022 11:47:06 +0200 Subject: [PATCH 03/18] feat(backend): actually trigger processing --- .../larsreimann/api_editor/transformation/TransformationPlan.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt index 4975c7729..6166d439e 100644 --- a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt @@ -28,6 +28,7 @@ private fun PythonPackage.preprocess() { */ private fun PythonPackage.processAnnotations() { processRemoveAnnotations() + processDescriptionAnnotations() processRenameAnnotations() processMoveAnnotations() processBoundaryAnnotations() From 2cfafbc49abbf2f67c15797a84d950e8b31256e0 Mon Sep 17 00:00:00 2001 From: lars-reimann Date: Fri, 3 Jun 2022 09:51:02 +0000 Subject: [PATCH 04/18] style: apply automatic fixes of linters --- .../com/larsreimann/api_editor/model/editorAnnotations.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt index 456b42346..bdc761051 100644 --- a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt @@ -61,7 +61,7 @@ data class ConstantAnnotation(val defaultValue: DefaultValue) : EditorAnnotation } @Serializable -data class DescriptionAnnotation(val newDescription: String): EditorAnnotation() { +data class DescriptionAnnotation(val newDescription: String) : EditorAnnotation() { @Transient override val validTargets = ANY_DECLARATION From 35991781e6a2253629a2ddcdb6e10dc736a325df Mon Sep 17 00:00:00 2001 From: Arsam Islami Date: Mon, 13 Jun 2022 12:42:32 +0200 Subject: [PATCH 05/18] feat: Adding description annotation (WIP) --- .../api_editor/model/editorAnnotations.kt | 6 ++ .../transformation/TransformationPlan.kt | 1 + .../python_api_editor/AnnotationDropdown.kt | 5 + .../model/AnnotatedPythonPackageBuilder.ts | 7 ++ .../model/InferableAnnotation.ts | 10 ++ .../annotations/AnnotationDropdown.tsx | 6 ++ .../annotations/AnnotationImportDialog.tsx | 1 + .../features/annotations/AnnotationView.tsx | 13 +++ .../features/annotations/annotationSlice.ts | 28 ++++++ .../annotations/forms/DescriptionForm.tsx | 91 +++++++++++++++++++ .../packageData/selectionView/ClassView.tsx | 2 +- .../selectionView/FunctionView.tsx | 1 + api-editor/gui/src/features/ui/uiSlice.ts | 7 ++ 13 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 api-editor/gui/src/features/annotations/forms/DescriptionForm.tsx diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt index bdc761051..01a35d232 100644 --- a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt @@ -122,6 +122,12 @@ data class RenameAnnotation(val newName: String) : EditorAnnotation() { override val validTargets = ANY_DECLARATION } +@Serializable +data class DescriptionAnnotation(val newDescription: String) : EditorAnnotation() { + @Transient + override val validTargets = CLASSES.union(FUNCTIONS).union(PARAMETERS) +} + @Serializable object RequiredAnnotation : EditorAnnotation() { @Transient diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt index 70c7e01c3..83635271f 100644 --- a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt @@ -30,6 +30,7 @@ private fun PythonPackage.processAnnotations() { processRemoveAnnotations() processDescriptionAnnotations() processRenameAnnotations() + processDescriptionAnnotations() processMoveAnnotations() processBoundaryAnnotations() processParameterAnnotations() diff --git a/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt b/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt index 3b03ef8f8..1d24cced1 100644 --- a/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt +++ b/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt @@ -96,6 +96,11 @@ fun AnnotationDropdown( Text(labels.getString("AnnotationDropdown.Option.Required")) } } + if (showDescription) { + DropdownMenuItem(onClick = {}) { + Text(labels.getString("AnnotationDropdown.Option.Description")) + } + } } } } diff --git a/api-editor/gui/src/features/annotatedPackageData/model/AnnotatedPythonPackageBuilder.ts b/api-editor/gui/src/features/annotatedPackageData/model/AnnotatedPythonPackageBuilder.ts index 5f059188d..672ec3799 100644 --- a/api-editor/gui/src/features/annotatedPackageData/model/AnnotatedPythonPackageBuilder.ts +++ b/api-editor/gui/src/features/annotatedPackageData/model/AnnotatedPythonPackageBuilder.ts @@ -25,6 +25,7 @@ import { InferableRemoveAnnotation, InferableRenameAnnotation, InferableRequiredAnnotation, + InferableDescriptionAnnotation, } from './InferableAnnotation'; export class AnnotatedPythonPackageBuilder { @@ -242,6 +243,12 @@ export class AnnotatedPythonPackageBuilder { return new InferableRequiredAnnotation(); } break; + case 'Description': + const descriptionAnnotation = this.annotationStore.descriptions[target]; + if (descriptionAnnotation) { + return new InferableDescriptionAnnotation(descriptionAnnotation); + } + break; } return undefined; } diff --git a/api-editor/gui/src/features/annotatedPackageData/model/InferableAnnotation.ts b/api-editor/gui/src/features/annotatedPackageData/model/InferableAnnotation.ts index 9cd13be13..df7da18ff 100644 --- a/api-editor/gui/src/features/annotatedPackageData/model/InferableAnnotation.ts +++ b/api-editor/gui/src/features/annotatedPackageData/model/InferableAnnotation.ts @@ -12,6 +12,7 @@ import { MoveAnnotation, OptionalAnnotation, RenameAnnotation, + DescriptionAnnotation, } from '../../annotations/annotationSlice'; const dataPathPrefix = 'com.larsreimann.api_editor.model.'; @@ -158,3 +159,12 @@ export class InferableRemoveAnnotation extends InferableAnnotation { super(dataPathPrefix + 'RemoveAnnotation'); } } + +export class InferableDescriptionAnnotation extends InferableAnnotation { + readonly newDescription: string; + + constructor(descriptionAnnotation: DescriptionAnnotation) { + super(dataPathPrefix + 'DescriptionAnnotation'); + this.newDescription = descriptionAnnotation.newDescription; + } +} diff --git a/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx b/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx index 4b1f7812d..a0e5c2820 100644 --- a/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx @@ -13,6 +13,7 @@ import { showMoveAnnotationForm, showOptionalAnnotationForm, showRenameAnnotationForm, + showDescriptionAnnotationForm, } from '../ui/uiSlice'; interface AnnotationDropdownProps { @@ -28,6 +29,7 @@ interface AnnotationDropdownProps { showRename?: boolean; showRequired?: boolean; showRemove?: boolean; + showDescription?: boolean; target: string; } @@ -44,6 +46,7 @@ export const AnnotationDropdown: React.FC = function ({ showRename = false, showRequired = false, showRemove = false, + showDescription = false, target, }) { const dispatch = useAppDispatch(); @@ -104,6 +107,9 @@ export const AnnotationDropdown: React.FC = function ({ dispatch(showRenameAnnotationForm(target))}>@rename )} {showRequired && dispatch(addRequired({ target }))}>@required} + {showRename && ( + dispatch(showDescriptionAnnotationForm(target))}>@description + )} diff --git a/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx b/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx index 5d006e59f..e3729329d 100644 --- a/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx @@ -35,6 +35,7 @@ export const AnnotationImportDialog: React.FC = function () { renamings: {}, requireds: {}, removes: {}, + descriptions: {}, }); const dispatch = useAppDispatch(); diff --git a/api-editor/gui/src/features/annotations/AnnotationView.tsx b/api-editor/gui/src/features/annotations/AnnotationView.tsx index 1edb4e38a..e8906e63e 100644 --- a/api-editor/gui/src/features/annotations/AnnotationView.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationView.tsx @@ -19,6 +19,7 @@ import { removeRenaming, removeRequired, removeRemove, + removeDescription, selectAttribute, selectBoundary, selectCalledAfters, @@ -31,6 +32,7 @@ import { selectRenaming, selectRequired, selectRemove, + selectDescription, } from './annotationSlice'; import { showAttributeAnnotationForm, @@ -41,6 +43,7 @@ import { showMoveAnnotationForm, showOptionalAnnotationForm, showRenameAnnotationForm, + showDescriptionAnnotationForm, } from '../ui/uiSlice'; interface AnnotationViewProps { @@ -62,6 +65,7 @@ export const AnnotationView: React.FC = function ({ target const removeAnnotation = useAppSelector(selectRemove(target)); const renameAnnotation = useAppSelector(selectRenaming(target)); const requiredAnnotation = useAppSelector(selectRequired(target)); + const descriptionAnnotation = useAppSelector(selectDescription(target)); if ( !attributeAnnotation && @@ -75,6 +79,7 @@ export const AnnotationView: React.FC = function ({ target !pureAnnotation && !removeAnnotation && !renameAnnotation && + !descriptionAnnotation && !requiredAnnotation ) { // eslint-disable-next-line react/jsx-no-useless-fragment @@ -159,6 +164,14 @@ export const AnnotationView: React.FC = function ({ target /> )} {requiredAnnotation && dispatch(removeRequired(target))} />} + {descriptionAnnotation && ( + dispatch(showDescriptionAnnotationForm(target))} + onDelete={() => dispatch(removeDescription(target))} + /> + )} ); }; diff --git a/api-editor/gui/src/features/annotations/annotationSlice.ts b/api-editor/gui/src/features/annotations/annotationSlice.ts index 9fb4bee6e..e5edeb6dd 100644 --- a/api-editor/gui/src/features/annotations/annotationSlice.ts +++ b/api-editor/gui/src/features/annotations/annotationSlice.ts @@ -39,6 +39,9 @@ export interface AnnotationStore { removes: { [target: string]: RemoveAnnotation; }; + descriptions: { + [target: string]: DescriptionAnnotation; + }; } export interface AttributeAnnotation { @@ -256,6 +259,18 @@ export interface RemoveAnnotation { readonly target: string; } +export interface DescriptionAnnotation { + /** + * ID of the annotated Python declaration. + */ + readonly target: string; + + /** + * New name for the declaration. + */ + readonly newDescription: string; +} + // Initial state ------------------------------------------------------------------------------------------------------- export const initialState: AnnotationStore = { @@ -271,6 +286,7 @@ export const initialState: AnnotationStore = { renamings: {}, requireds: {}, removes: {}, + descriptions: {}, }; // Thunks -------------------------------------------------------------------------------------------------------------- @@ -425,6 +441,12 @@ const annotationsSlice = createSlice({ removeRemove(state, action: PayloadAction) { delete state.removes[action.payload]; }, + upsertDescription(state, action: PayloadAction) { + state.descriptions[action.payload.target] = action.payload; + }, + removeDescription(state, action: PayloadAction) { + delete state.descriptions[action.payload]; + }, }, extraReducers(builder) { builder.addCase(initializeAnnotations.fulfilled, (state, action) => action.payload); @@ -458,6 +480,8 @@ export const { removeRenaming, addRequired, removeRequired, + upsertDescription, + removeDescription, addRemove, removeRemove, } = actions; @@ -512,3 +536,7 @@ export const selectRemove = (target: string) => (state: RootState): RemoveAnnotation | undefined => selectAnnotations(state).removes[target]; +export const selectDescription = + (target: string) => + (state: RootState): DescriptionAnnotation | undefined => + selectAnnotations(state).descriptions[target]; diff --git a/api-editor/gui/src/features/annotations/forms/DescriptionForm.tsx b/api-editor/gui/src/features/annotations/forms/DescriptionForm.tsx new file mode 100644 index 000000000..595702564 --- /dev/null +++ b/api-editor/gui/src/features/annotations/forms/DescriptionForm.tsx @@ -0,0 +1,91 @@ +import { FormControl, FormErrorIcon, FormErrorMessage, FormLabel, Input } from '@chakra-ui/react'; +import React, { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useAppDispatch, useAppSelector } from '../../../app/hooks'; +import { pythonIdentifierPattern } from '../../../common/validation'; +import { PythonDeclaration } from '../../packageData/model/PythonDeclaration'; +import { selectDescription, upsertDescription } from '../annotationSlice'; +import { AnnotationForm } from './AnnotationForm'; +import { hideAnnotationForm } from '../../ui/uiSlice'; + +interface DescriptionFormProps { + readonly target: PythonDeclaration; +} + +interface DescriptionFormState { + newDescription: string; +} + +export const DescriptionForm: React.FC = function ({ target }) { + const targetPath = target.pathAsString(); + const prevNewDescription = useAppSelector(selectDescription(targetPath))?.newDescription; + const oldDescription = target.description; + + // Hooks ----------------------------------------------------------------------------------------------------------- + + const dispatch = useAppDispatch(); + const { + register, + handleSubmit, + setFocus, + reset, + formState: { errors }, + } = useForm({ + defaultValues: { + newDescription: '', + }, + }); + + useEffect(() => { + try { + setFocus('newDescription'); + } catch (e) { + // ignore + } + }, [setFocus]); + + useEffect(() => { + reset({ + newDescription: prevNewDescription || oldDescription, + }); + }, [reset, prevNewDescription, oldDescription]); + + // Event handlers -------------------------------------------------------------------------------------------------- + + const onSave = (data: DescriptionFormState) => { + dispatch( + upsertDescription({ + target: targetPath, + ...data, + }), + ); + dispatch(hideAnnotationForm()); + }; + + const onCancel = () => { + dispatch(hideAnnotationForm()); + }; + + // Rendering ------------------------------------------------------------------------------------------------------- + + return ( + + + New description for "{oldDescription}": + + + {errors.newDescription?.message} + + + + ); +}; diff --git a/api-editor/gui/src/features/packageData/selectionView/ClassView.tsx b/api-editor/gui/src/features/packageData/selectionView/ClassView.tsx index cb1c8f577..ce51ec3f5 100644 --- a/api-editor/gui/src/features/packageData/selectionView/ClassView.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/ClassView.tsx @@ -20,7 +20,7 @@ export const ClassView: React.FC = function ({ pythonClass }) { {pythonClass.name} {!pythonClass.isPublic && '(private)'} - {pythonClass.isPublic && } + {pythonClass.isPublic && } diff --git a/api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx b/api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx index f59e2b58e..4eaf972f5 100644 --- a/api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx @@ -39,6 +39,7 @@ export const FunctionView: React.FC = function ({ pythonFunct showPure showRemove showRename + showDescription /> )} diff --git a/api-editor/gui/src/features/ui/uiSlice.ts b/api-editor/gui/src/features/ui/uiSlice.ts index 56fd61598..482c6801c 100644 --- a/api-editor/gui/src/features/ui/uiSlice.ts +++ b/api-editor/gui/src/features/ui/uiSlice.ts @@ -205,6 +205,12 @@ const uiSlice = createSlice({ target: action.payload, }; }, + showDescriptionAnnotationForm(state, action: PayloadAction) { + state.currentUserAction = { + type: 'description', + target: action.payload, + }; + }, hideAnnotationForm(state) { state.currentUserAction = NoUserAction; }, @@ -262,6 +268,7 @@ export const { showMoveAnnotationForm, showOptionalAnnotationForm, showRenameAnnotationForm, + showDescriptionAnnotationForm, hideAnnotationForm, toggleIsExpandedInTreeView, From 3ab3f554bfa5c3e27b2526b13c380823c01955b0 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 13 Jun 2022 14:36:41 +0200 Subject: [PATCH 06/18] revert: changes to Kotlin files --- .../com/larsreimann/api_editor/model/editorAnnotations.kt | 6 ------ .../api_editor/transformation/TransformationPlan.kt | 1 - .../com/larsreimann/python_api_editor/AnnotationDropdown.kt | 5 ----- 3 files changed, 12 deletions(-) diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt index 01a35d232..bdc761051 100644 --- a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt @@ -122,12 +122,6 @@ data class RenameAnnotation(val newName: String) : EditorAnnotation() { override val validTargets = ANY_DECLARATION } -@Serializable -data class DescriptionAnnotation(val newDescription: String) : EditorAnnotation() { - @Transient - override val validTargets = CLASSES.union(FUNCTIONS).union(PARAMETERS) -} - @Serializable object RequiredAnnotation : EditorAnnotation() { @Transient diff --git a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt index 83635271f..70c7e01c3 100644 --- a/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt +++ b/api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt @@ -30,7 +30,6 @@ private fun PythonPackage.processAnnotations() { processRemoveAnnotations() processDescriptionAnnotations() processRenameAnnotations() - processDescriptionAnnotations() processMoveAnnotations() processBoundaryAnnotations() processParameterAnnotations() diff --git a/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt b/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt index 1d24cced1..3b03ef8f8 100644 --- a/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt +++ b/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt @@ -96,11 +96,6 @@ fun AnnotationDropdown( Text(labels.getString("AnnotationDropdown.Option.Required")) } } - if (showDescription) { - DropdownMenuItem(onClick = {}) { - Text(labels.getString("AnnotationDropdown.Option.Description")) - } - } } } } From c1726ca39e97c24d688eece4d1d72fd9fed8883b Mon Sep 17 00:00:00 2001 From: Arsam Islami Date: Mon, 13 Jun 2022 16:42:42 +0200 Subject: [PATCH 07/18] feat: Added description annotation, a filter for it and adjusted the heatmap to also count description annotations. --- .../python_api_editor/AnnotationDropdown.kt | 5 ++++ .../src/main/resources/i18n/labels.properties | 1 + api-editor/gui/src/app/App.tsx | 2 ++ .../gui/src/common/FilterHelpButton.tsx | 2 +- .../model/InferableAnnotation.ts | 19 +++++++------- .../annotations/AnnotationDropdown.tsx | 10 +++---- .../annotations/AnnotationImportDialog.tsx | 2 +- .../features/annotations/AnnotationView.tsx | 26 +++++++++---------- .../features/annotations/annotationSlice.ts | 2 +- .../annotations/forms/DescriptionForm.tsx | 14 ++++------ .../packageData/model/PythonDeclaration.ts | 1 + .../model/filters/AnnotationFilter.ts | 4 +++ .../model/filters/filterFactory.ts | 2 ++ .../selectionView/FunctionView.tsx | 2 +- .../selectionView/ParameterNode.tsx | 1 + .../packageData/treeView/ClassNode.tsx | 1 + .../packageData/treeView/FunctionNode.tsx | 1 + .../packageData/treeView/ParameterNode.tsx | 1 + api-editor/gui/src/features/ui/uiSlice.ts | 8 +++++- 19 files changed, 63 insertions(+), 41 deletions(-) diff --git a/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt b/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt index 3b03ef8f8..725be393c 100644 --- a/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt +++ b/api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt @@ -56,6 +56,11 @@ fun AnnotationDropdown( Text(labels.getString("AnnotationDropdown.Option.Constant")) } } + if (showDescription) { + DropdownMenuItem(onClick = {}) { + Text(labels.getString("AnnotationDropdown.Option.Description")) + } + } if (showEnum) { DropdownMenuItem(onClick = {}) { Text(labels.getString("AnnotationDropdown.Option.Enum")) diff --git a/api-editor/desktop/src/main/resources/i18n/labels.properties b/api-editor/desktop/src/main/resources/i18n/labels.properties index 57226e31a..7ac66b3fd 100644 --- a/api-editor/desktop/src/main/resources/i18n/labels.properties +++ b/api-editor/desktop/src/main/resources/i18n/labels.properties @@ -28,6 +28,7 @@ AnnotationDropdown.Option.Attribute=@attribute AnnotationDropdown.Option.Boundary=@boundary AnnotationDropdown.Option.CalledAfter=@calledAfter AnnotationDropdown.Option.Constant=@constant +AnnotationDropdown.Option.Description=@description AnnotationDropdown.Option.Enum=@enum AnnotationDropdown.Option.Group=@group AnnotationDropdown.Option.Move=@move diff --git a/api-editor/gui/src/app/App.tsx b/api-editor/gui/src/app/App.tsx index a09631ee8..c2fc15f6c 100644 --- a/api-editor/gui/src/app/App.tsx +++ b/api-editor/gui/src/app/App.tsx @@ -17,6 +17,7 @@ import { initializeAnnotations, persistAnnotations, selectAnnotations } from '.. import { BoundaryForm } from '../features/annotations/forms/BoundaryForm'; import { CalledAfterForm } from '../features/annotations/forms/CalledAfterForm'; import { ConstantForm } from '../features/annotations/forms/ConstantForm'; +import { DescriptionForm } from '../features/annotations/forms/DescriptionForm'; import { EnumForm } from '../features/annotations/forms/EnumForm'; import { GroupForm } from '../features/annotations/forms/GroupForm'; import { MoveForm } from '../features/annotations/forms/MoveForm'; @@ -103,6 +104,7 @@ export const App: React.FC = function () { {currentUserAction.type === 'constant' && ( )} + {currentUserAction.type === 'description' && } {currentUserAction.type === 'enum' && } {currentUserAction.type === 'group' && ( - @attribute, @boundary, @calledAfter, @constant, @enum, @group, @move, @optional, + @attribute, @boundary, @calledAfter, @constant, @description, @enum, @group, @move, @optional, @pure, @remove, @renaming, @required . diff --git a/api-editor/gui/src/features/annotatedPackageData/model/InferableAnnotation.ts b/api-editor/gui/src/features/annotatedPackageData/model/InferableAnnotation.ts index df7da18ff..4be6155b6 100644 --- a/api-editor/gui/src/features/annotatedPackageData/model/InferableAnnotation.ts +++ b/api-editor/gui/src/features/annotatedPackageData/model/InferableAnnotation.ts @@ -4,6 +4,7 @@ import { CalledAfterAnnotation, ComparisonOperator, ConstantAnnotation, + DescriptionAnnotation, DefaultType, DefaultValue, EnumAnnotation, @@ -12,7 +13,6 @@ import { MoveAnnotation, OptionalAnnotation, RenameAnnotation, - DescriptionAnnotation, } from '../../annotations/annotationSlice'; const dataPathPrefix = 'com.larsreimann.api_editor.model.'; @@ -93,6 +93,15 @@ export class InferableConstantAnnotation extends InferableAnnotation { } } +export class InferableDescriptionAnnotation extends InferableAnnotation { + readonly newDescription: string; + + constructor(descriptionAnnotation: DescriptionAnnotation) { + super(dataPathPrefix + 'DescriptionAnnotation'); + this.newDescription = descriptionAnnotation.newDescription; + } +} + export class InferableGroupAnnotation extends InferableAnnotation { readonly groupName: string; readonly parameters: string[]; @@ -160,11 +169,3 @@ export class InferableRemoveAnnotation extends InferableAnnotation { } } -export class InferableDescriptionAnnotation extends InferableAnnotation { - readonly newDescription: string; - - constructor(descriptionAnnotation: DescriptionAnnotation) { - super(dataPathPrefix + 'DescriptionAnnotation'); - this.newDescription = descriptionAnnotation.newDescription; - } -} diff --git a/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx b/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx index a0e5c2820..609aeba75 100644 --- a/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationDropdown.tsx @@ -21,6 +21,7 @@ interface AnnotationDropdownProps { showBoundary?: boolean; showCalledAfter?: boolean; showConstant?: boolean; + showDescription?: boolean; showEnum?: boolean; showGroup?: boolean; showMove?: boolean; @@ -29,7 +30,6 @@ interface AnnotationDropdownProps { showRename?: boolean; showRequired?: boolean; showRemove?: boolean; - showDescription?: boolean; target: string; } @@ -38,6 +38,7 @@ export const AnnotationDropdown: React.FC = function ({ showBoundary = false, showCalledAfter = false, showConstant = false, + showDescription = false, showGroup = false, showEnum = false, showMove = false, @@ -46,7 +47,6 @@ export const AnnotationDropdown: React.FC = function ({ showRename = false, showRequired = false, showRemove = false, - showDescription = false, target, }) { const dispatch = useAppDispatch(); @@ -82,6 +82,9 @@ export const AnnotationDropdown: React.FC = function ({ {showConstant && ( dispatch(showConstantAnnotationForm(target))}>@constant )} + {showDescription && ( + dispatch(showDescriptionAnnotationForm(target))}>@description + )} {showEnum && dispatch(showEnumAnnotationForm(target))}>@enum} {showGroup && ( = function ({ dispatch(showRenameAnnotationForm(target))}>@rename )} {showRequired && dispatch(addRequired({ target }))}>@required} - {showRename && ( - dispatch(showDescriptionAnnotationForm(target))}>@description - )} diff --git a/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx b/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx index e3729329d..3c5ef152e 100644 --- a/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx @@ -27,6 +27,7 @@ export const AnnotationImportDialog: React.FC = function () { boundaries: {}, constants: {}, calledAfters: {}, + descriptions: {}, enums: {}, groups: {}, moves: {}, @@ -35,7 +36,6 @@ export const AnnotationImportDialog: React.FC = function () { renamings: {}, requireds: {}, removes: {}, - descriptions: {}, }); const dispatch = useAppDispatch(); diff --git a/api-editor/gui/src/features/annotations/AnnotationView.tsx b/api-editor/gui/src/features/annotations/AnnotationView.tsx index e8906e63e..e58e93261 100644 --- a/api-editor/gui/src/features/annotations/AnnotationView.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationView.tsx @@ -11,6 +11,7 @@ import { removeBoundary, removeCalledAfter, removeConstant, + removeDescription, removeEnum, removeGroup, removeMove, @@ -19,11 +20,11 @@ import { removeRenaming, removeRequired, removeRemove, - removeDescription, selectAttribute, selectBoundary, selectCalledAfters, selectConstant, + selectDescription, selectEnum, selectGroups, selectMove, @@ -32,18 +33,17 @@ import { selectRenaming, selectRequired, selectRemove, - selectDescription, } from './annotationSlice'; import { showAttributeAnnotationForm, showBoundaryAnnotationForm, showConstantAnnotationForm, + showDescriptionAnnotationForm, showEnumAnnotationForm, showGroupAnnotationForm, showMoveAnnotationForm, showOptionalAnnotationForm, showRenameAnnotationForm, - showDescriptionAnnotationForm, } from '../ui/uiSlice'; interface AnnotationViewProps { @@ -57,6 +57,7 @@ export const AnnotationView: React.FC = function ({ target const boundaryAnnotation = useAppSelector(selectBoundary(target)); const calledAfterAnnotation = useAppSelector(selectCalledAfters(target)); const constantAnnotation = useAppSelector(selectConstant(target)); + const descriptionAnnotation = useAppSelector(selectDescription(target)); const enumAnnotation = useAppSelector(selectEnum(target)); const groupAnnotations = useAppSelector(selectGroups(target)); const moveAnnotation = useAppSelector(selectMove(target)); @@ -65,13 +66,13 @@ export const AnnotationView: React.FC = function ({ target const removeAnnotation = useAppSelector(selectRemove(target)); const renameAnnotation = useAppSelector(selectRenaming(target)); const requiredAnnotation = useAppSelector(selectRequired(target)); - const descriptionAnnotation = useAppSelector(selectDescription(target)); if ( !attributeAnnotation && !boundaryAnnotation && !calledAfterAnnotation && !constantAnnotation && + !descriptionAnnotation && !enumAnnotation && !groupAnnotations && !moveAnnotation && @@ -79,7 +80,6 @@ export const AnnotationView: React.FC = function ({ target !pureAnnotation && !removeAnnotation && !renameAnnotation && - !descriptionAnnotation && !requiredAnnotation ) { // eslint-disable-next-line react/jsx-no-useless-fragment @@ -120,6 +120,14 @@ export const AnnotationView: React.FC = function ({ target onDelete={() => dispatch(removeConstant(target))} /> )} + {descriptionAnnotation && ( + dispatch(showDescriptionAnnotationForm(target))} + onDelete={() => dispatch(removeDescription(target))} + /> + )} {enumAnnotation && ( = function ({ target /> )} {requiredAnnotation && dispatch(removeRequired(target))} />} - {descriptionAnnotation && ( - dispatch(showDescriptionAnnotationForm(target))} - onDelete={() => dispatch(removeDescription(target))} - /> - )} ); }; diff --git a/api-editor/gui/src/features/annotations/annotationSlice.ts b/api-editor/gui/src/features/annotations/annotationSlice.ts index e5edeb6dd..5bac7baeb 100644 --- a/api-editor/gui/src/features/annotations/annotationSlice.ts +++ b/api-editor/gui/src/features/annotations/annotationSlice.ts @@ -266,7 +266,7 @@ export interface DescriptionAnnotation { readonly target: string; /** - * New name for the declaration. + * Description for the declaration. */ readonly newDescription: string; } diff --git a/api-editor/gui/src/features/annotations/forms/DescriptionForm.tsx b/api-editor/gui/src/features/annotations/forms/DescriptionForm.tsx index 595702564..4a63a4446 100644 --- a/api-editor/gui/src/features/annotations/forms/DescriptionForm.tsx +++ b/api-editor/gui/src/features/annotations/forms/DescriptionForm.tsx @@ -1,8 +1,7 @@ -import { FormControl, FormErrorIcon, FormErrorMessage, FormLabel, Input } from '@chakra-ui/react'; +import {FormControl, FormErrorIcon, FormErrorMessage, FormLabel, Textarea} from '@chakra-ui/react'; import React, { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { useAppDispatch, useAppSelector } from '../../../app/hooks'; -import { pythonIdentifierPattern } from '../../../common/validation'; import { PythonDeclaration } from '../../packageData/model/PythonDeclaration'; import { selectDescription, upsertDescription } from '../annotationSlice'; import { AnnotationForm } from './AnnotationForm'; @@ -46,7 +45,7 @@ export const DescriptionForm: React.FC = function ({ targe useEffect(() => { reset({ - newDescription: prevNewDescription || oldDescription, + newDescription: prevNewDescription ?? oldDescription, }); }, [reset, prevNewDescription, oldDescription]); @@ -75,12 +74,9 @@ export const DescriptionForm: React.FC = function ({ targe onCancel={onCancel} > - New description for "{oldDescription}": - Update description: +