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

Issue:
Recommendation by individual
The user should be able to get recommendations for one individual. This can be done by clicking on the drop down menu of each individual and on the individual's profile.


Discussion / hints:

Fix patch:
diff --git a/releases/unreleased/recommendation-by-individual.yml b/releases/unreleased/recommendation-by-individual.yml
new file mode 100644
index 00000000..d0cfb828
--- /dev/null
+++ b/releases/unreleased/recommendation-by-individual.yml
@@ -0,0 +1,11 @@
+---
+title: Recommendations by individual
+category: added
+author: Eva Millan <evamillan@bitergia.com>
+issue: 779
+notes: >
+  Users can generate matching recommendations for a
+  specific individual by clicking on the drop down
+  menu on each individual or on the individual's
+  profile.
+
diff --git a/ui/src/apollo/mutations.js b/ui/src/apollo/mutations.js
index 744d3220..3f7ef569 100644
--- a/ui/src/apollo/mutations.js
+++ b/ui/src/apollo/mutations.js
@@ -305,8 +305,16 @@ const MANAGE_MERGE_RECOMMENDATION = gql`
 `;
 
 const RECOMMEND_MATCHES = gql`
-  mutation recommendMatches($criteria: [String], $exclude: Boolean) {
-    recommendMatches(criteria: $criteria, exclude: $exclude) {
+  mutation recommendMatches(
+    $criteria: [String]
+    $exclude: Boolean
+    $sourceUuids: [String]
+  ) {
+    recommendMatches(
+      criteria: $criteria
+      exclude: $exclude
+      sourceUuids: $sourceUuids
+    ) {
       jobId
     }
   }
@@ -596,12 +604,13 @@ const manageMergeRecommendation = (apollo, id, apply) => {
   });
 };
 
-const recommendMatches = (apollo, criteria, exclude) => {
+const recommendMatches = (apollo, criteria, exclude, sourceUuids) => {
   return apollo.mutate({
     mutation: RECOMMEND_MATCHES,
     variables: {
       criteria: criteria,
       exclude: exclude,
+      sourceUuids: sourceUuids,
     },
   });
 };
diff --git a/ui/src/components/IndividualEntry.vue b/ui/src/components/IndividualEntry.vue
index 92b83ebc..23ae6ffd 100644
--- a/ui/src/components/IndividualEntry.vue
+++ b/ui/src/components/IndividualEntry.vue
@@ -126,6 +126,9 @@
           <v-list-item @click="$emit('saveIndividuals')">
             <v-list-item-title>Save in workspace</v-list-item-title>
           </v-list-item>
+          <v-list-item @click="$emit('openMatchesModal', $event)">
+            <v-list-item-title> Recommend matches </v-list-item-title>
+          </v-list-item>
           <v-list-item @click="$emit('delete', $event)" :disabled="isLocked">
             <v-list-item-title>Delete individual</v-list-item-title>
           </v-list-item>
diff --git a/ui/src/components/IndividualsTable.stories.js b/ui/src/components/IndividualsTable.stories.js
index 4e507e07..9154b06d 100644
--- a/ui/src/components/IndividualsTable.stories.js
+++ b/ui/src/components/IndividualsTable.stories.js
@@ -24,6 +24,7 @@ const IndividualsTableTemplate = `
     :outlined="outlined"
     :hide-header="hideHeader"
     :is-expandable="isExpandable"
+    :recommend-matches="recommendMatches"
   />
 `;
 
@@ -387,6 +388,15 @@ export const Default = () => ({
     getCountries() {
       return this.countries;
     },
+    recommendMatches() {
+      return {
+        data: {
+          recommendMatches: {
+            jobId: "b65d2170-a560-4b20-954e-fc8c9f5afdd4"
+          }
+        }
+      };
+    },
   },
   provide: () => ({
     getRecommendations: () => {},
@@ -445,6 +455,15 @@ export const Expandable = () => ({
     getCountries() {
       return true;
     },
+    recommendMatches() {
+      return {
+        data: {
+          recommendMatches: {
+            jobId: "b65d2170-a560-4b20-954e-fc8c9f5afdd4"
+          }
+        }
+      };
+    },
   },
   provide: () => ({
     getRecommendations: () => {},
@@ -493,6 +512,15 @@ export const Outlined = () => ({
     getCountries() {
       return true;
     },
+    recommendMatches() {
+      return {
+        data: {
+          recommendMatches: {
+            jobId: "b65d2170-a560-4b20-954e-fc8c9f5afdd4"
+          }
+        }
+      };
+    },
   },
   provide: () => ({
     getRecommendations: () => {},
@@ -541,6 +569,15 @@ export const HiddenHeader = () => ({
     getCountries() {
       return true;
     },
+    recommendMatches() {
+      return {
+        data: {
+          recommendMatches: {
+            jobId: "b65d2170-a560-4b20-954e-fc8c9f5afdd4"
+          }
+        }
+      };
+    },
   },
   provide: () => ({
     getRecommendations: () => {},
diff --git a/ui/src/components/IndividualsTable.vue b/ui/src/components/IndividualsTable.vue
index e651cce7..f3fe2a24 100644
--- a/ui/src/components/IndividualsTable.vue
+++ b/ui/src/components/IndividualsTable.vue
@@ -143,6 +143,7 @@
           @stopHighlight="$emit('stopHighlight', item)"
           @lock="handleLock(item.uuid, $event)"
           @enroll="confirmEnroll(item, $event)"
+          @openMatchesModal="openMatchesModal(item.uuid)"
         />
       </template>
       <template v-if="isExpandable" v-slot:expanded-item="{ item }">
@@ -261,6 +262,13 @@
       @updateTable="queryIndividuals"
     />
 
+    <matches-modal
+      v-if="matchesModal.open"
+      :is-open.sync="matchesModal.open"
+      :uuid="matchesModal.uuid"
+      :recommend-matches="recommendMatches"
+    />
+
     <v-card class="dragged-item" color="primary" dark>
       <v-card-subtitle>
         Moving
@@ -285,6 +293,7 @@ import Search from "./Search.vue";
 import EnrollModal from "./EnrollModal.vue";
 import TeamEnrollModal from "./TeamEnrollModal.vue";
 import Recommendations from "./Recommendations.vue";
+import MatchesModal from "./MatchesModal.vue";
 
 export default {
   name: "IndividualsTable",
@@ -296,6 +305,7 @@ export default {
     EnrollModal,
     TeamEnrollModal,
     Recommendations,
+    MatchesModal,
   },
   mixins: [enrollMixin],
   props: {
@@ -363,6 +373,10 @@ export default {
       type: Function,
       required: true,
     },
+    recommendMatches: {
+      type: Function,
+      required: true,
+    },
     hideHeader: {
       type: Boolean,
       required: false,
@@ -401,6 +415,10 @@ export default {
       allSelected: false,
       orderBy: null,
       loading: false,
+      matchesModal: {
+        open: false,
+        uuid: "",
+      },
     };
   },
   computed: {
@@ -731,6 +749,12 @@ export default {
         enrollments,
       });
     },
+    openMatchesModal(uuid) {
+      Object.assign(this.matchesModal, {
+        open: true,
+        uuid: uuid,
+      });
+    },
   },
 };
 </script>
diff --git a/ui/src/components/MatchesModal.stories.js b/ui/src/components/MatchesModal.stories.js
new file mode 100644
index 00000000..9d37cf31
--- /dev/null
+++ b/ui/src/components/MatchesModal.stories.js
@@ -0,0 +1,51 @@
+import MatchesModal from "./MatchesModal.vue";
+
+export default {
+  title: "MatchesModal",
+  excludeStories: /.*Data$/,
+};
+
+const template = `
+  <div data-app="true" class="ma-auto">
+    <v-btn color="primary" dark @click.stop="isOpen = true">
+      Open modal
+    </v-btn>
+    <matches-modal
+      :is-open.sync="isOpen"
+      :recommend-matches="recommendMatches"
+      uuid="abc123"
+    />
+  </div>
+`;
+
+export const Default = () => ({
+  components: { MatchesModal },
+  template: template,
+  data: () => ({
+    isOpen: false,
+  }),
+  methods: {
+    recommendMatches() {
+      return {
+        data: {
+          recommendMatches: {
+            jobId: "b65d2170-a560-4b20-954e-fc8c9f5afdd4"
+          }
+        }
+      }
+    }
+  },
+});
+
+export const Error = () => ({
+  components: { MatchesModal },
+  template: template,
+  data: () => ({
+    isOpen: false,
+  }),
+  methods: {
+    recommendMatches() {
+      throw new TypeError("Error creating job")
+    }
+  },
+});
diff --git a/ui/src/components/MatchesModal.vue b/ui/src/components/MatchesModal.vue
new file mode 100644
index 00000000..ee8ff4a0
--- /dev/null
+++ b/ui/src/components/MatchesModal.vue
@@ -0,0 +1,120 @@
+<template>
+  <v-dialog v-model="isOpen" max-width="500px" @click:outside="closeModal">
+    <v-card class="pa-3">
+      <v-card-title class="headline">Recommend matches</v-card-title>
+      <v-card-text v-if="jobId">
+        <v-alert type="success" text>
+          <p>
+            Enqueued job <code>{{ jobId }}</code> to recommend matches.
+          </p>
+          <router-link :to="{ name: 'Jobs' }" target="_blank">
+            View jobs
+          </router-link>
+        </v-alert>
+      </v-card-text>
+      <v-card-text v-else>
+        <p class="subtitle-2 mt-2">
+          Find possible matches based on this profile's:
+        </p>
+        <div>
+          <v-checkbox
+            v-model="form.criteria"
+            label="Name"
+            value="name"
+            dense
+            hide-details
+          ></v-checkbox>
+          <v-checkbox
+            v-model="form.criteria"
+            label="Email"
+            value="email"
+            dense
+            hide-details
+          ></v-checkbox>
+          <v-checkbox
+            v-model="form.criteria"
+            label="Username"
+            value="username"
+            dense
+          ></v-checkbox>
+        </div>
+        <v-divider></v-divider>
+        <div class="my-4">
+          <v-checkbox
+            v-model="form.exclude"
+            label="Exclude individuals in RecommenderExclusionTerm list"
+            dense
+            hide-details
+          ></v-checkbox>
+        </div>
+        <v-alert v-if="error" dense text type="error">
+          {{ error }}
+        </v-alert>
+      </v-card-text>
+      <v-card-actions v-if="jobId">
+        <v-spacer></v-spacer>
+        <v-btn text color="primary" @click="closeModal"> OK </v-btn>
+      </v-card-actions>
+      <v-card-actions v-else>
+        <v-spacer></v-spacer>
+        <v-btn text @click="closeModal"> Cancel </v-btn>
+        <v-btn color="primary" id="confirm" depressed @click.stop="createJob">
+          Confirm
+        </v-btn>
+      </v-card-actions>
+    </v-card>
+  </v-dialog>
+</template>
+<script>
+export default {
+  name: "MatchesModal",
+  props: {
+    isOpen: {
+      type: Boolean,
+      required: true,
+    },
+    recommendMatches: {
+      type: Function,
+      required: true,
+    },
+    uuid: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      form: {
+        criteria: ["name", "email", "username"],
+        exclude: true,
+      },
+      jobId: null,
+      error: null,
+    };
+  },
+  methods: {
+    closeModal() {
+      this.$emit("update:isOpen", false);
+      this.form = {
+        criteria: ["name", "email", "username"],
+        exclude: true,
+      };
+      this.error = null;
+      this.jobId = null;
+    },
+    async createJob() {
+      try {
+        const response = await this.recommendMatches(
+          this.form.criteria,
+          this.form.exclude,
+          [this.uuid]
+        );
+        this.jobId = response.data.recommendMatches.jobId;
+      } catch (error) {
+        this.error = this.$getErrorMessage(error);
+        this.$logger.error(`Error recommending matches: ${error}`);
+      }
+    },
+  },
+};
+</script>
diff --git a/ui/src/views/Dashboard.vue b/ui/src/views/Dashboard.vue
index 70c31d5b..6da89c70 100644
--- a/ui/src/views/Dashboard.vue
+++ b/ui/src/views/Dashboard.vue
@@ -38,6 +38,7 @@
           :unlock-individual="unlockIndividual"
           :withdraw="withdraw"
           :update-enrollment="updateEnrollment"
+          :recommend-matches="recommendMatches"
           @saveIndividuals="addSavedIndividuals"
           @updateOrganizations="updateOrganizations"
           @updateWorkspace="updateWorkspace"
@@ -101,6 +102,7 @@ import {
   withdraw,
   updateEnrollment,
   manageMergeRecommendation,
+  recommendMatches,
 } from "../apollo/mutations";
 import IndividualsTable from "../components/IndividualsTable";
 import OrganizationsTable from "../components/OrganizationsTable";
@@ -335,6 +337,15 @@ export default {
       const response = await manageMergeRecommendation(this.$apollo, id, apply);
       return response;
     },
+    async recommendMatches(criteria, exclude, uuid) {
+      const response = await recommendMatches(
+        this.$apollo,
+        criteria,
+        exclude,
+        uuid
+      );
+      return response;
+    },
   },
   async mounted() {
     if (this.workspace && this.workspace.length > 0) {
diff --git a/ui/src/views/Individual.vue b/ui/src/views/Individual.vue
index 8785540a..5fc892de 100644
--- a/ui/src/views/Individual.vue
+++ b/ui/src/views/Individual.vue
@@ -6,7 +6,7 @@
           :cols="showRecommendations ? 8 : 12"
           :xl="showRecommendations ? 9 : 12"
         >
-          <v-row class="section pa-3 mb-4">
+          <v-row class="section pa-3 mb-4 mt-0">
             <v-col>
               <v-list-item class="pl-1">
                 <avatar :name="individual.name" :email="individual.email" />
@@ -84,6 +84,16 @@
             </v-col>
 
             <v-col cols="2" class="d-flex justify-end align-center mr-1">
+              <v-btn
+                class="mr-4"
+                text
+                small
+                outlined
+                @click="matchesModal.open = true"
+              >
+                <v-icon small left>mdi-lightbulb-on</v-icon>
+                Find matches
+              </v-btn>
               <v-btn
                 v-if="individual.isLocked"
                 class="mr-4"
@@ -331,6 +341,12 @@
       @updateIndividual="updateIndividual($event)"
     />
 
+    <matches-modal
+      :is-open.sync="matchesModal.open"
+      :recommend-matches="recommendMatches"
+      :uuid="mk"
+    />
+
     <v-dialog v-model="dialog.open" max-width="500px">
       <v-card class="pa-3">
         <v-card-title class="headline">{{ dialog.title }}</v-card-title>
@@ -379,6 +395,7 @@ import {
   updateEnrollment,
   updateProfile,
   withdraw,
+  recommendMatches,
 } from "../apollo/mutations";
 import { formatIndividual } from "../utils/actions";
 import { enrollMixin } from "../mixins/enroll";
@@ -388,6 +405,7 @@ import IndividualCard from "../components/IndividualCard.vue";
 import EnrollmentList from "../components/EnrollmentList.vue";
 import EnrollModal from "../components/EnrollModal.vue";
 import TeamEnrollModal from "../components/TeamEnrollModal.vue";
+import MatchesModal from "../components/MatchesModal.vue";
 
 export default {
   name: "Individual",
@@ -398,6 +416,7 @@ export default {
     EnrollmentList,
     EnrollModal,
     TeamEnrollModal,
+    MatchesModal,
   },
   mixins: [enrollMixin],
   data() {
@@ -420,6 +439,9 @@ export default {
         open: false,
         organization: "",
       },
+      matchesModal: {
+        open: false,
+      },
       countries: [],
     };
   },
@@ -651,6 +673,15 @@ export default {
         this.$logger.error(`Error applying recommendation ${id}: ${error}`);
       }
     },
+    async recommendMatches(criteria, exclude, uuid) {
+      const response = await recommendMatches(
+        this.$apollo,
+        criteria,
+        exclude,
+        uuid
+      );
+      return response;
+    },
   },
   mounted() {
     this.fetchIndividual();


Test patch:
diff --git a/ui/tests/unit/__snapshots__/mutations.spec.js.snap b/ui/tests/unit/__snapshots__/mutations.spec.js.snap
index 6fec7883..dff4b0ca 100644
--- a/ui/tests/unit/__snapshots__/mutations.spec.js.snap
+++ b/ui/tests/unit/__snapshots__/mutations.spec.js.snap
@@ -276,6 +276,8 @@ exports[`IndividualsTable Mock query for deleteIdentity 1`] = `
    
   <!---->
    
+  <!---->
+   
   <v-card-stub
     class="dragged-item"
     color="primary"
@@ -572,6 +574,8 @@ exports[`IndividualsTable Mock query for merge 1`] = `
    
   <!---->
    
+  <!---->
+   
   <v-card-stub
     class="dragged-item"
     color="primary"
@@ -868,6 +872,8 @@ exports[`IndividualsTable Mock query for moveIdentity 1`] = `
    
   <!---->
    
+  <!---->
+   
   <v-card-stub
     class="dragged-item"
     color="primary"
@@ -1164,6 +1170,8 @@ exports[`IndividualsTable Mock query for unmerge 1`] = `
    
   <!---->
    
+  <!---->
+   
   <v-card-stub
     class="dragged-item"
     color="primary"
@@ -1460,6 +1468,8 @@ exports[`IndividualsTable Mock query for updateEnrollment 1`] = `
    
   <!---->
    
+  <!---->
+   
   <v-card-stub
     class="dragged-item"
     color="primary"
@@ -1756,6 +1766,8 @@ exports[`IndividualsTable Mock query for withdraw 1`] = `
    
   <!---->
    
+  <!---->
+   
   <v-card-stub
     class="dragged-item"
     color="primary"
diff --git a/ui/tests/unit/__snapshots__/queries.spec.js.snap b/ui/tests/unit/__snapshots__/queries.spec.js.snap
index c260659b..18a2e86b 100644
--- a/ui/tests/unit/__snapshots__/queries.spec.js.snap
+++ b/ui/tests/unit/__snapshots__/queries.spec.js.snap
@@ -283,6 +283,8 @@ exports[`IndividualsTable Mock query for getCountries 1`] = `
    
   <!---->
    
+  <!---->
+   
   <v-card-stub
     class="dragged-item"
     color="primary"
@@ -579,6 +581,8 @@ exports[`IndividualsTable Mock query for getPaginatedIndividuals 1`] = `
    
   <!---->
    
+  <!---->
+   
   <v-card-stub
     class="dragged-item"
     color="primary"
diff --git a/ui/tests/unit/__snapshots__/storybook.spec.js.snap b/ui/tests/unit/__snapshots__/storybook.spec.js.snap
index 777bf32f..7a622bf3 100644
--- a/ui/tests/unit/__snapshots__/storybook.spec.js.snap
+++ b/ui/tests/unit/__snapshots__/storybook.spec.js.snap
@@ -7855,6 +7855,8 @@ exports[`Storyshots IndividualsTable Default 1`] = `
        
       <!---->
        
+      <!---->
+       
       <div
         class="dragged-item v-card v-sheet theme--dark primary"
       >
@@ -8450,6 +8452,8 @@ exports[`Storyshots IndividualsTable Expandable 1`] = `
        
       <!---->
        
+      <!---->
+       
       <div
         class="dragged-item v-card v-sheet theme--dark primary"
       >
@@ -8978,6 +8982,8 @@ exports[`Storyshots IndividualsTable Hidden Header 1`] = `
        
       <!---->
        
+      <!---->
+       
       <div
         class="dragged-item v-card v-sheet theme--dark primary"
       >
@@ -9573,6 +9579,8 @@ exports[`Storyshots IndividualsTable Outlined 1`] = `
        
       <!---->
        
+      <!---->
+       
       <div
         class="dragged-item v-card v-sheet theme--dark primary"
       >
@@ -9746,6 +9754,78 @@ exports[`Storyshots JobsTable Empty 1`] = `
 </div>
 `;
 
+exports[`Storyshots MatchesModal Default 1`] = `
+<div
+  class="v-application v-application--is-ltr theme--light"
+  data-app="true"
+  id="app"
+>
+  <div
+    class="v-application--wrap"
+  >
+    <div
+      class="ma-auto"
+      data-app="true"
+    >
+      <button
+        class="v-btn v-btn--is-elevated v-btn--has-bg theme--dark v-size--default primary"
+        type="button"
+      >
+        <span
+          class="v-btn__content"
+        >
+          
+      Open modal
+    
+        </span>
+      </button>
+       
+      <div
+        class="v-dialog__container"
+      >
+        <!---->
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Storyshots MatchesModal Error 1`] = `
+<div
+  class="v-application v-application--is-ltr theme--light"
+  data-app="true"
+  id="app"
+>
+  <div
+    class="v-application--wrap"
+  >
+    <div
+      class="ma-auto"
+      data-app="true"
+    >
+      <button
+        class="v-btn v-btn--is-elevated v-btn--has-bg theme--dark v-size--default primary"
+        type="button"
+      >
+        <span
+          class="v-btn__content"
+        >
+          
+      Open modal
+    
+        </span>
+      </button>
+       
+      <div
+        class="v-dialog__container"
+      >
+        <!---->
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
 exports[`Storyshots OrganizationEntry Default 1`] = `
 <div
   class="v-application v-application--is-ltr theme--light"
@@ -10075,7 +10155,7 @@ exports[`Storyshots OrganizationSelector Default 1`] = `
           <div
             aria-expanded="false"
             aria-haspopup="listbox"
-            aria-owns="list-1631"
+            aria-owns="list-1645"
             class="v-input__slot"
             role="combobox"
           >
@@ -10097,14 +10177,14 @@ exports[`Storyshots OrganizationSelector Default 1`] = `
             >
               <label
                 class="v-label theme--light"
-                for="input-1631"
+                for="input-1645"
                 style="left: 0px; position: absolute;"
               >
                 Organization
               </label>
               <input
                 autocomplete="off"
-                id="input-1631"
+                id="input-1645"
                 type="text"
               />
               <div
@@ -10173,7 +10253,7 @@ exports[`Storyshots OrganizationSelector Selected Organization 1`] = `
           <div
             aria-expanded="false"
             aria-haspopup="listbox"
-            aria-owns="list-1641"
+            aria-owns="list-1655"
             class="v-input__slot"
             role="combobox"
           >
@@ -10195,14 +10275,14 @@ exports[`Storyshots OrganizationSelector Selected Organization 1`] = `
             >
               <label
                 class="v-label v-label--active theme--light"
-                for="input-1641"
+                for="input-1655"
                 style="left: 0px; position: absolute;"
               >
                 Organization
               </label>
               <input
                 autocomplete="off"
-                id="input-1641"
+                id="input-1655"
                 type="text"
               />
               <div
@@ -10369,13 +10449,13 @@ exports[`Storyshots OrganizationsTable Groups 1`] = `
                 >
                   <label
                     class="v-label theme--light"
-                    for="input-1659"
+                    for="input-1673"
                     style="left: 0px; position: absolute;"
                   >
                     Search
                   </label>
                   <input
-                    id="input-1659"
+                    id="input-1673"
                     type="text"
                   />
                 </div>
@@ -10535,13 +10615,13 @@ exports[`Storyshots OrganizationsTable Groups 1`] = `
               >
                 <label
                   class="v-label v-label--active theme--light"
-                  for="input-1671"
+                  for="input-1685"
                   style="left: 0px; position: absolute;"
                 >
                   Items per page
                 </label>
                 <input
-                  id="input-1671"
+                  id="input-1685"
                   max="0"
                   min="1"
                   type="number"
@@ -10680,13 +10760,13 @@ exports[`Storyshots OrganizationsTable Organizations 1`] = `
                 >
                   <label
                     class="v-label theme--light"
-                    for="input-1713"
+                    for="input-1727"
                     style="left: 0px; position: absolute;"
                   >
                     Search
                   </label>
                   <input
-                    id="input-1713"
+                    id="input-1727"
                     type="text"
                   />
                 </div>
@@ -10846,13 +10926,13 @@ exports[`Storyshots OrganizationsTable Organizations 1`] = `
               >
                 <label
                   class="v-label v-label--active theme--light"
-                  for="input-1725"
+                  for="input-1739"
                   style="left: 0px; position: absolute;"
                 >
                   Items per page
                 </label>
                 <input
-                  id="input-1725"
+                  id="input-1739"
                   max="0"
                   min="1"
                   type="number"
@@ -11240,13 +11320,13 @@ exports[`Storyshots Search Default 1`] = `
               >
                 <label
                   class="v-label theme--light"
-                  for="input-1816"
+                  for="input-1830"
                   style="left: 0px; position: absolute;"
                 >
                   Search
                 </label>
                 <input
-                  id="input-1816"
+                  id="input-1830"
                   type="text"
                 />
               </div>
@@ -11367,13 +11447,13 @@ exports[`Storyshots Search Filter Selector 1`] = `
               >
                 <label
                   class="v-label theme--light"
-                  for="input-1825"
+                  for="input-1839"
                   style="left: 0px; position: absolute;"
                 >
                   Search
                 </label>
                 <input
-                  id="input-1825"
+                  id="input-1839"
                   type="text"
                 />
               </div>
@@ -11464,13 +11544,13 @@ exports[`Storyshots Search Order Selector 1`] = `
               >
                 <label
                   class="v-label theme--light"
-                  for="input-1837"
+                  for="input-1851"
                   style="left: 0px; position: absolute;"
                 >
                   Search
                 </label>
                 <input
-                  id="input-1837"
+                  id="input-1851"
                   type="text"
                 />
               </div>
@@ -11533,7 +11613,7 @@ exports[`Storyshots Search Order Selector 1`] = `
             <div
               aria-expanded="false"
               aria-haspopup="listbox"
-              aria-owns="list-1841"
+              aria-owns="list-1855"
               class="v-input__slot"
               role="button"
             >
@@ -11553,7 +11633,7 @@ exports[`Storyshots Search Order Selector 1`] = `
               >
                 <label
                   class="v-label theme--light"
-                  for="input-1841"
+                  for="input-1855"
                   style="left: 0px; position: absolute;"
                 >
                   Order by
@@ -11564,7 +11644,7 @@ exports[`Storyshots Search Order Selector 1`] = `
                   <input
                     aria-readonly="false"
                     autocomplete="off"
-                    id="input-1841"
+                    id="input-1855"
                     readonly="readonly"
                     type="text"
                   />
@@ -12492,7 +12572,7 @@ exports[`Storyshots WorkSpace Drag And Drop 1`] = `
                   />
                   <input
                     aria-checked="false"
-                    id="input-1981"
+                    id="input-1995"
                     role="checkbox"
                     type="checkbox"
                     value=""
@@ -12503,7 +12583,7 @@ exports[`Storyshots WorkSpace Drag And Drop 1`] = `
                 </div>
                 <label
                   class="v-label theme--light"
-                  for="input-1981"
+                  for="input-1995"
                   style="left: 0px; position: relative;"
                 >
                   Select all
@@ -12591,13 +12671,13 @@ exports[`Storyshots WorkSpace Drag And Drop 1`] = `
                   >
                     <label
                       class="v-label theme--light"
-                      for="input-1986"
+                      for="input-2000"
                       style="left: 0px; position: absolute;"
                     >
                       Search
                     </label>
                     <input
-                      id="input-1986"
+                      id="input-2000"
                       type="text"
                     />
                   </div>
@@ -12660,7 +12740,7 @@ exports[`Storyshots WorkSpace Drag And Drop 1`] = `
                 <div
                   aria-expanded="false"
                   aria-haspopup="listbox"
-                  aria-owns="list-1993"
+                  aria-owns="list-2007"
                   class="v-input__slot"
                   role="button"
                 >
@@ -12680,7 +12760,7 @@ exports[`Storyshots WorkSpace Drag And Drop 1`] = `
                   >
                     <label
                       class="v-label theme--light"
-                      for="input-1993"
+                      for="input-2007"
                       style="left: 0px; position: absolute;"
                     >
                       Order by
@@ -12691,7 +12771,7 @@ exports[`Storyshots WorkSpace Drag And Drop 1`] = `
                       <input
                         aria-readonly="false"
                         autocomplete="off"
-                        id="input-1993"
+                        id="input-2007"
                         readonly="readonly"
                         type="text"
                       />
@@ -12918,13 +12998,13 @@ exports[`Storyshots WorkSpace Drag And Drop 1`] = `
                   >
                     <label
                       class="v-label v-label--active theme--light"
-                      for="input-2015"
+                      for="input-2029"
                       style="left: 0px; position: absolute;"
                     >
                       Items per page
                     </label>
                     <input
-                      id="input-2015"
+                      id="input-2029"
                       max="0"
                       min="1"
                       type="number"
@@ -12967,6 +13047,8 @@ exports[`Storyshots WorkSpace Drag And Drop 1`] = `
          
         <!---->
          
+        <!---->
+         
         <div
           class="dragged-item v-card v-sheet theme--dark primary"
         >
diff --git a/ui/tests/unit/mutations.spec.js b/ui/tests/unit/mutations.spec.js
index 2215161e..1ef3defe 100644
--- a/ui/tests/unit/mutations.spec.js
+++ b/ui/tests/unit/mutations.spec.js
@@ -280,6 +280,7 @@ describe("IndividualsTable", () => {
         unlockIndividual: () => {},
         withdraw: () => {},
         updateEnrollment: () => {},
+        recommendMatches: () => {}
       },
       ...options,
     });
diff --git a/ui/tests/unit/queries.spec.js b/ui/tests/unit/queries.spec.js
index dbb1a881..197e23a5 100644
--- a/ui/tests/unit/queries.spec.js
+++ b/ui/tests/unit/queries.spec.js
@@ -262,6 +262,7 @@ describe("IndividualsTable", () => {
         unlockIndividual: () => {},
         withdraw: () => {},
         updateEnrollment: () => {},
+        recommendMatches: () => {}
       },
       ...options,
     });