From 1baec4f47d3bf10eeb6d9c79139d6dabdf7d16a0 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 2 Sep 2025 15:31:46 +0200 Subject: [PATCH] Add tags input to task page [SCI-12286] --- app/assets/javascripts/my_modules.js | 81 ----------- app/assets/stylesheets/tailwind/tags.css | 8 +- app/controllers/concerns/taggable_actions.rb | 39 ++++++ app/controllers/my_modules_controller.rb | 6 + app/javascript/vue/my_module/details.vue | 15 ++- app/javascript/vue/shared/tags_input.vue | 133 +++++++++++++++++++ app/serializers/my_module_serializer.rb | 16 ++- app/views/my_modules/protocols.html.erb | 26 ---- config/locales/en.yml | 3 + config/routes.rb | 2 + 10 files changed, 216 insertions(+), 113 deletions(-) create mode 100644 app/controllers/concerns/taggable_actions.rb create mode 100644 app/javascript/vue/shared/tags_input.vue diff --git a/app/assets/javascripts/my_modules.js b/app/assets/javascripts/my_modules.js index bfb3e2a16..d605fb4c1 100644 --- a/app/assets/javascripts/my_modules.js +++ b/app/assets/javascripts/my_modules.js @@ -98,87 +98,7 @@ } } - function initTagsSelector() { - var myModuleTagsSelector = '#module-tags-selector'; - dropdownSelector.init($(myModuleTagsSelector), { - closeOnSelect: true, - tagClass: 'my-module-white-tags sci-tag', - labelHTML: true, - tagStyle: (data) => { - return `background: ${data.params.color}`; - }, - customDropdownIcon: () => { - return ''; - }, - optionLabel: (data) => { - if (data.value > 0) { - return `${data.label}`; - } - return ` - ${data.label + ' '} - ${I18n.t('my_modules.details.create_new_tag')}`; - }, - onOpen: function() { - $('.select-container .edit-button-container').removeClass('hidden'); - }, - onClose: function() { - $('.select-container .edit-button-container').addClass('hidden'); - }, - onSelect: function() { - var selectElement = $(myModuleTagsSelector); - var lastTag = selectElement.next().find('.ds-tags').last(); - var lastTagId = lastTag.find('.tag-label').data('ds-tag-id'); - var newTag; - - if (lastTagId > 0) { - newTag = { my_module_tag: { tag_id: lastTagId } }; - $.post(selectElement.data('update-module-tags-url'), newTag) - .fail(function(response) { - dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true); - if (response.status === 403) { - HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger'); - } - }); - } else if (lastTag.length > 0) { - newTag = { - tag: { - name: lastTag.find('.tag-label').text(), - project_id: selectElement.data('project-id'), - color: null - }, - my_module_id: selectElement.data('module-id'), - simple_creation: true - }; - - $.post(selectElement.data('tags-create-url'), newTag, function(result) { - dropdownSelector.removeValue(myModuleTagsSelector, 0, '', true); - dropdownSelector.addValue(myModuleTagsSelector, { - value: result.tag.id, - label: result.tag.name, - params: { - color: result.tag.color - } - }, true); - }).fail(function() { - dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true); - }); - } - }, - onUnSelect: (id) => { - $.post(`${$(myModuleTagsSelector).data('update-module-tags-url')}/${id}/destroy_by_tag_id`) - .done(() => { - dropdownSelector.closeDropdown(myModuleTagsSelector); - }) - .fail(function(r) { - if (r.status === 403) { - HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger'); - } - }); - } - }).getContainer(myModuleTagsSelector).addClass('my-module-tags-container'); - } function initAssignedUsersSelector() { @@ -258,7 +178,6 @@ } initTaskCollapseState(); - initTagsSelector(); initStartDatePicker(); initDueDatePicker(); initAssignedUsersSelector(); diff --git a/app/assets/stylesheets/tailwind/tags.css b/app/assets/stylesheets/tailwind/tags.css index 373354a16..68fe7b6f5 100644 --- a/app/assets/stylesheets/tailwind/tags.css +++ b/app/assets/stylesheets/tailwind/tags.css @@ -1,9 +1,13 @@ @layer components { .sci-tag { - @apply text-xs !rounded-full !px-2 !py-1 inline-flex items-center gap-1; + @apply text-xs !rounded-full !pl-2 !pr-1.5 h-6 !py-0.5 inline-flex items-center gap-1; } .sci-tag .sn-icon { - @apply -ml-2; + @apply leading-4 !text-base cursor-pointer shrink-0; + } + + .sci-tag .sn-icon.sn-icon-close { + @apply cursor-pointer; } } diff --git a/app/controllers/concerns/taggable_actions.rb b/app/controllers/concerns/taggable_actions.rb new file mode 100644 index 000000000..343f21789 --- /dev/null +++ b/app/controllers/concerns/taggable_actions.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module TaggableActions + extend ActiveSupport::Concern + + included do + before_action :load_taggable_item, only: %i(link_tag unlink_tag) + before_action :load_tag, only: %i(link_tag unlink_tag) + end + + def link_tag + tagging = @taggable_item.taggings.new(tag: @tag, created_by: current_user) + if tagging.save + render json: { tag: [@tag.id, @tag.name, @tag.color] } + else + render json: { status: :error }, status: :unprocessable_entity + end + end + + def unlink_tag + tagging = @taggable_item.taggings.find_by(tag_id: @tag.id) + if tagging&.destroy + render json: { status: :ok } + else + render json: { status: :error }, status: :unprocessable_entity + end + end + + private + + def load_taggable_item + @taggable_item = controller_name.singularize.camelize.constantize.find(params[:id]) + end + + def load_tag + @tag = @taggable_item.team.tags.find_by(id: params[:tag_id]) + render_404 unless @tag + end +end diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index d4896548d..b9321d1f5 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -8,6 +8,7 @@ class MyModulesController < ApplicationController include MyModulesHelper include Breadcrumbs include FavoritesActions + include TaggableActions before_action :load_vars, except: %i(index restore_group create new save_table_state inventory_assigning_my_module_filter actions_toolbar) @@ -17,6 +18,7 @@ class MyModulesController < ApplicationController before_action :check_manage_permissions, only: %i( description due_date update_description update_protocol_description update_protocol ) + before_action :check_tag_manage_permissions, only: %i(link_tag unlink_tag) before_action :check_read_permissions, except: %i(create new update update_description inventory_assigning_my_module_filter update_protocol_description restore_group @@ -514,6 +516,10 @@ class MyModulesController < ApplicationController render_404 unless @my_module.my_module_status end + def check_tag_manage_permissions + render_403 && return unless can_manage_my_module_tags?(@my_module) + end + def set_inline_name_editing if action_name == 'index' return unless can_manage_experiment?(@experiment) diff --git a/app/javascript/vue/my_module/details.vue b/app/javascript/vue/my_module/details.vue index c7a8f0fd3..0e2a18c5e 100644 --- a/app/javascript/vue/my_module/details.vue +++ b/app/javascript/vue/my_module/details.vue @@ -128,7 +128,7 @@ {{ i18n.t('my_modules.details.completed_date') }} -
+
{{ i18n.t('my_modules.details.assigned_users') }} @@ -148,6 +148,15 @@
+
+ + + {{ i18n.t('my_modules.details.tags') }} + +
+ +
+
@@ -156,6 +165,7 @@ import GeneralDropdown from '../shared/general_dropdown.vue'; import DateTimePicker from '../shared/date_time_picker.vue'; import SelectDropdown from '../shared/select_dropdown.vue'; +import TagsInput from '../shared/tags_input.vue'; import axios from '../../packs/custom_axios.js'; import escapeHtml from '../shared/escape_html.js'; import { @@ -175,7 +185,8 @@ export default { components: { GeneralDropdown, DateTimePicker, - SelectDropdown + SelectDropdown, + TagsInput }, data() { return { diff --git a/app/javascript/vue/shared/tags_input.vue b/app/javascript/vue/shared/tags_input.vue new file mode 100644 index 000000000..8e31855b8 --- /dev/null +++ b/app/javascript/vue/shared/tags_input.vue @@ -0,0 +1,133 @@ + + + diff --git a/app/serializers/my_module_serializer.rb b/app/serializers/my_module_serializer.rb index edd827639..006dec253 100644 --- a/app/serializers/my_module_serializer.rb +++ b/app/serializers/my_module_serializer.rb @@ -6,7 +6,7 @@ class MyModuleSerializer < ActiveModel::Serializer include ApplicationHelper include ActionView::Helpers::TextHelper - attributes :name, :description, :permissions, :description_view, :urls, :last_modified_by_name, :created_at, :updated_at, + attributes :name, :description, :permissions, :description_view, :urls, :last_modified_by_name, :created_at, :updated_at, :tags, :team_id, :project_name, :experiment_name, :created_by_name, :is_creator_current_user, :code, :designated_user_ids, :due_date_cell, :start_date_cell, :completed_on def project_name @@ -17,6 +17,10 @@ class MyModuleSerializer < ActiveModel::Serializer object.experiment.name end + def team_id + object.team.id + end + def created_by_name object.created_by&.full_name end @@ -53,7 +57,9 @@ class MyModuleSerializer < ActiveModel::Serializer def urls { show_access: access_permissions_my_module_path(object), - show_user_group_assignments_access: show_user_group_assignments_access_permissions_my_module_path(object) + show_user_group_assignments_access: show_user_group_assignments_access_permissions_my_module_path(object), + link_tag: link_tag_my_module_path(object), + unlink_tag: unlink_tag_my_module_path(object) } end @@ -100,4 +106,10 @@ class MyModuleSerializer < ActiveModel::Serializer def description sanitize_input(object.tinymce_render('description')) end + + def tags + object.tags.map do |tag| + [tag.id, tag.name, tag.color] + end + end end diff --git a/app/views/my_modules/protocols.html.erb b/app/views/my_modules/protocols.html.erb index 1b728db67..9c8f9b748 100644 --- a/app/views/my_modules/protocols.html.erb +++ b/app/views/my_modules/protocols.html.erb @@ -97,30 +97,6 @@ <%= render partial: 'my_modules/repositories/consume_stock_modal'%> -
-
- - - -
@@ -137,8 +113,6 @@ <%= stylesheet_link_tag 'datatables' %> <%= javascript_include_tag "handsontable.full" %> <%= render partial: "shared/formulas_libraries" %> -<%= javascript_include_tag("my_modules/protocols") %> -<%= javascript_include_tag("my_modules/tags") %> <%= javascript_include_tag 'emoji_button' %> <%= javascript_include_tag("my_modules/repositories") %> <%= javascript_include_tag("my_modules/pwa_mobile_app") %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 710b11de9..456e11d51 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4236,6 +4236,9 @@ en: merge: "Merge" merge_success: "Tags merged successfully." merge_error: "There was an error merging tags." + tags_input: + add_tag: "Add tag" + edit_tags: "Edit tags" user_groups: promo: head_title: 'User groups' diff --git a/config/routes.rb b/config/routes.rb index bdc4e5f09..d04e8b4c9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -525,6 +525,8 @@ Rails.application.routes.draw do post :favorite post :unfavorite get :assigned_users + post :link_tag + post :unlink_tag end resources :user_my_modules, path: '/users', only: %i(index create destroy) do collection do