Merge pull request #8840 from aignatov-bio/ai-sci-12302-add-tags-merge-modal

Add merge tags modal [SCI-12302]
This commit is contained in:
Alex Kriuchykhin 2025-09-01 11:12:19 +02:00 committed by GitHub
commit fe17f2256c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 185 additions and 11 deletions

View file

@ -5,7 +5,7 @@ module Users
class TagsController < ApplicationController
before_action :load_team
before_action :check_team_permissions
before_action :load_vars, only: %i(update destroy)
before_action :load_vars, only: %i(update destroy merge)
before_action :set_breadcrumbs_items, only: %i(index)
def index
@ -20,6 +20,10 @@ module Users
end
end
def list
@tags = @team.tags.order(:name)
end
def actions_toolbar
render json: {
actions:
@ -63,6 +67,23 @@ module Users
end
end
def merge
ActiveRecord::Base.transaction do
tags_to_merge = @team.tags.where(id: params[:merge_ids]).where.not(id: @tag.id)
taggings_to_update = Tagging.where(tag_id: tags_to_merge.select(:id))
.where.not(id: Tagging.where(tag_id: @tag.id).select(:id))
taggings_to_update.update!(tag_id: @tag.id)
tags_to_merge.each(&:destroy!)
render json: { message: :ok }, status: :ok
rescue ActiveRecord::RecordInvalid => e
render json: { error: e.message }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
private
def load_team

View file

@ -14,8 +14,14 @@
@changeColor="changeColor"
@changeName="changeName"
@createRow="createTag"
@merge="openMergeModal"
@delete="deleteTag"
/>
<merge-modal v-if="mergeIds"
:team-id="teamId"
:mergeIds="mergeIds"
:list-url="listUrl"
@close="mergeIds = null; reloadingTable = true"/>
</div>
</template>
@ -27,6 +33,7 @@ import axios from '../../packs/custom_axios.js';
import DataTable from '../shared/datatable/table.vue';
import colorRenderer from './renderers/color.vue';
import nameRenderer from './renderers/name.vue';
import mergeModal from './modals/merge.vue';
import {
users_settings_team_tag_path,
@ -38,6 +45,8 @@ export default {
components: {
DataTable,
colorRenderer,
nameRenderer,
mergeModal
},
props: {
dataSource: {
@ -54,12 +63,20 @@ export default {
tagsColors: {
type: Object,
required: true
},
teamId: {
required: true
},
listUrl: {
type: String,
required: true
}
},
data() {
return {
reloadingTable: false,
addingNewRow: false,
mergeIds: null,
newRowTemplate: {
name: {
value: '',
@ -144,6 +161,9 @@ export default {
this.reloadingTable = true;
})
},
openMergeModal(event, rows) {
this.mergeIds = rows.map(row => row.id);
},
createTag(newTag) {
this.addingNewRow = false;
axios.post(this.createUrl, {

View file

@ -0,0 +1,87 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" >
{{ i18n.t('tags.merge_modal.title') }}
</h4>
</div>
<div class="modal-body">
<p>{{ i18n.t('tags.merge_modal.description_html') }}</p>
<div>
<div class="sci-label mb-1">{{ i18n.t('tags.merge_modal.target_tag') }}</div>
<SelectDropdown
:optionsUrl="listUrl"
:value="selectedTagId"
:searchable="true"
:placeholder="i18n.t('tags.merge_modal.target_tag_placeholder')"
@change="selectedTagId = $event"
:option-renderer="tagRenderer"
></SelectDropdown>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.close') }}</button>
<button type="button" class="btn btn-danger" :disabled="!selectedTagId" @click="mergeTags">
{{ i18n.t('tags.merge_modal.merge') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import SelectDropdown from '../../shared/select_dropdown.vue';
import modalMixin from '../../shared/modal_mixin';
import axios from '../../../packs/custom_axios.js';
import escapeHtml from '../../shared/escape_html.js';
import {
merge_users_settings_team_tag_path,
} from '../../../routes.js';
export default {
name: 'MergeTagsModal',
props: {
teamId: {
required: true
},
listUrl: {
type: String,
required: true
},
mergeIds: {
type: Array,
required: true
}
},
components: {
SelectDropdown
},
mixins: [modalMixin, escapeHtml],
data() {
return {
selectedTagId: null
};
},
methods: {
mergeTags() {
axios.post(merge_users_settings_team_tag_path(this.selectedTagId, {team_id: this.teamId}), {
merge_ids: this.mergeIds
}).then(() => {
HelperModule.flashAlertMsg(this.i18n.t('tags.merge_modal.merge_success'), 'success');
this.$emit('close');
}).catch((error) => {
HelperModule.flashAlertMsg(this.i18n.t('tags.merge_modal.merge_error'), 'danger');
});
},
tagRenderer(tag) {
return `<div class="sci-tag text-white" style="background-color: ${tag[2]};">${escapeHtml(tag[1])}</div>`;
}
}
};
</script>

View file

@ -53,7 +53,7 @@
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
import escapeHtml from '../../../shared/escape_html.js';
import escapeHtml from '../../shared/escape_html.js';
export default {
name: 'AddMembersModal',

View file

@ -5,7 +5,7 @@ module Lists
private
def fetch_records
@records = @raw_data.left_joins(:created_by, :taggings)
@records = @raw_data.left_joins(:created_by, :taggings).includes(:created_by, :last_modified_by)
.select('tags.*')
.select('array_agg(users.full_name) AS created_by_user')
.select('COUNT(taggings.id) AS taggings_count')

View file

@ -19,6 +19,7 @@ module Toolbars
return [] if @tags.none?
[
merge_action,
delete_action
].compact
end
@ -38,5 +39,16 @@ module Toolbars
type: :emit
}
end
def merge_action
# return unless can_manage_team?(@team)
{
name: 'merge',
label: I18n.t('tags.index.toolbar.merge'),
icon: 'sn-icon sn-icon-merge',
type: :emit
}
end
end
end

View file

@ -9,6 +9,8 @@
<%= render partial: 'users/settings/teams/header' %>
<div id="tagsTable" class="fixed-content-body user-groups-table-container">
<tags-table
team-id="<%= @team.id %>"
list-url="<%= list_users_settings_team_tags_path(@team) %>"
actions-url="<%= actions_toolbar_users_settings_team_tags_path(@team) %>"
data-source="<%= users_settings_team_tags_path(@team, format: :json) %>"
create-url="<%= users_settings_team_tags_path(@team) %>"

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
json.data do
json.array! @tags do |t|
json.array! [t.id, t.name, t.color]
end
end

View file

@ -4227,6 +4227,15 @@ en:
too_short_name: "too short (minimum is %{count} characters)"
toolbar:
delete: "Delete"
merge: "Merge into"
merge_modal:
title: "Merge tags"
description_html: "Selected tags will be merged into the target tag you choose below. They will be replaced wherever they were used, and then deleted."
target_tag: "Target tag"
target_tag_placeholder: "Search for tag to merge into"
merge: "Merge"
merge_success: "Tags merged successfully."
merge_error: "There was an error merging tags."
user_groups:
promo:
head_title: 'User groups'

View file

@ -152,6 +152,10 @@ Rails.application.routes.draw do
resources :tags, only: %i(index create update destroy) do
collection do
post :actions_toolbar
get :list
end
member do
post :merge
end
end
resources :user_groups, only: %i(index create update destroy show) do

Binary file not shown.

View file

@ -117,8 +117,8 @@
<glyph unicode="&#xe958;" glyph-name="courses" data-tags="courses" d="M506.027 745.387c2.987 1.707 6.827 1.707 9.813 0l368.64-214.613c7.253-3.84 10.667-11.093 10.667-18.773v-170.667c0-11.947-8.96-21.333-20.053-21.333s-20.053 9.387-20.053 21.333v134.4l-80.64-46.933v-172.8c0-8.107-4.267-15.787-11.093-19.2l-242.347-128c-5.547-2.987-12.373-2.987-17.92 0l-242.347 128c-6.827 3.84-11.093 11.093-11.093 19.2v172.8l-110.933 64.427c-14.080 8.107-14.080 29.44 0 37.547l368.64 214.613zM190.72 512l321.707-187.307 321.707 187.307-321.707 187.307-321.707-187.307zM734.72 405.334l-217.6-126.72c-2.987-1.707-6.827-1.707-9.813 0l-217.6 126.72v-136.107l222.293-117.333 222.293 117.333v136.107z" />
<glyph unicode="&#xe959;" glyph-name="comments" data-tags="comments" d="M128 62.294v659.2c0 20.053 6.4 36.267 19.627 49.493s29.867 19.627 49.493 19.627h629.333c20.053 0 36.267-6.4 49.493-19.627s19.627-29.867 19.627-49.493v-458.667c0-20.053-6.4-36.267-19.627-49.493s-29.867-19.627-49.493-19.627h-567.467l-130.987-131.413zM170.667 165.974l70.4 70.4h585.387c7.68 0 14.080 2.56 19.2 7.68s7.253 11.52 7.253 19.2v458.24c0 7.68-2.56 14.080-7.253 19.2-5.12 5.12-11.52 7.68-19.2 7.68h-629.333c-7.68 0-14.080-2.56-19.2-7.68s-7.68-11.52-7.68-19.2v-555.52zM170.667 721.494v0zM725.333 535.040h-426.667v42.667h426.667v-42.667zM725.333 407.040h-426.667v42.667h426.667v-42.667z" />
<glyph unicode="&#xe95a;" glyph-name="collapse" data-tags="collapse" d="M810.667 426.667v42.667h-256v256h-42.667v-298.667h298.667zM823.040 768l-283.733-283.733 30.293-30.293 283.733 283.733-30.293 30.293zM213.333 426.667v-42.667h256v-256h42.667v298.667h-298.667zM453.973 399.36l-283.733-283.733 30.293-30.293 283.733 283.733-30.293 30.293z" />
<glyph unicode="&#xe95b;" glyph-name="close" data-tags="close-remove" d="M273.067 157.867l-29.867 29.867 238.933 238.933-238.933 238.933 29.867 29.867 238.933-238.933 238.933 238.933 29.867-29.867-238.933-238.933 238.933-238.933-29.867-29.867-238.933 238.933-238.933-238.933z" />
<glyph unicode="&#xe95c;" glyph-name="close-small" data-tags="close-remove-small" d="M331.093 637.867l-30.293-30.293 392.107-392.107 30.293 30.293-392.107 392.107zM300.8 245.76l30.293-30.293 392.107 392.107-30.293 30.293-392.107-392.107z" />
<glyph unicode="&#xe95b;" glyph-name="close" data-tags="close-remove" d="M277.655 178.876l-30.197 30.195 238.933 238.933-238.933 238.934 30.197 30.197 238.935-238.932 238.933 238.932 30.195-30.197-238.933-238.934 238.933-238.933-30.195-30.195-238.933 238.933-238.935-238.933z" />
<glyph unicode="&#xe95c;" glyph-name="close-small" data-tags="close-remove-small" d="M343.633 248.614l-31.008 31.010 168.361 168.371-168.361 167.309 31.008 31.008 168.371-168.365 167.309 168.365 31.006-31.008-168.363-167.309 168.363-168.371-31.006-31.010-167.309 168.363-168.371-168.363z" />
<glyph unicode="&#xe95d;" glyph-name="up" data-tags="close-hide" d="M512 554.667l213.333-225.28-28.16-30.72-185.173 195.413-185.173-195.413-28.16 30.72 213.333 225.28z" />
<glyph unicode="&#xe95e;" glyph-name="checkllist" data-tags="checkllist" d="M281.6 541.44l-68.267 68.267 30.293 30.293 37.973-38.4 123.733 123.733 30.293-30.293-154.027-153.6zM810.667 640h-298.667v-42.667h298.667v42.667zM810.667 256h-298.667v-42.667h298.667v42.667zM324.267 298.667c35.413 0 64-28.587 64-64s-28.587-64-64-64-64 28.587-64 64 28.587 64 64 64zM324.267 341.334c-58.88 0-106.667-47.787-106.667-106.667s47.787-106.667 106.667-106.667 106.667 47.787 106.667 106.667-47.787 106.667-106.667 106.667z" />
<glyph unicode="&#xe95f;" glyph-name="check" data-tags="check" d="M407.467 200.96l-212.053 211.2 30.72 31.147 181.333-181.333 390.4 390.4 30.72-30.72-421.547-420.267z" />
@ -235,4 +235,7 @@
<glyph unicode="&#xe9ce;" glyph-name="printer-labels" data-tags="printer-labels" d="M128 677.739c0 19.641 6.578 36.039 19.733 49.195s29.554 19.733 49.195 19.733h630.144c19.639 0 36.041-6.578 49.195-19.733s19.733-29.554 19.733-49.195v-336.405l-192-192h-507.072c-19.641 0-36.039 6.579-49.195 19.733s-19.733 29.555-19.733 49.195v459.477zM682.667 362.667h170.667v315.072c0 6.571-2.735 12.59-8.201 18.059-5.47 5.469-11.49 8.203-18.061 8.203h-630.144c-6.571 0-12.59-2.734-18.059-8.203s-8.203-11.488-8.203-18.059v-459.477c0-6.571 2.734-12.591 8.203-18.057 5.469-5.47 11.488-8.205 18.059-8.205h485.739v170.667z" />
<glyph unicode="&#xe9cf;" glyph-name="audit-trails" data-tags="audit-trails" d="M265.845 64c-26.254 0-48.683 9.165-67.285 27.49-18.595 18.325-27.893 40.341-27.893 66.048v579.283c0 26.254 9.298 48.683 27.893 67.285 18.603 18.595 41.031 27.893 67.285 27.893h443.083v-623.595h-443.083c-14.279 0-26.599-4.907-36.96-14.72-10.368-9.822-15.552-21.871-15.552-36.147 0-14.272 5.184-26.321 15.552-36.143 10.361-9.818 22.681-14.729 36.96-14.729h544.821v640h42.667v-682.667h-587.488zM265.845 251.072h400.416v538.261h-400.416c-14.827 0-27.285-5.184-37.376-15.552-10.091-10.361-15.136-22.681-15.136-36.96v-503.059c7.659 4.868 15.822 8.973 24.491 12.309 8.669 3.332 18.009 5.001 28.021 5.001zM315.072 393.847h34.795l25.269 67.772h130.379l25.024-67.772h33.975l-104.623 278.974h-39.379l-105.44-278.974zM386.795 493.623l53.581 143.422h0.486l52.757-143.422h-106.825z" />
<glyph unicode="&#xe9d0;" glyph-name="cloud-storage" data-tags="cloud-storage" d="M512 106.667c-99.719 0-181.539 12.702-245.461 38.11-63.915 25.408-95.872 58.108-95.872 98.095v418.462c0 35.449 33.273 65.643 99.819 90.581 66.539 24.946 147.043 37.419 241.515 37.419s174.976-12.473 241.515-37.419c66.547-24.939 99.819-55.133 99.819-90.581v-418.462c0-39.987-31.957-72.687-95.872-98.095-63.923-25.408-145.741-38.11-245.461-38.11zM512 580.672c61.099 0 122.692 8.523 184.777 25.568 62.089 17.038 99.23 35.595 111.428 55.669-11.648 21.17-48.205 40.587-109.666 58.251-61.453 17.671-123.635 26.507-186.539 26.507-61.973 0-124.071-8.519-186.293-25.557s-99.406-35.677-111.552-55.915c11.598-21.333 48.235-40.754 109.909-58.261s124.32-26.261 187.936-26.261zM512 365.952c29.321 0 58.121 1.421 86.4 4.267s55.317 7.070 81.109 12.672c25.792 5.611 49.749 12.599 71.872 20.971 22.131 8.363 41.89 17.796 59.285 28.297v174.775c-17.395-10.503-37.154-19.939-59.285-28.309-22.123-8.37-46.080-15.357-71.872-20.96-25.792-5.611-52.83-9.838-81.109-12.683s-57.079-4.267-86.4-4.267c-30.413 0-59.977 1.561-88.693 4.683-28.722 3.115-55.84 7.477-81.355 13.088-25.522 5.603-49.166 12.455-70.933 20.555-21.774 8.093-41.003 17.39-57.685 27.893v-174.775c16.683-10.5 35.911-19.797 57.685-27.891 21.767-8.094 45.411-14.946 70.933-20.557 25.515-5.602 52.633-9.963 81.355-13.086 28.717-3.115 58.281-4.672 88.693-4.672zM512 149.333c36.535 0 71.283 2.078 104.243 6.238 32.96 4.156 62.571 10.048 88.823 17.677s48.572 16.738 66.957 27.328c18.381 10.581 31.262 22.025 38.643 34.334v154.581c-17.395-10.5-37.154-19.934-59.285-28.297-22.123-8.371-46.080-15.36-71.872-20.971-25.792-5.602-52.83-9.826-81.109-12.672s-57.079-4.267-86.4-4.267c-30.413 0-59.977 1.557-88.693 4.672-28.722 3.123-55.84 7.484-81.355 13.086-25.522 5.611-49.166 12.463-70.933 20.557-21.774 8.094-41.003 17.391-57.685 27.891v-154.825c7.381-12.855 20.224-24.397 38.528-34.624 18.297-10.231 40.573-19.166 66.827-26.795s55.901-13.521 88.939-17.677c33.047-4.16 67.838-6.238 104.373-6.238z" />
<glyph unicode="&#xe9d1;" glyph-name="merge" data-tags="merge" d="M213.588 656.719l30.197 30.197 221.538-221.534h304.981l-132.023 132.020 30.443 30.443 183.799-183.796-184.043-184.043-30.443 30.443 132.267 132.267h-322.709l-234.007 234.004zM213.342 231.377l152.693 152.939 30.443-30.443-152.939-152.695-30.197 30.199z" />
<glyph unicode="&#xe9d2;" glyph-name="ai" data-tags="AI" d="M579.874 537.25l324.668-99.917-324.668-99.874-99.874-324.668-99.917 324.668-324.625 99.874 324.625 99.917 99.917 324.625 99.874-324.625zM417.583 513.873l-3.334-10.79-10.792-3.332-202.958-62.417 213.75-65.749 3.334-10.79 62.417-202.918 65.749 213.709 10.79 3.332 202.918 62.417-213.709 65.749-3.332 10.79-62.417 202.96-62.417-202.96zM814.541 767.5l128.585-42.167-128.585-42.125-46.541-126.125-46.583 126.125-128.542 42.125 128.542 42.167 46.583 126.083 46.541-126.083z" />
<glyph unicode="&#xe9d3;" glyph-name="close-solid" data-tags="close-solid" d="M512 917.333c259.204 0 469.333-210.128 469.333-469.333 0-259.204-210.129-469.333-469.333-469.333-259.206 0-469.333 210.129-469.333 469.333 0 259.206 210.128 469.333 469.333 469.333zM516.582 478.208l-238.916 238.917-30.208-30.167 238.916-238.959-238.916-238.916 30.208-30.208 238.916 238.916 238.959-238.916 30.165 30.208-238.916 238.916 238.916 238.959-30.165 30.167-238.959-238.917z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Binary file not shown.

View file

@ -1,11 +1,11 @@
@font-face {
font-family: 'SN-icon-font';
src: url('fonts/SN-icon-font.eot?gqphqg');
src: url('fonts/SN-icon-font.eot?gqphqg#iefix') format('embedded-opentype'),
url('fonts/SN-icon-font.woff2?gqphqg') format('woff2'),
url('fonts/SN-icon-font.ttf?gqphqg') format('truetype'),
url('fonts/SN-icon-font.woff?gqphqg') format('woff'),
url('fonts/SN-icon-font.svg?gqphqg#SN-icon-font') format('svg');
src: url('fonts/SN-icon-font.eot?uepbw');
src: url('fonts/SN-icon-font.eot?uepbw#iefix') format('embedded-opentype'),
url('fonts/SN-icon-font.woff2?uepbw') format('woff2'),
url('fonts/SN-icon-font.ttf?uepbw') format('truetype'),
url('fonts/SN-icon-font.woff?uepbw') format('woff'),
url('fonts/SN-icon-font.svg?uepbw#SN-icon-font') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@ -653,3 +653,12 @@
.sn-icon-cloud-storage:before {
content: "\e9d0";
}
.sn-icon-merge:before {
content: "\e9d1";
}
.sn-icon-ai:before {
content: "\e9d2";
}
.sn-icon-close-solid:before {
content: "\e9d3";
}