From c160050a18fd85c245de8270f6dc62c8c7154d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eva=20Mill=C3=A1n?= Date: Mon, 8 Sep 2025 17:37:35 +0200 Subject: [PATCH 1/2] [schema] Order merge recommendations by number of suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Orders the list of merge recommendations by the individual's number of suggestions. Signed-off-by: Eva Millán --- sortinghat/core/schema.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sortinghat/core/schema.py b/sortinghat/core/schema.py index 5e84a419..2a6ae527 100644 --- a/sortinghat/core/schema.py +++ b/sortinghat/core/schema.py @@ -2129,7 +2129,15 @@ def resolve_recommended_merge(self, info, filters=None, page=1, else: query = MergeRecommendation.objects.filter(applied=None) - query = query.order_by('created_at') + query = query.annotate(individual_matches_count=Count( + 'individual1__match_recommendation_individual_1', + filter=Q(individual1__match_recommendation_individual_1__applied=None), + distinct=True + ) + Count( + 'individual1__match_recommendation_individual_2', + filter=Q(individual1__match_recommendation_individual_2__applied=None), + distinct=True + )) return RecommendedMergePaginatedType.create_paginated_result(query, page, From 562743c227d683fecb616939d536b7c7d2d78139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eva=20Mill=C3=A1n?= Date: Mon, 8 Sep 2025 17:41:00 +0200 Subject: [PATCH 2/2] [ui] Group merge recommendations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recommendations belonging to the same individual are grouped and can be managed in batches. Signed-off-by: Eva Millán --- .../improved-ui-for-merge-recommendations.yml | 10 + sortinghat/core/schema.py | 2 + ui/src/apollo/queries.js | 52 +- ui/src/components/IndividualCard.stories.js | 53 + ui/src/components/IndividualCard.vue | 75 +- ui/src/components/IndividualsTable.stories.js | 2 + ui/src/components/Recommendation.stories.js | 113 +- ui/src/components/Recommendations.vue | 94 +- ui/src/components/WorkSpace.stories.js | 76 +- .../unit/__snapshots__/storybook.spec.js.snap | 34110 ++++++++-------- 10 files changed, 17598 insertions(+), 16989 deletions(-) create mode 100644 releases/unreleased/improved-ui-for-merge-recommendations.yml diff --git a/releases/unreleased/improved-ui-for-merge-recommendations.yml b/releases/unreleased/improved-ui-for-merge-recommendations.yml new file mode 100644 index 00000000..48003455 --- /dev/null +++ b/releases/unreleased/improved-ui-for-merge-recommendations.yml @@ -0,0 +1,10 @@ +--- +title: Improved UI for merge recommendations +category: added +author: Eva Millán +issue: null +notes: > + The user interface now shows more than one merge + recommendation at a time, and recommendations for + an individual are now grouped so they can be managed + in batches. diff --git a/sortinghat/core/schema.py b/sortinghat/core/schema.py index 2a6ae527..d9617928 100644 --- a/sortinghat/core/schema.py +++ b/sortinghat/core/schema.py @@ -2139,6 +2139,8 @@ def resolve_recommended_merge(self, info, filters=None, page=1, distinct=True )) + query = query.order_by('-individual_matches_count', 'individual1__mk', 'created_at') + return RecommendedMergePaginatedType.create_paginated_result(query, page, page_size=page_size) diff --git a/ui/src/apollo/queries.js b/ui/src/apollo/queries.js index 363f61a1..ad140b14 100644 --- a/ui/src/apollo/queries.js +++ b/ui/src/apollo/queries.js @@ -205,10 +205,53 @@ const GET_PAGINATED_RECOMMENDED_MERGE = gql` entities { id individual1 { - ...individual - } - individual2 { - ...individual + mk + isLocked + profile { + name + email + isBot + } + identities { + name + source + email + uuid + username + } + enrollments { + start + end + group { + name + } + } + matchRecommendationSet { + id + individual { + mk + isLocked + profile { + name + email + isBot + } + identities { + name + source + email + uuid + username + } + enrollments { + start + end + group { + name + } + } + } + } } } pageInfo { @@ -220,7 +263,6 @@ const GET_PAGINATED_RECOMMENDED_MERGE = gql` } } } - ${FULL_INDIVIDUAL} `; const GET_IMPORTERS = gql` diff --git a/ui/src/components/IndividualCard.stories.js b/ui/src/components/IndividualCard.stories.js index f46a2ee1..a159aba4 100644 --- a/ui/src/components/IndividualCard.stories.js +++ b/ui/src/components/IndividualCard.stories.js @@ -21,6 +21,7 @@ const individualCardTemplate = ` :usernames="usernames" :emails="emails" :detailed="detailed" + :recommendation="recommendation" />`; export const Default = () => ({ @@ -805,3 +806,55 @@ export const Detailed = () => ({ }, }, }); + +export const Recommendation = () => ({ + components: { IndividualCard }, + template: individualCardTemplate, + props: { + name: { + default: "Tom Marvolo Riddle", + }, + sources: { + default: () => [], + }, + isLocked: { + default: false, + }, + uuid: { + default: "10f546", + }, + email: { + default: "triddle@example.net", + }, + identities: { + default: () => [], + }, + enrollments: { + default: () => [], + }, + isHighlighted: { + default: false, + }, + closable: { + default: false, + }, + selectable: { + default: false, + }, + isSelected: { + default: false, + }, + usernames: { + default: () => ['{"mdi-gitlab":"triddle"}', '{"mdi-github":"voldemort"}'], + }, + emails: { + default: () => ["triddle@example.net", "voldemort@example.net"], + }, + detailed: { + default: true, + }, + recommendation: { + default: true, + }, + }, +}); diff --git a/ui/src/components/IndividualCard.vue b/ui/src/components/IndividualCard.vue index aef518e4..1e938157 100644 --- a/ui/src/components/IndividualCard.vue +++ b/ui/src/components/IndividualCard.vue @@ -4,9 +4,11 @@ :class="{ locked: isLocked, dropzone: isDragging, - selected: isSelected, + selected: isSelected && !recommendation, highlighted: isHighlighted, disabled: !selectable, + 'selected--merge': state.merge, + 'selected--dismiss': state.dismiss, }" :ripple="selectable" v-bind="$attrs" @@ -103,6 +105,29 @@ @mousedown.stop /> + @@ -157,7 +182,8 @@ export default { }, isLocked: { type: Boolean, - required: true, + required: false, + default: false, }, closable: { type: Boolean, @@ -180,10 +206,19 @@ export default { required: false, default: false, }, + recommendation: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { isDragging: false, + state: { + merge: false, + dismiss: false, + }, }; }, computed: { @@ -252,6 +287,30 @@ export default { this.isDragging = false; } }, + handleMerge() { + if (this.state.merge) { + this.state.merge = false; + this.$emit("apply", null); + } else { + this.state.merge = true; + this.$emit("apply", true); + if (this.state.dismiss) { + this.state.dismiss = false; + } + } + }, + handleDismiss() { + if (this.state.dismiss) { + this.state.dismiss = false; + this.$emit("apply", null); + } else { + this.state.dismiss = true; + this.$emit("apply", false); + if (this.state.merge) { + this.state.merge = false; + } + } + }, }, }; @@ -300,4 +359,16 @@ export default { opacity: 0.7; } } + +.v-card.selected--merge { + background-color: #f1f8e9; +} + +.v-card.selected--dismiss { + background-color: #ffebee; +} + +.v-card-actions { + padding-top: 0; +} diff --git a/ui/src/components/IndividualsTable.stories.js b/ui/src/components/IndividualsTable.stories.js index 628a9495..65c994e0 100644 --- a/ui/src/components/IndividualsTable.stories.js +++ b/ui/src/components/IndividualsTable.stories.js @@ -6,6 +6,7 @@ export default { }; const IndividualsTableTemplate = ` + + `; const query = [ diff --git a/ui/src/components/Recommendation.stories.js b/ui/src/components/Recommendation.stories.js index 966f8c65..b7fd2899 100644 --- a/ui/src/components/Recommendation.stories.js +++ b/ui/src/components/Recommendation.stories.js @@ -6,12 +6,12 @@ export default { }; const defaultTemplate = ` -
+ -
`; +`; const slotTemplate = ` -
+ -
`; +`; const recommendations = [ { @@ -58,36 +58,62 @@ const recommendations = [ end: "1945-06-02T00:00:00+00:00", }, ], - }, - individual2: { - mk: "8998b2f0bd86780fb7c8c141956d68c9628cbec8", - isLocked: false, - profile: { - name: "Voldemort", - id: "37", - email: "triddle@example.com", - isBot: false, - }, - identities: [ + matchRecommendationSet: [ { - name: "Voldemort", - source: "git", - email: "triddle@example.com", - uuid: "8998b2f0bd86780fb7c8c141956d68c9628cbec8", + id: 39, + individual: { + mk: "8998b2f0bd86780fb7c8c141956d68c9628cbec8", + isLocked: false, + profile: { + name: "Voldemort", + id: "37", + email: "triddle@example.com", + isBot: false, + }, + identities: [ + { + name: "Voldemort", + source: "git", + email: "triddle@example.com", + uuid: "8998b2f0bd86780fb7c8c141956d68c9628cbec8", + }, + { + username: "voldemort", + source: "github", + email: "triddle@example.com", + uuid: "8998b2f0bd86780fb7c8c141956d68c9628cbec9", + }, + ], + enrollments: [], + }, }, { - username: "voldemort", - source: "github", - email: "triddle@example.com", - uuid: "8998b2f0bd86780fb7c8c141956d68c9628cbec9", + id: 40, + individual: { + mk: "8998b2f0bd86780fb7c8c141956d68c9628cbec8", + isLocked: false, + profile: { + name: "T. Riddle", + id: "37", + email: "triddle@example.com", + isBot: false, + }, + identities: [ + { + source: "git", + email: "triddle@example.com", + uuid: "8998b2f0bd86780fb7c8c141956d68c9628cbec9", + }, + ], + enrollments: [], + }, }, ], - enrollments: [], }, }, ], pageInfo: { - totalResults: 2, + totalResults: 4, page: 1, hasNext: true, }, @@ -136,23 +162,28 @@ const recommendations = [ end: "1899-06-02T00:00:00+00:00", }, ], - }, - individual2: { - mk: "8998b2f0bd86780fb7c8c141956d68c9628cbec8", - isLocked: false, - profile: { - id: "37", - email: "dumbledore@example.net", - isBot: false, - }, - identities: [ + matchRecommendationSet: [ { - source: "git", - email: "dumbledore@example.net", - uuid: "4350d4c5916cfe8e2e18d290e02a471d95b112d7", + id: 39, + individual: { + mk: "8998b2f0bd86780fb7c8c141956d68c9628cbec8", + isLocked: false, + profile: { + id: "37", + email: "dumbledore@example.net", + isBot: false, + }, + identities: [ + { + source: "git", + email: "dumbledore@example.net", + uuid: "4350d4c5916cfe8e2e18d290e02a471d95b112d7", + }, + ], + enrollments: [], + }, }, ], - enrollments: [], }, }, ], @@ -179,7 +210,9 @@ export const Default = () => ({ return recommendations[this.index]; }, manageRecommendation() { - this.index = +!this.index; + if (recommendations.length > 1) { + recommendations.shift(); + } return true; }, }, diff --git a/ui/src/components/Recommendations.vue b/ui/src/components/Recommendations.vue index b4266d4f..555971e1 100644 --- a/ui/src/components/Recommendations.vue +++ b/ui/src/components/Recommendations.vue @@ -30,6 +30,7 @@