Class: Crm::TagsController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- Crm::TagsController
- Defined in:
- app/controllers/crm/tags_controller.rb
Overview
CRUD, merge, autocomplete, and taggable-browsing actions for Tag
records.
Constant Summary
Constants included from Controllers::ReferenceFindable
Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS
Constants included from Controllers::AnalyticsEvents
Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY
Constants included from Controllers::ErrorRendering
Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES
Instance Method Summary collapse
-
#autocomplete ⇒ void
GET /crm/tags/autocomplete Remote tag lookup for the TomSelect tag inputs (see TagsInput) — SQL-filtered and paginated so a form never embeds the full ~5k tag table.
-
#batch_create ⇒ void
POST /crm/tags/batch_create Creates multiple tags at once.
-
#cleanup_unused ⇒ void
POST /crm/tags/cleanup_unused Deletes all tags with no associated records.
-
#create ⇒ void
Creates a tag.
-
#destroy ⇒ void
Deletes the tag loaded by #set_tag, refusing when it still has associated records.
-
#edit ⇒ void
Loads the tag to edit via #set_tag.
-
#index ⇒ void
Lists tags with usage stats, Ransack-searchable and filterable by taggable type and public flag.
-
#merge_into ⇒ void
POST /crm/tags/:id/merge_into Merges the current tag into the target tag.
-
#new ⇒ void
Builds a new tag.
-
#show ⇒ void
Shows the tag loaded by #set_tag, with recent taggings grouped by type.
-
#taggables ⇒ void
GET /crm/tags/:id/taggables Returns paginated records of a specific taggable type for a tag.
-
#update ⇒ void
Updates the tag loaded by #set_tag.
Methods inherited from CrmController
#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen
Methods inherited from ApplicationController
#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y
Methods included from Controllers::ReturnPathHandling
#check_for_return_path, #redirect_to_return_path_or_default
Methods included from Controllers::AnalyticsEvents
#consume_queued_analytics_events, #registration_lead_type, #track_event
Methods included from Controllers::DeviceDetection
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
Methods included from Controllers::TrackingDetection
#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?
Methods included from Controllers::AcceleratedFileSending
#send_file_accelerated, #send_upload_accelerated
Methods included from Controllers::ErrorRendering
#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback
Methods included from Controllers::TurnstileVerification
#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!
Methods included from Controllers::CloudflareCaching
edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session
Methods included from Controllers::Webpackable
#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?
Methods included from Controllers::Localizable
#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale
Methods included from Controllers::Authenticable
#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak
Methods included from UrlsHelper
#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?
Instance Method Details
#autocomplete ⇒ void
This method returns an undefined value.
GET /crm/tags/autocomplete
Remote tag lookup for the TomSelect tag inputs (see TagsInput) — SQL-filtered
and paginated so a form never embeds the full ~5k tag table. Embedding them
made every other TomSelect on the page pay a full-document forced reflow
(~500ms each, ~8s per page load).
id is the tag NAME, not the record id: Taggable#tags= takes names, and
TomSelect submits the value field.
Params:
q - substring filter (case-insensitive)
taggable_type - optional; restrict to tags applied to that type (e.g. 'Image').
Omitted means "all tags", which is what the generic
as: :tags inputs want — a tag is valid on any record even
if it has never been applied to this type before.
exclude - comma-separated tag names to omit (e.g. popular tags already
offered as checkboxes)
page/per_page - pagination
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'app/controllers/crm/tags_controller.rb', line 168 def autocomplete query = params[:q].to_s.strip page = [params[:page].to_i, 1].max per_page = (params[:per_page].presence || 20).to_i.clamp(1, 100) excluded = params[:exclude].to_s.split(',').map { |t| t.strip.downcase }.compact_blank scope = Tag.ordered scope = scope.by_taggable_type(params[:taggable_type]) if params[:taggable_type].present? scope = scope.search(query) if query.present? scope = scope.where.not(name: excluded) if excluded.any? names = scope.offset((page - 1) * per_page).limit(per_page + 1).pluck(:name) more = names.size > per_page results = names.first(per_page).map { |name| { id: name, text: name } } # Synthetic CMS page tags are derived from PagesController.page_ids and may not # exist in `tags` yet, so they can't come from the query above. Surface them on # the first page only, and only when unscoped — a taggable_type scope asks for # "tags actually applied to this type", which these are not. # # Only while searching: on a blank preload-on-focus they'd be ~145 entries of # noise sitting ahead of the real tags, and a page tag is only useful once you # are actually looking for a page. if query.present? && page == 1 && params[:taggable_type].blank? results = page_tag_results(query, excluded, names) + results # Page tags occupy slots on this page, so honour the advertised page size # rather than returning up to 2 * per_page, and report the overflow. more ||= results.size > per_page results = results.first(per_page) end render json: { results: results, pagination: { more: more } } end |
#batch_create ⇒ void
This method returns an undefined value.
POST /crm/tags/batch_create
Creates multiple tags at once
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'app/controllers/crm/tags_controller.rb', line 206 def batch_create tag_names = params[:tag_names].to_s.split(/[,\n]/).map(&:strip).compact_blank.uniq created_count = 0 existing_count = 0 tag_names.each do |name| tag = Tag.find_or_create_by_name(name) if tag.previously_new_record? created_count += 1 else existing_count += 1 end end redirect_to , notice: "Created #{created_count} new tags. #{existing_count} already existed." end |
#cleanup_unused ⇒ void
This method returns an undefined value.
POST /crm/tags/cleanup_unused
Deletes all tags with no associated records
227 228 229 230 |
# File 'app/controllers/crm/tags_controller.rb', line 227 def cleanup_unused count = Tag.destroy_by(taggings_count: 0).size redirect_to , notice: "Deleted #{count} unused tags." end |
#create ⇒ void
This method returns an undefined value.
Creates a tag.
81 82 83 84 85 86 87 88 89 |
# File 'app/controllers/crm/tags_controller.rb', line 81 def create @tag = Tag.new(tag_params) if @tag.save redirect_to tag_path(@tag), notice: 'Tag was successfully created.' else render :new, status: :unprocessable_content end end |
#destroy ⇒ void
This method returns an undefined value.
Deletes the tag loaded by #set_tag, refusing when it still has
associated records.
106 107 108 109 110 111 112 113 |
# File 'app/controllers/crm/tags_controller.rb', line 106 def destroy if @tag.taggings_count.to_i.positive? redirect_to tag_path(@tag), alert: "Cannot delete tag with #{@tag.taggings_count} associated records. Merge it into another tag first." else @tag.destroy redirect_to , notice: 'Tag was successfully deleted.' end end |
#edit ⇒ void
This method returns an undefined value.
Loads the tag to edit via #set_tag.
76 |
# File 'app/controllers/crm/tags_controller.rb', line 76 def edit; end |
#index ⇒ void
This method returns an undefined value.
Lists tags with usage stats, Ransack-searchable and filterable by
taggable type and public flag.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'app/controllers/crm/tags_controller.rb', line 12 def index @q = Tag.ransack(params[:q]) @q.sorts = 'taggings_count DESC' if @q.sorts.blank? @tags = @q.result # Apply search if provided @tags = @tags.search(params[:search]) if params[:search].present? # Filter by taggable type @tags = @tags.by_taggable_type(params[:taggable_type]) if params[:taggable_type].present? # Filter by public flag @tags = @tags.where(public: true) if params[:public] == '1' @tags = @tags.where(public: false) if params[:public] == '0' # Stats @total_tags = Tag.count @public_tags = Tag..count @unused_tags = Tag.where(taggings_count: 0).count # Usage by type - count distinct tags per type (not taggings) @usage_by_type = Tagging.select(:taggable_type, :tag_id) .distinct .group(:taggable_type) .count(:tag_id) respond_to do |format| format.html do @pagy, @tags = pagy(@tags, items: 50) end format.json do @tags = @tags.limit(50) render json: @tags.map { |t| { id: t.id, name: t.name, slug: t.slug } } end format.csv do send_data generate_csv(@tags), filename: "tags-#{Date.current}.csv" end end end |
#merge_into ⇒ void
This method returns an undefined value.
POST /crm/tags/:id/merge_into
Merges the current tag into the target tag
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'app/controllers/crm/tags_controller.rb', line 119 def merge_into target_tag = Tag.find(params.expect(:target_tag_id)) if target_tag == @tag redirect_to tag_path(@tag), alert: 'Cannot merge a tag into itself.' return end # Move all taggings to the target tag count = @tag.taggings.count old_name = @tag.name Tag.transaction do Tagging.connection.execute(<<~SQL.squish) INSERT INTO taggings (tag_id, taggable_id, taggable_type, tagger_id, tagger_type, context, created_at) SELECT #{target_tag.id}, taggable_id, taggable_type, tagger_id, tagger_type, context, created_at FROM taggings WHERE tag_id = #{@tag.id} ON CONFLICT (taggable_id, taggable_type, tag_id) DO NOTHING SQL @tag.taggings.delete_all Tag.reset_counters(target_tag.id, :taggings) @tag.destroy! end redirect_to tag_path(target_tag), notice: "Successfully merged '#{old_name}' into '#{target_tag.name}'. #{count} records updated." end |
#new ⇒ void
This method returns an undefined value.
Builds a new tag.
69 70 71 |
# File 'app/controllers/crm/tags_controller.rb', line 69 def new @tag = Tag.new end |
#show ⇒ void
This method returns an undefined value.
Shows the tag loaded by #set_tag, with recent taggings grouped by
type.
56 57 58 59 60 61 62 63 64 |
# File 'app/controllers/crm/tags_controller.rb', line 56 def show @usage_by_type = @tag.usage_by_type # Get recent taggings grouped by type @recent_taggings = {} @tag.taggings.includes(:taggable).group_by(&:taggable_type).each do |type, taggings| @recent_taggings[type] = taggings.first(10).filter_map(&:taggable) end end |
#taggables ⇒ void
This method returns an undefined value.
GET /crm/tags/:id/taggables
Returns paginated records of a specific taggable type for a tag
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'app/controllers/crm/tags_controller.rb', line 236 def taggables type = params[:type] unless type.present? && Tagging::TAGGABLE_TYPES.include?(type) head :bad_request return end taggings = @tag.taggings.where(taggable_type: type).includes(:taggable) @pagy, @taggings = pagy(taggings, items: 20, page: params[:page]) @records = @taggings.filter_map(&:taggable) @type = type respond_to do |format| format.html { render partial: 'taggables_table', locals: { records: @records, type: @type, tag: @tag, pagy: @pagy } } format.json do render json: { records: @records.map { |r| taggable_to_json(r, type) }, pagination: { current_page: @pagy.page, total_pages: @pagy.pages, total_count: @pagy.count, has_more: @pagy.page < @pagy.pages } } end end end |
#update ⇒ void
This method returns an undefined value.
Updates the tag loaded by #set_tag.
94 95 96 97 98 99 100 |
# File 'app/controllers/crm/tags_controller.rb', line 94 def update if @tag.update(tag_params) redirect_to tag_path(@tag), notice: 'Tag was successfully updated.' else render :edit, status: :unprocessable_content end end |