diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 389808f5f..d3587aef0 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -157,7 +157,8 @@ class SearchController < ApplicationController def filter_records filter_datetime!(:created_at) if @filters[:created_at].present? filter_datetime!(:updated_at) if @filters[:updated_at].present? - filter_users! if @filters[:users].present? + filter_by_users! if @filters[:users].present? + filter_by_tags! if @filters[:tags].present? && @model == MyModule end def sort_records @@ -198,7 +199,7 @@ class SearchController < ApplicationController @records = @records.where("#{model_name}.#{attribute} <= ?", to_date) if to_date.present? end - def filter_users! + def filter_by_users! @records = @records.joins("INNER JOIN activities ON #{@model.model_name.collection}.id = activities.subject_id AND activities.subject_type= '#{@model.name}'") @@ -209,4 +210,9 @@ class SearchController < ApplicationController @records.where('activities.owner_id': user_ids) end end + + def filter_by_tags! + tag_ids = @filters[:tags]&.values&.collect(&:to_i) + @records = @records.joins(:tags).where(tags: { id: tag_ids }) + end end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 1b67ceeb1..495c07fb8 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -9,7 +9,7 @@ class TeamsController < ApplicationController before_action :load_vars, only: %i(sidebar export_projects export_projects_modal disable_tasks_sharing_modal shared_tasks_toggle) before_action :load_current_folder, only: :sidebar - before_action :check_read_permissions, except: %i(view_type visible_teams visible_users current_team_users) + before_action :check_read_permissions, except: %i(view_type visible_teams visible_users visible_tags current_team_users) before_action :check_export_projects_permissions, only: %i(export_projects_modal export_projects) def visible_teams @@ -26,6 +26,16 @@ class TeamsController < ApplicationController render json: users, each_serializer: UserSerializer, user: current_user end + def visible_tags + teams = if params[:teams].present? + current_user.teams.where(id: params[:teams]) + else + current_team + end + tags = Tag.where(team: teams) + render json: tags, each_serializer: TagSerializer + end + def current_team_users users = current_team.users.order(:full_name) render json: users, each_serializer: UserSerializer, user: current_user diff --git a/app/javascript/vue/global_search/container.vue b/app/javascript/vue/global_search/container.vue index 8d99c680a..e68c6150b 100644 --- a/app/javascript/vue/global_search/container.vue +++ b/app/javascript/vue/global_search/container.vue @@ -109,6 +109,7 @@ v-if="filterModalOpened" :teamsUrl="teamsUrl" :usersUrl="usersUrl" + :tagsUrl="tagsUrl" :filters="filters" :currentTeam="currentTeam" @search="applyFilters" @@ -157,6 +158,10 @@ export default { type: String, required: true }, + tagsUrl: { + type: String, + required: true + }, currentTeam: { type: Number || String, required: true @@ -213,7 +218,7 @@ export default { return Object.keys(this.filters).filter((key) => { if (key === 'created_at' || key === 'updated_at') { return this.filters[key].on || this.filters[key].from || this.filters[key].to; - } if (key === 'teams' || key === 'users') { + } if (key === 'teams' || key === 'users' || key === 'tags') { return this.filters[key].length > 0; } return this.filters[key]; @@ -242,6 +247,7 @@ export default { include_archived: urlParams.get('include_archived') === 'true', teams: (this.singleTeam ? [] : urlParams.getAll('teams[]').map((team) => parseInt(team, 10))), users: urlParams.getAll('users[]').map((user) => parseInt(user, 10)), + tags: urlParams.getAll('tags[]').map((tag) => parseInt(tag, 10)), group: urlParams.get('group') }; ['created_at', 'updated_at'].forEach((key) => { @@ -364,6 +370,7 @@ export default { include_archived: false, teams: [], users: [], + tags: [], group: null }; this.activeGroup = null; diff --git a/app/javascript/vue/global_search/filters.vue b/app/javascript/vue/global_search/filters.vue index bec1db43c..bc934e415 100644 --- a/app/javascript/vue/global_search/filters.vue +++ b/app/javascript/vue/global_search/filters.vue @@ -13,6 +13,20 @@ +
{{ i18n.t('search.filters.by_tag') }}
+
+ + +
{{ i18n.t('search.filters.by_created_date') }}
{ this.selectedUsers = this.filters.users; + this.selectedTags = this.filters.tags; }); this.includeArchived = this.filters.include_archived; this.activeGroup = this.filters.group; @@ -118,6 +138,8 @@ export default { selectedTeams() { this.selectedUsers = []; this.fetchUsers(); + this.selectedTags = []; + this.fetchTags(); } }, data() { @@ -135,9 +157,11 @@ export default { }, selectedTeams: [], selectedUsers: [], + selectedTags: [], includeArchived: true, teams: [], users: [], + tags:[], searchGroups: [ { value: 'FoldersComponent', label: this.i18n.t('search.index.folders') }, { value: 'ProjectsComponent', label: this.i18n.t('search.index.projects') }, @@ -161,9 +185,12 @@ export default { userRenderer(option) { return `
-
${option[1]}
+
${escapeHtml(option[1])}
`; }, + tagRenderer(option) { + return `
${escapeHtml(option[1])}
`; + }, setActiveGroup(group) { if (group === this.activeGroup) { this.activeGroup = null; @@ -183,6 +210,12 @@ export default { this.users = response.data.data.map((user) => ([parseInt(user.id, 10), user.attributes.name, { avatar_url: user.attributes.avatar_url }])); }); }, + fetchTags() { + axios.get(this.tagsUrl, { params: { teams: this.selectedTeams } }) + .then((response) => { + this.tags = response.data.data.map((tag) => ([parseInt(tag.id, 10), tag.attributes.name, tag.attributes.color])); + }); + }, selectTeams(teams) { if (Array.isArray(teams)) { this.selectedTeams = teams; @@ -193,6 +226,11 @@ export default { this.selectedUsers = users; } }, + selectTags(tags) { + if (Array.isArray(tags)) { + this.selectedTags = tags; + } + }, clearFilters() { this.createdAt = { on: null, @@ -220,6 +258,7 @@ export default { updated_at: this.updatedAt, teams: this.selectedTeams, users: this.selectedUsers, + tags: this.selectedTags, include_archived: this.includeArchived, group: this.activeGroup }); @@ -249,6 +288,10 @@ export default { searchParams.append('users[]', user); }); + this.selectedTags.forEach((tag) => { + searchParams.append('tags[]', tag.id); + }); + window.location.href = `${this.searchUrl}?${searchParams.toString()}`; } } diff --git a/app/javascript/vue/global_search/filters_modal.vue b/app/javascript/vue/global_search/filters_modal.vue index 1391486ff..b71e5f0f7 100644 --- a/app/javascript/vue/global_search/filters_modal.vue +++ b/app/javascript/vue/global_search/filters_modal.vue @@ -18,6 +18,7 @@ - - ${tag[1]} + + ${escapeHtml(tag[1])} `; }, usersRenderer(user) { diff --git a/app/javascript/vue/navigation/quick_search.vue b/app/javascript/vue/navigation/quick_search.vue index d0b83a74e..e5d98241b 100644 --- a/app/javascript/vue/navigation/quick_search.vue +++ b/app/javascript/vue/navigation/quick_search.vue @@ -29,6 +29,7 @@ v-if="filtersOpened" :teamsUrl="teamsUrl" :usersUrl="usersUrl" + :tagsUrl="tagsUrl" :currentTeam="currentTeam" :searchUrl="searchUrl" :searchQuery="searchQuery" @@ -150,6 +151,10 @@ export default { usersUrl: { type: String, required: true + }, + tagsUrl: { + type: String, + required: true } }, components: { diff --git a/app/javascript/vue/navigation/top_menu.vue b/app/javascript/vue/navigation/top_menu.vue index 74162b052..8caca65af 100644 --- a/app/javascript/vue/navigation/top_menu.vue +++ b/app/javascript/vue/navigation/top_menu.vue @@ -22,6 +22,7 @@ :currentTeam="currentTeam" :teamsUrl="teamsUrl" :usersUrl="usersUrl" + :tagsUrl="tagsUrl" > ${escapeHtml(tag[1])}`; + return `
${escapeHtml(tag[1])}
`; } } }; diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 9e2951f9c..ba5bc38ee 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -116,7 +116,7 @@ class MyModule < ApplicationRecord ) teams = options[:teams] || current_team || user.teams.select(:id) - new_query = distinct.left_joins(:task_comments, my_module_tags: :tag, user_my_modules: :user) + new_query = distinct.left_joins(:task_comments, :tags, user_my_modules: :user) .readable_by_user(user, teams) .where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options) diff --git a/app/serializers/tag_serializer.rb b/app/serializers/tag_serializer.rb index f3049e842..41d193775 100644 --- a/app/serializers/tag_serializer.rb +++ b/app/serializers/tag_serializer.rb @@ -7,7 +7,7 @@ class TagSerializer < ActiveModel::Serializer def urls { - update: project_tag_path(object.project, object, format: :json) + update: users_settings_team_tag_path(object.team, object, format: :json) } end end diff --git a/app/views/search/index.html.erb b/app/views/search/index.html.erb index 4b1d7d56a..da9371a19 100644 --- a/app/views/search/index.html.erb +++ b/app/views/search/index.html.erb @@ -6,6 +6,7 @@ :search-url="'<%= search_path(format: :json) %>'" teams-url="<%= visible_teams_teams_path %>" users-url="<%= visible_users_teams_path %>" + tags-url="<%= visible_tags_teams_path %>" :single-team="<%= current_user.teams.count == 1 %>" :current-team="<%= current_team.id %>" /> diff --git a/app/views/shared/navigation/_top.html.erb b/app/views/shared/navigation/_top.html.erb index 2445e4572..88b730466 100644 --- a/app/views/shared/navigation/_top.html.erb +++ b/app/views/shared/navigation/_top.html.erb @@ -8,6 +8,7 @@ unseen-notifications-url="<%= unseen_counter_user_notifications_path %>" teams-url="<%= visible_teams_teams_path %>" users-url="<%= visible_users_teams_path %>" + tags-url="<%= visible_tags_teams_path %>" logo-url="<%= application_logo_url %>" /> diff --git a/config/locales/en.yml b/config/locales/en.yml index 710b11de9..8edc7a08f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -494,6 +494,8 @@ en: title: "More search options" sub_title: "Refine your search by applying the filters below." by_type: "Filter by type" + by_tag: "Filter by tag" + by_tag_placeholder: "Select tags" by_created_date: "Filter by created date" by_updated_date: "Filter by updated date" by_team: "Filter by workspace" diff --git a/db/migrate/20250818111931_migrate_tags_from_projects_to_workspaces.rb b/db/migrate/20250818111931_migrate_tags_from_projects_to_workspaces.rb index 544e35b4f..d246fffb7 100644 --- a/db/migrate/20250818111931_migrate_tags_from_projects_to_workspaces.rb +++ b/db/migrate/20250818111931_migrate_tags_from_projects_to_workspaces.rb @@ -17,6 +17,10 @@ class MigrateTagsFromProjectsToWorkspaces < ActiveRecord::Migration[7.2] FROM my_module_tags SQL + execute <<~SQL.squish + SELECT setval('taggings_id_seq', (SELECT MAX(id) FROM taggings)); + SQL + execute <<~SQL.squish UPDATE tags SET team_id = projects.team_id