From 5d193823bd7b4fc6234dc2e7583cbca9f5ec25bc Mon Sep 17 00:00:00 2001
From: Himanshu Kapoor <hkapoor@gitlab.com>
Date: Mon, 5 Feb 2024 15:45:05 +0800
Subject: [PATCH] Add rich text editor in vulnerabilities

Allow using rich text editor when creating/editing vulnerabilities and solutions.

Changelog: added
---
 .../new_vulnerability/section_name.vue        | 41 +++++--------
 .../new_vulnerability/section_solution.vue    | 40 +++++--------
 .../new_vulnerability/section_name_spec.js    | 58 +++++++++++++------
 .../section_solution_spec.js                  | 16 +++--
 4 files changed, 82 insertions(+), 73 deletions(-)

diff --git a/ee/app/assets/javascripts/vulnerabilities/components/new_vulnerability/section_name.vue b/ee/app/assets/javascripts/vulnerabilities/components/new_vulnerability/section_name.vue
index fc973c78036f0..f7effa5a30012 100644
--- a/ee/app/assets/javascripts/vulnerabilities/components/new_vulnerability/section_name.vue
+++ b/ee/app/assets/javascripts/vulnerabilities/components/new_vulnerability/section_name.vue
@@ -1,15 +1,14 @@
 <script>
-import { GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
 import { s__, __ } from '~/locale';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
 import * as i18n from './i18n';
 
 export default {
   components: {
     GlFormGroup,
     GlFormInput,
-    GlFormTextarea,
-    MarkdownField,
+    MarkdownEditor,
   },
   inject: ['markdownDocsPath', 'markdownPreviewPath'],
   props: {
@@ -24,6 +23,12 @@ export default {
       isSubmitting: false,
       vulnerabilityName: '',
       vulnerabilityDesc: '',
+      formFieldProps: {
+        id: 'form-vulnerability-desc',
+        name: 'form-vulnerability-desc',
+        'aria-label': this.$options.i18n.vulnerabilityDesc.description,
+        placeholder: this.$options.i18n.vulnerabilityDesc.description,
+      },
     };
   },
   methods: {
@@ -49,7 +54,6 @@ export default {
       ),
     },
   },
-  restrictedToolBarItems: ['attach-file'],
 };
 </script>
 <template>
@@ -75,28 +79,15 @@ export default {
       label-for="form-vulnerability-desc"
     >
       <div class="gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base">
-        <markdown-field
-          ref="markdownField"
-          :can-attach-file="false"
-          :add-spacing-classes="false"
-          :markdown-preview-path="markdownPreviewPath"
+        <markdown-editor
+          v-model="vulnerabilityDesc"
+          :disable-attachments="true"
+          :render-markdown-path="markdownPreviewPath"
           :markdown-docs-path="markdownDocsPath"
-          :textarea-value="vulnerabilityDesc"
           :is-submitting="isSubmitting"
-          :restricted-tool-bar-items="$options.restrictedToolBarItems"
-        >
-          <template #textarea>
-            <gl-form-textarea
-              id="form-vulnerability-desc"
-              v-model.trim="vulnerabilityDesc"
-              rows="8"
-              class="gl-shadow-none! gl-py-4! gl-h-auto!"
-              :aria-label="$options.i18n.vulnerabilityDesc.description"
-              :placeholder="$options.i18n.vulnerabilityDesc.description"
-              @input="emitChanges"
-            />
-          </template>
-        </markdown-field>
+          :form-field-props="formFieldProps"
+          @input="emitChanges"
+        />
       </div>
     </gl-form-group>
   </section>
diff --git a/ee/app/assets/javascripts/vulnerabilities/components/new_vulnerability/section_solution.vue b/ee/app/assets/javascripts/vulnerabilities/components/new_vulnerability/section_solution.vue
index 015b3f46744f7..bb866787bc814 100644
--- a/ee/app/assets/javascripts/vulnerabilities/components/new_vulnerability/section_solution.vue
+++ b/ee/app/assets/javascripts/vulnerabilities/components/new_vulnerability/section_solution.vue
@@ -1,12 +1,10 @@
 <script>
-import { GlFormTextarea } from '@gitlab/ui';
 import { s__, __ } from '~/locale';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
 
 export default {
   components: {
-    GlFormTextarea,
-    MarkdownField,
+    MarkdownEditor,
   },
   inject: ['markdownDocsPath', 'markdownPreviewPath'],
   props: {
@@ -19,6 +17,12 @@ export default {
   data() {
     return {
       solution: '',
+      formFieldProps: {
+        id: 'form-solution',
+        name: 'form-solution',
+        'aria-label': this.$options.i18n.description,
+        placeholder: this.$options.i18n.description,
+      },
     };
   },
   methods: {
@@ -43,29 +47,15 @@ export default {
       </h3>
     </header>
     <div class="gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base">
-      <markdown-field
-        ref="markdownField"
-        class="gl-px-4"
-        :can-attach-file="false"
-        :add-spacing-classes="false"
-        :markdown-preview-path="markdownPreviewPath"
+      <markdown-editor
+        v-model="solution"
+        :disable-attachments="true"
+        :render-markdown-path="markdownPreviewPath"
         :markdown-docs-path="markdownDocsPath"
-        :restricted-tool-bar-items="$options.restrictedToolBarItems"
-        :textarea-value="solution"
         :is-submitting="isSubmitting"
-      >
-        <template #textarea>
-          <gl-form-textarea
-            id="form-solution"
-            v-model="solution"
-            rows="8"
-            class="gl-shadow-none! gl-px-0! gl-py-4! gl-h-auto!"
-            :aria-label="$options.i18n.description"
-            :placeholder="$options.i18n.description"
-            @change="emitChanges"
-          />
-        </template>
-      </markdown-field>
+        :form-field-props="formFieldProps"
+        @input="emitChanges"
+      />
     </div>
   </section>
 </template>
diff --git a/ee/spec/frontend/vulnerabilities/new_vulnerability/section_name_spec.js b/ee/spec/frontend/vulnerabilities/new_vulnerability/section_name_spec.js
index 2bd8ee2da38de..be7f43cebf5ad 100644
--- a/ee/spec/frontend/vulnerabilities/new_vulnerability/section_name_spec.js
+++ b/ee/spec/frontend/vulnerabilities/new_vulnerability/section_name_spec.js
@@ -1,9 +1,11 @@
 import { nextTick } from 'vue';
-import { GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
 import { mountExtended } from 'helpers/vue_test_utils_helper';
 import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
 import SectionName from 'ee/vulnerabilities/components/new_vulnerability/section_name.vue';
 import { ERROR_NAME } from 'ee/vulnerabilities/components/new_vulnerability/i18n';
+import { stubComponent } from 'helpers/stub_component';
 
 describe('New vulnerability - Section Name', () => {
   const markdownDocsPath = '/path/to/markdown/docs';
@@ -13,26 +15,24 @@ describe('New vulnerability - Section Name', () => {
 
   const findFormGroup = (index) => wrapper.findAllComponents(GlFormGroup).at(index);
 
-  const createWrapper = () => {
-    return mountExtended(SectionName, {
+  const createWrapper = ({ stubs = {} } = {}) => {
+    wrapper = mountExtended(SectionName, {
       provide: {
         markdownDocsPath,
         markdownPreviewPath,
       },
+      stubs,
     });
   };
 
-  beforeEach(() => {
-    wrapper = createWrapper();
-  });
-
   it('creates markdown editor with correct props', () => {
+    createWrapper();
+
     expect(wrapper.findComponent(MarkdownField).props()).toMatchObject({
       markdownDocsPath,
       markdownPreviewPath,
       textareaValue: '',
       canAttachFile: false,
-      addSpacingClasses: false,
       isSubmitting: false,
       restrictedToolBarItems: ['attach-file'],
     });
@@ -43,6 +43,8 @@ describe('New vulnerability - Section Name', () => {
     ${'Name'}        | ${'Vulnerability name or type. Ex: Cross-site scripting'}
     ${'Description'} | ${''}
   `('displays the input with the correct label: $labelText', ({ labelText, description }) => {
+    createWrapper();
+
     expect(wrapper.findByLabelText(labelText).exists()).toBe(true);
 
     if (description) {
@@ -50,25 +52,41 @@ describe('New vulnerability - Section Name', () => {
     }
   });
 
-  it.each`
-    field            | component         | fieldKey               | fieldValue
-    ${'Name'}        | ${GlFormInput}    | ${'vulnerabilityName'} | ${'CVE 2021'}
-    ${'Description'} | ${GlFormTextarea} | ${'vulnerabilityDesc'} | ${'Password leak'}
-  `('emits the changes: $field', ({ component, fieldKey, fieldValue }) => {
-    wrapper.findComponent(component).setValue(fieldValue);
-    wrapper.findComponent(component).vm.$emit('change', fieldValue);
+  it('emits the changes: Name', () => {
+    const fieldValue = 'CVE 2021';
+    createWrapper({ stubs: { MarkdownEditor: stubComponent(MarkdownEditor) } });
+
+    wrapper.findComponent(GlFormInput).setValue(fieldValue);
+    wrapper.findComponent(GlFormInput).vm.$emit('change', fieldValue);
+
     expect(wrapper.emitted('change')[0][0]).toEqual({
-      vulnerabilityName: '',
+      vulnerabilityName: fieldValue,
       vulnerabilityDesc: '',
-      [fieldKey]: fieldValue,
+    });
+  });
+
+  it('emits the changes: Description', () => {
+    const fieldValue = 'Password leak';
+    createWrapper();
+
+    wrapper.findComponent(MarkdownField).find('textarea').setValue(fieldValue);
+    wrapper.findComponent(MarkdownEditor).vm.$emit('input', fieldValue);
+
+    expect(wrapper.emitted('change')[1][0]).toEqual({
+      vulnerabilityName: '',
+      vulnerabilityDesc: fieldValue,
     });
   });
 
   it('does not display invalid state by default', () => {
+    createWrapper();
+
     expect(findFormGroup(0).attributes('aria-invalid')).toBeUndefined();
   });
 
   it('handles form validation', async () => {
+    createWrapper();
+
     wrapper.setProps({
       validationState: {
         name: false,
@@ -80,4 +98,10 @@ describe('New vulnerability - Section Name', () => {
     expect(findFormGroup(0).attributes('aria-invalid')).toBe('true');
     expect(findFormGroup(0).text()).toContain(ERROR_NAME);
   });
+
+  it('allows switching to rich text editor', () => {
+    createWrapper();
+
+    expect(wrapper.text()).toContain('Switch to rich text editing');
+  });
 });
diff --git a/ee/spec/frontend/vulnerabilities/new_vulnerability/section_solution_spec.js b/ee/spec/frontend/vulnerabilities/new_vulnerability/section_solution_spec.js
index 48e0706336a45..749fd4f6dfca6 100644
--- a/ee/spec/frontend/vulnerabilities/new_vulnerability/section_solution_spec.js
+++ b/ee/spec/frontend/vulnerabilities/new_vulnerability/section_solution_spec.js
@@ -1,6 +1,6 @@
-import { GlFormTextarea } from '@gitlab/ui';
 import { mountExtended } from 'helpers/vue_test_utils_helper';
 import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
 import SectionSolution from 'ee/vulnerabilities/components/new_vulnerability/section_solution.vue';
 
 describe('New vulnerability - Section Solution', () => {
@@ -28,7 +28,6 @@ describe('New vulnerability - Section Solution', () => {
       markdownPreviewPath,
       textareaValue: '',
       canAttachFile: false,
-      addSpacingClasses: false,
       isSubmitting: false,
       restrictedToolBarItems: ['attach-file'],
     });
@@ -36,9 +35,14 @@ describe('New vulnerability - Section Solution', () => {
 
   it('emits the changes for the markdown field', () => {
     const solution = 'The solution of the vulnerability';
-    const component = wrapper.findComponent(GlFormTextarea);
-    component.vm.$emit('input', solution);
-    component.vm.$emit('change', solution);
-    expect(wrapper.emitted('change')[0][0]).toEqual({ solution });
+
+    wrapper.findComponent(MarkdownField).find('textarea').setValue(solution);
+    wrapper.findComponent(MarkdownEditor).vm.$emit('input', solution);
+
+    expect(wrapper.emitted('change')[1][0]).toEqual({ solution });
+  });
+
+  it('allows switching to rich text editor', () => {
+    expect(wrapper.text()).toContain('Switch to rich text editing');
   });
 });
-- 
GitLab