Repository: chaoss/grimoirelab-sortinghat (language: python)

Issue:
Improve organization selector
The organization selector on the affiliation modal is quite small and the longer names are not readable.

![image](https://github.com/chaoss/grimoirelab-sortinghat/assets/26812577/4edd4475-9acf-437e-b977-e7154ef5248f)

It would also be helpful to have the option to create the organization on the selector if it doesn't exist yet.


Discussion / hints:

Fix patch:
diff --git a/releases/unreleased/improve-organization-selector.yml b/releases/unreleased/improve-organization-selector.yml
new file mode 100644
index 00000000..e94a6ff6
--- /dev/null
+++ b/releases/unreleased/improve-organization-selector.yml
@@ -0,0 +1,11 @@
+---
+title: Improved organization selector
+category: added
+author: Eva Millán <evamillan@bitergia.com>
+issue: 820
+notes: >
+  The organization selector that is used to affiliate individuals
+  now has the option to create an organization if the desired one
+  is not found.
+  Its size is also increased to improve the readability of longer
+  names.
diff --git a/ui/src/components/EnrollModal.stories.js b/ui/src/components/EnrollModal.stories.js
index 545ff1ab..a91a5bb0 100644
--- a/ui/src/components/EnrollModal.stories.js
+++ b/ui/src/components/EnrollModal.stories.js
@@ -15,6 +15,7 @@ const template = `
       :organization='organization'
       :enroll='enroll'
       :title='title'
+      :add-organization='addOrganization'
       uuid="123"
     />
   </div>
@@ -32,6 +33,17 @@ export const Default = () => ({
     enroll() {
       return true;
     },
+    addOrganization(name) {
+      return {
+        data: {
+          addOrganization: {
+            organization: {
+              name: name
+            }
+          }
+        }
+      }
+    }
   },
 });
 
@@ -47,5 +59,8 @@ export const WithOrganization = () => ({
     enroll() {
       return true;
     },
+    addOrganization() {
+      return;
+    }
   },
 });
diff --git a/ui/src/components/EnrollModal.vue b/ui/src/components/EnrollModal.vue
index 433133c7..0551365d 100644
--- a/ui/src/components/EnrollModal.vue
+++ b/ui/src/components/EnrollModal.vue
@@ -1,5 +1,5 @@
 <template>
-  <v-dialog v-model="isOpen" max-width="550px">
+  <v-dialog v-model="isOpen" :max-width="organization ? '550px' : '700px'">
     <v-card class="pa-3">
       <v-card-title class="headline">{{ title }}</v-card-title>
       <v-card-text>
@@ -10,16 +10,18 @@
           Enrollment dates (optional)
         </h6>
         <v-row>
-          <v-col v-if="!organization" cols="4">
+          <v-col v-if="!organization" cols="6">
             <organization-selector
               v-model="form.organization"
+              :add-organization="addOrganization"
               :fetch-organizations="fetchOrganizations"
+              @error="($event) => (errorMessage = $event)"
             />
           </v-col>
-          <v-col :cols="organization ? 6 : 4">
+          <v-col :cols="organization ? 6 : 3">
             <date-input v-model="form.dateFrom" label="Date from" outlined />
           </v-col>
-          <v-col :cols="organization ? 6 : 4">
+          <v-col :cols="organization ? 6 : 3">
             <date-input v-model="form.dateTo" label="Date to" outlined />
           </v-col>
         </v-row>
@@ -76,6 +78,11 @@ export default {
       type: String,
       required: false,
     },
+    addOrganization: {
+      type: Function,
+      required: false,
+      default: () => {},
+    },
     fetchOrganizations: {
       type: Function,
       required: false,
diff --git a/ui/src/components/IndividualsTable.vue b/ui/src/components/IndividualsTable.vue
index d55faa21..32f542f6 100644
--- a/ui/src/components/IndividualsTable.vue
+++ b/ui/src/components/IndividualsTable.vue
@@ -237,6 +237,7 @@
       :enroll="enroll"
       :get-countries="getCountries"
       :fetch-organizations="fetchOrganizations"
+      :add-organization="addOrganization"
       @updateTable="queryIndividuals"
       @updateOrganizations="$emit('updateOrganizations')"
     />
@@ -249,6 +250,7 @@
       :uuid="enrollmentModal.uuid"
       :enroll="enrollIndividual"
       :fetch-organizations="fetchOrganizations"
+      :add-organization="addOrganization"
     />
 
     <team-enroll-modal
diff --git a/ui/src/components/OrganizationSelector.stories.js b/ui/src/components/OrganizationSelector.stories.js
index 4627bb66..214344e6 100644
--- a/ui/src/components/OrganizationSelector.stories.js
+++ b/ui/src/components/OrganizationSelector.stories.js
@@ -10,6 +10,7 @@ const template = `
     <organization-selector
       v-model="model"
       :fetch-organizations="fetchOrganizations"
+      :add-organization="addOrganization"
     />
   </div>
 `;
@@ -52,6 +53,17 @@ export const Default = () => ({
       }
       return results;
     },
+    addOrganization(name) {
+      return {
+        data: {
+          addOrganization: {
+            organization: {
+              name: name
+            }
+          }
+        }
+      }
+    }
   },
 });
 
@@ -72,5 +84,16 @@ export const SelectedOrganization = () => ({
       }
       return results;
     },
+    addOrganization(name) {
+      return {
+        data: {
+          addOrganization: {
+            organization: {
+              name: name
+            }
+          }
+        }
+      }
+    }
   },
 });
diff --git a/ui/src/components/OrganizationSelector.vue b/ui/src/components/OrganizationSelector.vue
index f1bdfdb7..30698b67 100644
--- a/ui/src/components/OrganizationSelector.vue
+++ b/ui/src/components/OrganizationSelector.vue
@@ -6,12 +6,23 @@
     :label="label"
     item-text="name"
     item-value="name"
+    :no-data-text="`No matches for &quot;${search}&quot;`"
+    :hide-no-data="!search"
     clearable
     dense
-    hide-selected
-    hide-no-data
     outlined
-  ></v-autocomplete>
+  >
+    <template v-slot:append-item>
+      <v-list-item v-if="appendContent" @click="createOrganization">
+        <v-list-item-content>
+          <v-list-item-title>
+            <span class="font-weight-regular">Create</span>
+            {{ search }}
+          </v-list-item-title>
+        </v-list-item-content>
+      </v-list-item>
+    </template>
+  </v-autocomplete>
 </template>
 
 <script>
@@ -29,6 +40,10 @@ export default {
       type: String,
       required: false,
     },
+    addOrganization: {
+      type: Function,
+      required: true,
+    },
     fetchOrganizations: {
       type: Function,
       required: true,
@@ -41,11 +56,35 @@ export default {
   },
   methods: {
     async getSelectorItems(value) {
-      const response = await this.fetchOrganizations(1, 10, { term: value });
+      const filters = value ? { term: value } : {};
+      const response = await this.fetchOrganizations(1, 10, filters);
       if (response) {
         this.organizations = response.entities;
       }
     },
+    async createOrganization() {
+      try {
+        const response = await this.addOrganization(this.search);
+        this.organizations.splice(0, 0, {
+          name: response.data.addOrganization.organization.name,
+        });
+        this.inputValue = this.search;
+        this.$logger.debug(`Created organization "${this.search}"`);
+      } catch (error) {
+        this.$logger.error(`Error creating organization: ${error}`);
+        this.$emit("error", this.$getErrorMessage(error));
+      }
+    },
+  },
+  computed: {
+    appendContent() {
+      return (
+        this.search &&
+        !this.organizations.some(
+          (org) => org.name.toLowerCase() === this.search.toLowerCase()
+        )
+      );
+    },
   },
   watch: {
     search(value) {
@@ -61,9 +100,7 @@ export default {
     },
   },
   mounted() {
-    if (this.value) {
-      this.getSelectorItems(this.value);
-    }
+    this.getSelectorItems(this.value);
   },
 };
 </script>
diff --git a/ui/src/components/ProfileModal.stories.js b/ui/src/components/ProfileModal.stories.js
index f66327cc..3c550a68 100644
--- a/ui/src/components/ProfileModal.stories.js
+++ b/ui/src/components/ProfileModal.stories.js
@@ -17,6 +17,7 @@ const ProfileModalTemplate = `
       :enroll="mockFunction"
       :fetch-organizations="mockFunction"
       :get-countries="getCountries.bind(this)"
+      :add-organization="mockFunction"
     />
   </div>
 `;
diff --git a/ui/src/components/ProfileModal.vue b/ui/src/components/ProfileModal.vue
index d753e213..3228d348 100644
--- a/ui/src/components/ProfileModal.vue
+++ b/ui/src/components/ProfileModal.vue
@@ -88,6 +88,7 @@
               <organization-selector
                 v-model="enrollmentsForm[index].organization"
                 :fetch-organizations="fetchOrganizations"
+                :add-organization="addOrganization"
               />
             </v-col>
             <v-col cols="3">
@@ -178,6 +179,10 @@ export default {
       type: Function,
       required: true,
     },
+    addOrganization: {
+      type: Function,
+      required: true,
+    },
   },
   data() {
     return {
diff --git a/ui/src/mixins/enroll.js b/ui/src/mixins/enroll.js
index f90de95f..f56d58fd 100644
--- a/ui/src/mixins/enroll.js
+++ b/ui/src/mixins/enroll.js
@@ -1,4 +1,5 @@
 import { formatIndividuals } from "../utils/actions";
+import { addOrganization } from "../apollo/mutations";
 
 const enrollMixin = {
   data() {
@@ -85,6 +86,10 @@ const enrollMixin = {
         });
       }
     },
+    async addOrganization(name) {
+      const response = await addOrganization(this.$apollo, name);
+      return response;
+    },
   },
 };
 
diff --git a/ui/src/views/Individual.vue b/ui/src/views/Individual.vue
index 89c08c02..c8ecda97 100644
--- a/ui/src/views/Individual.vue
+++ b/ui/src/views/Individual.vue
@@ -330,6 +330,7 @@
       :uuid="mk"
       :enroll="enrollIndividual"
       :fetch-organizations="fetchOrganizations"
+      :add-organization="addOrganization"
     />
 
     <team-enroll-modal


Test patch:
diff --git a/ui/tests/unit/__snapshots__/mutations.spec.js.snap b/ui/tests/unit/__snapshots__/mutations.spec.js.snap
index eb882660..8ecc9f11 100644
--- a/ui/tests/unit/__snapshots__/mutations.spec.js.snap
+++ b/ui/tests/unit/__snapshots__/mutations.spec.js.snap
@@ -261,6 +261,7 @@ exports[`IndividualsTable Mock query for deleteIdentity 1`] = `
    
   <profile-modal-stub
     addidentity="[Function]"
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     getcountries="[Function]"
@@ -268,6 +269,7 @@ exports[`IndividualsTable Mock query for deleteIdentity 1`] = `
   />
    
   <enroll-modal-stub
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     text=""
@@ -559,6 +561,7 @@ exports[`IndividualsTable Mock query for merge 1`] = `
    
   <profile-modal-stub
     addidentity="[Function]"
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     getcountries="[Function]"
@@ -566,6 +569,7 @@ exports[`IndividualsTable Mock query for merge 1`] = `
   />
    
   <enroll-modal-stub
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     text=""
@@ -857,6 +861,7 @@ exports[`IndividualsTable Mock query for moveIdentity 1`] = `
    
   <profile-modal-stub
     addidentity="[Function]"
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     getcountries="[Function]"
@@ -864,6 +869,7 @@ exports[`IndividualsTable Mock query for moveIdentity 1`] = `
   />
    
   <enroll-modal-stub
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     text=""
@@ -1155,6 +1161,7 @@ exports[`IndividualsTable Mock query for unmerge 1`] = `
    
   <profile-modal-stub
     addidentity="[Function]"
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     getcountries="[Function]"
@@ -1162,6 +1169,7 @@ exports[`IndividualsTable Mock query for unmerge 1`] = `
   />
    
   <enroll-modal-stub
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     text=""
@@ -1453,6 +1461,7 @@ exports[`IndividualsTable Mock query for updateEnrollment 1`] = `
    
   <profile-modal-stub
     addidentity="[Function]"
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     getcountries="[Function]"
@@ -1460,6 +1469,7 @@ exports[`IndividualsTable Mock query for updateEnrollment 1`] = `
   />
    
   <enroll-modal-stub
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     text=""
@@ -1751,6 +1761,7 @@ exports[`IndividualsTable Mock query for withdraw 1`] = `
    
   <profile-modal-stub
     addidentity="[Function]"
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     getcountries="[Function]"
@@ -1758,6 +1769,7 @@ exports[`IndividualsTable Mock query for withdraw 1`] = `
   />
    
   <enroll-modal-stub
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     text=""
diff --git a/ui/tests/unit/__snapshots__/queries.spec.js.snap b/ui/tests/unit/__snapshots__/queries.spec.js.snap
index 13c223d6..2a438cd2 100644
--- a/ui/tests/unit/__snapshots__/queries.spec.js.snap
+++ b/ui/tests/unit/__snapshots__/queries.spec.js.snap
@@ -268,6 +268,7 @@ exports[`IndividualsTable Mock query for getCountries 1`] = `
    
   <profile-modal-stub
     addidentity="[Function]"
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     getcountries="[Function]"
@@ -275,6 +276,7 @@ exports[`IndividualsTable Mock query for getCountries 1`] = `
   />
    
   <enroll-modal-stub
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     text=""
@@ -566,6 +568,7 @@ exports[`IndividualsTable Mock query for getPaginatedIndividuals 1`] = `
    
   <profile-modal-stub
     addidentity="[Function]"
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     getcountries="[Function]"
@@ -573,6 +576,7 @@ exports[`IndividualsTable Mock query for getPaginatedIndividuals 1`] = `
   />
    
   <enroll-modal-stub
+    addorganization="[Function]"
     enroll="[Function]"
     fetchorganizations="[Function]"
     text=""