diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..bbae52abf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,104 @@ +## VERSION 2.4 + +Version 2.4 is packed with some great new stuff to make your Helpy experience better than ever. It also includes some important security updates +to the underlying software running Helpy and its recommended your update as soon as possible. + +Security Updates: + +- Rails has been updated to 4.2.11.1 +- Devise has been updated to 4.6.1 +- Many other dependencies have been updated + +New features, improvements and fixes: +- New: tag manager for controlling tags through the admin settings. +- New: Tag picker on the agent ticket view +- New: Quick KB search when creating or responding to tickets to add links to articles +- New: Autosave for ticket replies and knowledgebase article editor +- New: A number of new settings have been added to customize how Helpy works. +- Fixed: support email addresses are now removed from the CC field automatically +- Fixed: Flash wrapper width reduced @cr0vy +- Fixed: Widget mixed content issue with Google Fonts @karser +- Optimizations: A number of optimizations have been made to improve performance +- Update: Email parsing has been improved, particularly for non English email +- Update: Onboarding has been moved to the unlogged-in state. This only affects new installs +- Docker: Uploads folder made writable @sarke + +## VERSION 2.3 + +This release includes a new theme contributed by the team at Seravo called "Nordic" (thanks @ottok, @elguitar, @simoke, @tlxo and anyone else I missed), along with a number of dependency updates, bug fixes, and improvements to the docker container. In addition ENV vars were added for remote file storage and database as a service (docker only) that should make it easier to work with Elastic Beanstalk/Kubernetes. + +Full list of improvements and fixes: +- Dependency updates +- Fix a bug which disabled validations for associated fields +- Prevent a 500 when a topic is missing a user_id (direct result of missing validation above) +- Resolved a lot of intermittent tests +- New theme: Nordic +- Enable clicking outside keyboard shortcuts modal to close +- Conditional support for S3 compatible remote filestore using fog gem +- Updates to Docker container from @ypcs and adds + + +## VERSION 2.2 + +This release includes fixes to several serious vulnerabilities including: + +[CVE-2018-18886](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-18886). This fixes a serious XSS vulnerability (Reported by @joanbono). This was fixed in the master branch several weeks ago, but if you are running a prior version, including 1.x releases, you should upgrade to `2.2.0` as soon as possible. + +Upgrades Rails to 4.2.11. This includes a fix to a significant security vulnerability in ActiveJob. + +Other improvements in this release include: + +- Bring dependencies up to date +- Improved support for forwarded emails +- Accept emails from users who use a number in the first part of their email or configured email name +- Correctly handle emails with no subject +- Add support for IMAP email +- Prevent agents from accessing API +- Harden agents ability to edit administrators +- Rename Login to Sign in +- Allow new users when admin creating an internal note + + +## VERSION 2.1 + +This release builds on the awesomeness of version 2 by adding several new enhancements- + +- Editable header and footer for html ticket email to customers. +- support for merge tokens (%customer_name% and %customer_email%) with more coming soon. +- Ability to create a ticket with a note as the first post (useful for calls, walk ins, etc) +- Refactor of settings backend and addition of ability to test smtp settings +- Restrict API access from agents + +Upgrading: + +Make sure you run `bundle exec rake db:migrate` and also `bundle exec rake update:enable_templates` to turn on the templates feature. + +## VERSION 2.0 + +Version 2 includes a number of awesome improvements, listed below. This should be a fairly straightforward update for most people, make sure you: + +`bundle install` +`bundle exec rake db:migrate` + +We have a live demo at https://demo.helpy.io/ The admin username is "admin@test.com" and admin password is "12345678" + +Updated/New Features: + +- Refreshed Admin UI +- New Helpcenter theme: Singular +- HTML support when responding to tickets +- Nicer HTML alert emails +- Nicer HTML responses to customers +- HTML emails now include the full ticket history +- UI for replying to tickets re-imagined +- Inline customer editing +- Channel and source reporting +- New support for emoji's in ticket replies +- Customize the colors of the admin UI +- Ability to email customers from the create ticket dialogue +- New internal ticket type +- Set all ticket params from admin create ticket UI +- Font Awesome 5 iconography +- Improved support for CC and BCC recipients +- Import/Export data in CSV +- Comply with GDPR by deleting or anonymizing users \ No newline at end of file diff --git a/Gemfile b/Gemfile index 633f8f581..e31532bd5 100644 --- a/Gemfile +++ b/Gemfile @@ -120,6 +120,7 @@ gem 'config', '~> 1.1.0' gem 'daemons' gem 'mailman'#, require: false gem 'mail_extract' +gem 'email_reply_trimmer' gem 'griddler' gem 'griddler-mandrill' @@ -148,14 +149,14 @@ gem 'rails-timeago' gem 'faker' gem 'timecop' #used to populate - +gem "hashid-rails", "~> 1.0" gem 'themes_on_rails' gem "recaptcha", '< 3', require: "recaptcha/rails" # TODO: Update gem 'best_in_place', '~> 3.1' # Add onboarding component -gem 'helpy_onboarding', path: 'vendor/helpy_onboarding' +gem 'helpy_onboarding', git: 'https://github.com/helpyio/helpy_onboarding', branch: 'master' gem 'helpy_imap', git: 'https://github.com/helpyio/helpy_imap', branch: 'master' group :development, :test do @@ -180,7 +181,8 @@ group :development do gem "better_errors" # Check Eager Loading / N+1 query problems - gem 'bullet' + # gem 'bullet' + gem 'scout_apm' # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '~> 3.3' diff --git a/Gemfile.lock b/Gemfile.lock index 6736c06c4..5692861c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,10 +9,12 @@ GIT mailman rails (~> 4.2.10) -PATH - remote: vendor/helpy_onboarding +GIT + remote: https://github.com/helpyio/helpy_onboarding + revision: cc742c8e48476003757ceda17c825c80e36ca694 + branch: master specs: - helpy_onboarding (1.0) + helpy_onboarding (2.0) deface rails (~> 4.2.7) @@ -96,9 +98,6 @@ GEM builder (3.2.3) bulk_insert (1.7.0) activerecord (>= 3.2.0) - bullet (5.9.0) - activesupport (>= 3.0.0) - uniform_notifier (~> 1.11) bundler-audit (0.6.0) bundler (~> 1.2) thor (~> 0.18) @@ -179,6 +178,7 @@ GEM docile (1.3.1) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) + email_reply_trimmer (0.1.12) equalizer (0.0.11) erubi (1.8.0) erubis (2.7.0) @@ -267,7 +267,11 @@ GEM mail groupdate (4.1.0) activesupport (>= 4.2) - hashie (3.5.7) + hashid-rails (1.2.2) + activerecord (>= 4.0) + hashids (~> 1.0) + hashids (1.0.5) + hashie (3.6.0) hitimes (1.3.0) htmlentities (4.3.4) http-cookie (1.0.3) @@ -381,8 +385,8 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - omniauth (1.8.1) - hashie (>= 3.4.6, < 3.6.0) + omniauth (1.9.0) + hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) omniauth-facebook (5.0.0) omniauth-oauth2 (~> 1.2) @@ -519,6 +523,7 @@ GEM sassc (2.0.1) ffi (~> 1.9) rake + scout_apm (2.4.21) scss-lint (0.38.0) rainbow (~> 2.0) sass (~> 3.4.1) @@ -585,7 +590,6 @@ GEM unicorn (5.5.0) kgio (~> 2.6) raindrops (~> 0.7) - uniform_notifier (1.12.1) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) @@ -617,7 +621,6 @@ DEPENDENCIES bootstrap_form brakeman bulk_insert - bullet bundler-audit byebug capybara (< 3.0) @@ -635,6 +638,7 @@ DEPENDENCIES devise-bootstrap-views devise-i18n devise_invitable + email_reply_trimmer factory_bot_rails faker fog-aws @@ -658,6 +662,7 @@ DEPENDENCIES griddler-sendgrid griddler-sparkpost groupdate + hashid-rails (~> 1.0) helpy_imap! helpy_onboarding! http_accept_language @@ -704,6 +709,7 @@ DEPENDENCIES route_translator rubocop sass-rails (~> 5.0.7) + scout_apm scss-lint sdoc (~> 1.0.0) selectize-rails diff --git a/app/assets/javascripts/agent_assistant.js b/app/assets/javascripts/agent_assistant.js new file mode 100644 index 000000000..104a53ffc --- /dev/null +++ b/app/assets/javascripts/agent_assistant.js @@ -0,0 +1,36 @@ +$(document).ready(function () { + + $(".agent-assist").autocomplete({ + source: function (request, response) { + jQuery.get("/admin/agent_assistant.json", { + query: request.term + }, function (data) { + response(data); + }); + }, + minLength: 3, + appendTo: $('assist-results'), + focus: function (event, ui) { + event.preventDefault(); + $(this).val(ui.item.name); + }, + select: function (event, ui) { + event.preventDefault(); + // set value of summernote with existing value + common reply + var link = "" + ui.item.name + ""; + $('#post_body').summernote('code', $('#post_body').summernote('code') + link); + $('#topic_post_body').summernote('code', $('#topic_post_body').summernote('code') + link); + $('.assist-results').html('').fadeOut(); + $(".agent-assist").val(''); + return false; + }, + messages: { + noResults: '', + results: function () { } + } + + }); + + + +}); diff --git a/app/assets/javascripts/app.js b/app/assets/javascripts/app.js index aaab2ef4f..2959386a1 100644 --- a/app/assets/javascripts/app.js +++ b/app/assets/javascripts/app.js @@ -459,6 +459,9 @@ Helpy.didthisHelp = function(yesno){ Helpy.showGroup = function() { if ($('#topic_private_true').is(':checked')) { $('#topic_team_list').parent().removeClass('hidden'); + $("#topic_forum_id").parent().hide(); + $('#new_topic').append(""); + $('#topic_team_list').removeClass('hidden'); } else if ($('#topic_private_false').is(':checked')) { $('#topic_team_list').parent().addClass('hidden'); } else { diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c01320498..ba0bcfde5 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -44,6 +44,7 @@ //= require bootstrap/dropdown //= require Chart.bundle //= require chartkick +//= require sisyphus.min.js // Jtruncate plugin, http://www.jeremymartin.name/projects.php?project=jTruncate // modified by Scott Miller- remove animation, newline for more link diff --git a/app/assets/javascripts/sisyphus.min.js b/app/assets/javascripts/sisyphus.min.js new file mode 100755 index 000000000..d7694e53e --- /dev/null +++ b/app/assets/javascripts/sisyphus.min.js @@ -0,0 +1,3 @@ +// jshint ignore: start +!function(a){function b(a){return"[id="+a.attr("id")+"][name="+a.attr("name")+"]"}a.fn.sisyphus=function(c){var d=a.map(this,function(c){return b(a(c))}).join(),e=Sisyphus.getInstance(d);return e.protect(this,c),e};var c={};c.isAvailable=function(){if("object"==typeof a.jStorage)return!0;try{return localStorage.getItem}catch(b){return!1}},c.set=function(b,c){if("object"==typeof a.jStorage)a.jStorage.set(b,c+"");else try{localStorage.setItem(b,c+"")}catch(d){}},c.get=function(b){if("object"==typeof a.jStorage){var c=a.jStorage.get(b);return c?c.toString():c}return localStorage.getItem(b)},c.remove=function(b){"object"==typeof a.jStorage?a.jStorage.deleteKey(b):localStorage.removeItem(b)},Sisyphus=function(){function f(){return{setInstanceIdentifier:function(a){this.identifier=a},getInstanceIdentifier:function(){return this.identifier},setInitialOptions:function(b){var d={excludeFields:[],customKeySuffix:"",locationBased:!1,timeout:0,autoRelease:!0,onBeforeSave:function(){},onSave:function(){},onBeforeRestore:function(){},onRestore:function(){},onRelease:function(){}};this.options=this.options||a.extend(d,b),this.browserStorage=c},setOptions:function(b){this.options=this.options||this.setInitialOptions(b),this.options=a.extend(this.options,b)},protect:function(b,c){this.setOptions(c),b=b||{};var f=this;if(this.targets=this.targets||[],f.options.name?this.href=f.options.name:this.href=location.hostname+location.pathname+location.search+location.hash,this.targets=a.merge(this.targets,b),this.targets=a.unique(this.targets),this.targets=a(this.targets),!this.browserStorage.isAvailable())return!1;var g=f.options.onBeforeRestore.call(f);if((void 0===g||g)&&f.restoreAllData(),this.options.autoRelease&&f.bindReleaseData(),!d.started[this.getInstanceIdentifier()])if(f.isCKEditorPresent())var h=setInterval(function(){e.isLoaded&&(clearInterval(h),f.bindSaveData(),d.started[f.getInstanceIdentifier()]=!0)},100);else f.bindSaveData(),d.started[f.getInstanceIdentifier()]=!0},isCKEditorPresent:function(){return this.isCKEditorExists()?(e.isLoaded=!1,e.on("instanceReady",function(){e.isLoaded=!0}),!0):!1},isCKEditorExists:function(){return"undefined"!=typeof e},findFieldsToProtect:function(a){return a.find(":input").not(":submit").not(":reset").not(":button").not(":file").not(":password").not(":disabled").not("[readonly]")},bindSaveData:function(){var c=this;c.options.timeout&&c.saveDataByTimeout(),c.targets.each(function(){var d=b(a(this));c.findFieldsToProtect(a(this)).each(function(){if(-1!==a.inArray(this,c.options.excludeFields))return!0;var e=a(this),f=(c.options.locationBased?c.href:"")+d+b(e)+c.options.customKeySuffix;(e.is(":text")||e.is("textarea"))&&(c.options.timeout||c.bindSaveDataImmediately(e,f)),c.bindSaveDataOnChange(e)})})},saveAllData:function(){var c=this;c.targets.each(function(){var d=b(a(this)),f={};c.findFieldsToProtect(a(this)).each(function(){var g=a(this);if(-1!==a.inArray(this,c.options.excludeFields)||void 0===g.attr("name")&&void 0===g.attr("id"))return!0;var h=(c.options.locationBased?c.href:"")+d+b(g)+c.options.customKeySuffix,i=g.val();if(g.is(":checkbox")){var j=g.attr("name");if(void 0!==j&&-1!==j.indexOf("[")){if(f[j]===!0)return;i=[],a("[name='"+j+"']:checked").each(function(){i.push(a(this).val())}),f[j]=!0}else i=g.is(":checked");c.saveToBrowserStorage(h,i,!1)}else if(g.is(":radio"))g.is(":checked")&&(i=g.val(),c.saveToBrowserStorage(h,i,!1));else if(c.isCKEditorExists()){var k=e.instances[g.attr("name")]||e.instances[g.attr("id")];k?(k.updateElement(),c.saveToBrowserStorage(h,g.val(),!1)):c.saveToBrowserStorage(h,i,!1)}else c.saveToBrowserStorage(h,i,!1)})}),c.options.onSave.call(c)},restoreAllData:function(){var c=this,d=!1;c.targets.each(function(){var e=a(this),f=b(a(this));c.findFieldsToProtect(e).each(function(){if(-1!==a.inArray(this,c.options.excludeFields))return!0;var e=a(this),g=(c.options.locationBased?c.href:"")+f+b(e)+c.options.customKeySuffix,h=c.browserStorage.get(g);null!==h&&(c.restoreFieldsData(e,h),d=!0)})}),d&&c.options.onRestore.call(c)},restoreFieldsData:function(a,b){if(void 0===a.attr("name")&&void 0===a.attr("id"))return!1;var c=a.attr("name");!a.is(":checkbox")||"false"===b||void 0!==c&&-1!==c.indexOf("[")?!a.is(":checkbox")||"false"!==b||void 0!==c&&-1!==c.indexOf("[")?a.is(":radio")?a.val()===b&&a.prop("checked",!0):void 0===c||-1===c.indexOf("[")?a.val(b):(b=b.split(","),a.val(b)):a.prop("checked",!1):a.prop("checked",!0)},bindSaveDataImmediately:function(a,b){var c=this;if("onpropertychange"in a?a.get(0).onpropertychange=function(){c.saveToBrowserStorage(b,a.val())}:a.get(0).oninput=function(){c.saveToBrowserStorage(b,a.val())},this.isCKEditorExists()){var d=e.instances[a.attr("name")]||e.instances[a.attr("id")];d&&d.document.on("keyup",function(){d.updateElement(),c.saveToBrowserStorage(b,a.val())})}},saveToBrowserStorage:function(a,b,c){var d=this,e=d.options.onBeforeSave.call(d);(void 0===e||e!==!1)&&(c=void 0===c?!0:c,this.browserStorage.set(a,b),c&&""!==b&&this.options.onSave.call(this))},bindSaveDataOnChange:function(a){var b=this;a.change(function(){b.saveAllData()})},saveDataByTimeout:function(){var a=this,b=a.targets;setTimeout(function(){function b(){a.saveAllData(),setTimeout(b,1e3*a.options.timeout)}return b}(b),1e3*a.options.timeout)},bindReleaseData:function(){var c=this;c.targets.each(function(){var d=a(this),e=b(d);a(this).bind("submit reset",function(){c.releaseData(e,c.findFieldsToProtect(d))})})},manuallyReleaseData:function(){var c=this;c.targets.each(function(){var d=a(this),e=b(d);c.releaseData(e,c.findFieldsToProtect(d))})},releaseData:function(c,e){var f=!1,g=this;d.started[g.getInstanceIdentifier()]=!1,e.each(function(){if(-1!==a.inArray(this,g.options.excludeFields))return!0;var d=a(this),e=(g.options.locationBased?g.href:"")+c+b(d)+g.options.customKeySuffix;g.browserStorage.remove(e),f=!0}),f&&g.options.onRelease.call(g)}}}var d={instantiated:[],started:[]},e=window.CKEDITOR;return{getInstance:function(a){return d.instantiated[a]||(d.instantiated[a]=f(),d.instantiated[a].setInstanceIdentifier(a),d.instantiated[a].setInitialOptions()),a?d.instantiated[a]:d.instantiated[a]},free:function(){return d={instantiated:[],started:[]},null},version:"1.1.3"}}()}(jQuery); +// jshint ignore: end \ No newline at end of file diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 703e6af13..43802a478 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -237,6 +237,11 @@ ul.settings-menu { margin-top: 20px; } +.label-light, +.btn-light { + background-color: #aaaaaa; +} + #user-info-horizontal { padding-top: 30px; } diff --git a/app/controllers/admin/agent_assistant_controller.rb b/app/controllers/admin/agent_assistant_controller.rb new file mode 100644 index 000000000..0bfff4c24 --- /dev/null +++ b/app/controllers/admin/agent_assistant_controller.rb @@ -0,0 +1,36 @@ +class Admin::AgentAssistantController < Admin::BaseController + + def index + depth = params[:depth].present? ? params[:depth] : 10 + @results = Doc.active.publicly.agent_assist(params[:query]).first(depth) + respond_to do |format| + format.json { + render json: serialize_autocomplete_result(@results).to_json.html_safe + } + end + end + + private + + def serialize_autocomplete_result(results) + serialized_result = [] + results.each do |result| + serialized_result << { + name: CGI::escapeHTML(result.title), + content: result.meta_description.present? ? meta_content(result) : sanitized_content(result), + link: category_doc_url(result.category_id, Doc.find(result.id)) + } + end + serialized_result + end + + def sanitized_content(result) + return nil if result.body.nil? + ActionView::Base.full_sanitizer.sanitize(result.body).truncate_words(20) + end + + def meta_content(result) + result.meta_description + end + +end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index d7d71f3f8..c625e7db8 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -86,7 +86,9 @@ def get_tickets_by_status else topics_raw = params[:team].present? ? Topic.all.tagged_with(params[:team], any: true) : Topic end - topics_raw = topics_raw.includes(user: :avatar_files).chronologic + + # Only include cloudinary files if enabled + topics_raw = cloudinary_enabled? ? topics_raw.includes(user: :avatar_files).chronologic : topics_raw.includes(:user).chronologic get_all_teams @@ -125,9 +127,9 @@ def fetch_counts end def set_categories_and_non_featured - @public_categories = Category.publicly.featured.ordered - @public_nonfeatured_categories = Category.publicly.unfeatured.alpha - @internal_categories = Category.only_internally.ordered + @public_categories = Category.publicly.featured.ordered.includes(:docs) + @public_nonfeatured_categories = Category.publicly.unfeatured.alpha.includes(:docs) + @internal_categories = Category.only_internally.ordered.includes(:docs) end end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index fb127554b..e1bcb513e 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -49,7 +49,7 @@ def create def destroy @team = ActsAsTaggableOn::Tag.find(params[:id]) - @team.taggings.destroy_all if @team.taggings.present? + @team.taggings.destroy_all if @team.taggings.exists? @team.destroy redirect_to admin_groups_path end diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb new file mode 100644 index 000000000..a1211f816 --- /dev/null +++ b/app/controllers/admin/tags_controller.rb @@ -0,0 +1,80 @@ +# == Schema Information +# +# Table name: tags +# +# id :integer not null, primary key +# name :string# + +class Admin::TagsController < Admin::BaseController + + before_action :verify_admin + + layout 'admin-settings' + + def index + @tag = ActsAsTaggableOn::Tag.new + tag_ids = ActsAsTaggableOn::Tagging.all.where(context: "tags", taggable_type: "Topic").includes(:tag).map{|tagging| tagging.tag.id }.uniq + @tags = ActsAsTaggableOn::Tag.where("id IN (?)", tag_ids) + end + + def new + @tag = ActsAsTaggableOn::Tag.new + end + + def edit + @tag = ActsAsTaggableOn::Tag.find(params[:id]) + end + + def update + @tag = ActsAsTaggableOn::Tag.find(params[:id]) + if @tag.update_attributes(tag_params) + redirect_to admin_tags_path + else + render :edit + end + end + + def create + @tag = ActsAsTaggableOn::Tag.new(tag_params) + if @tag.save && ActsAsTaggableOn::Tagging.create(tag_id: @tag.id, taggable_type: 'Topic', context: "tags") + flash[:notice] = "Tag Saved" + respond_to do |format| + format.html { + redirect_to admin_tags_path + } + format.js {} + end + else + respond_to do |format| + format.html { + render :new + } + format.js { + } + end + end + end + + def destroy + @tag = ActsAsTaggableOn::Tag.find(params[:id]) + @tag.taggings.destroy_all if @tag.taggings.present? + @tag.destroy + flash[:notice] = "Tag has been deleted" + respond_to do |format| + format.html { + redirect_to admin_tags_path + } + format.js {} + end + end + + private + + def tag_params + params.require(:acts_as_taggable_on_tag).permit( + :name, + :description, + :color + ) + end +end diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb index 509e55366..7056b503a 100644 --- a/app/controllers/admin/topics_controller.rb +++ b/app/controllers/admin/topics_controller.rb @@ -30,6 +30,7 @@ class Admin::TopicsController < Admin::BaseController before_action :fetch_counts, only: ['index','show', 'update_topic', 'user_profile'] before_action :remote_search, only: ['index', 'show', 'update_topic'] before_action :get_all_teams, except: ['shortcuts'] + before_action :set_hash_id_salt respond_to :js, :html, only: :show respond_to :js @@ -43,7 +44,7 @@ def index def show get_tickets_by_status - @topic = Topic.where(id: params[:id]).first + @topic = Topic.find(params[:id]) @doc = Doc.find(@topic.doc_id) if @topic.doc_id.present? && @topic.doc_id != 0 if check_current_user_is_allowed? @topic @@ -53,7 +54,7 @@ def show # @topic.open # end get_all_teams - @posts = @topic.posts.chronologic.includes(:user) + @posts = @topic.posts.chronologic.includes(:user, :topic) tracker("Agent: #{current_user.name}", "Viewed Ticket", @topic.to_param, @topic.id) fetch_counts @include_tickets = false @@ -66,7 +67,7 @@ def show def new fetch_counts - @topic = Topic.new + @topic = Topic.new(channel: AppSettings['settings.default_channel']) @user = params[:user_id].present? ? User.find(params[:user_id]) : User.new end @@ -290,13 +291,10 @@ def update def update_tags @topic = Topic.find(params[:id]) - - if @topic.update_attributes(topic_params) - @posts = @topic.posts.chronologic - - fetch_counts - get_all_teams - get_tickets_by_status + previous_tagging = @topic.tag_list + @topic.tag_list = params[:topic][:tag_list] + if @topic.save && previous_tagging != @topic.tag_list + # if @topic.update(tag_list: params[:tag][:tag_list]) @topic.posts.create( @@ -306,16 +304,21 @@ def update_tags ) flash[:notice] = t('tagged_with', topic_id: @topic.id, tagged_with: @topic.tag_list) - respond_to do |format| - format.html { - redirect_to admin_topic_path(@topic) - } - format.js { - render 'update_ticket', id: @topic.id - } - end - else - logger.info("error") + end + + @posts = @topic.posts.chronologic + + fetch_counts + get_all_teams + get_tickets_by_status + + respond_to do |format| + format.html { + redirect_to admin_topic_path(@topic) + } + format.js { + render 'update_ticket', id: @topic.id + } end end @@ -616,4 +619,8 @@ def topic_params ) end + def set_hash_id_salt + Hashid::Rails.configuration.salt=AppSettings['settings.anonymous_salt'] + end + end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 80e53ae86..649d7a738 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -58,9 +58,9 @@ def index @roles = [[t('team'), 'team'], [t(:admin_role), 'admin'], [t(:agent_role), 'agent'], [t(:editor_role), 'editor'], [t(:user_role), 'user']] if params[:role].present? if params[:role] == 'team' - @users = User.team.alpha.all.page params[:page] + @users = User.team.includes(:topics, :teams).alpha.all.page params[:page] else - @users = User.by_role(params[:role]).active_first.all.page params[:page] + @users = User.by_role(params[:role]).includes(:topics, teams: :tags).active_first.all.page params[:page] end else @users = User.active_first.all.page params[:page] diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 4454bc487..ed70c1179 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -1,3 +1,4 @@ + # == Schema Information # # Table name: topics @@ -35,6 +36,7 @@ class TopicsController < ApplicationController before_action :topic_creation_enabled?, only: ['new', 'create'] before_action :get_all_teams, only: 'new' before_action :get_public_forums, only: ['new', 'create'] + before_action :check_anonymous_ticket_access, only: :show layout "clean", only: [:new, :index, :thanks] theme :theme_chosen @@ -84,6 +86,18 @@ def ticket end end + def show + @topic = Topic.undeleted.external.find_by_hashid(params[:id]) + redirect_to root_path unless @topic + + if @topic.present? + @posts = @topic.posts.ispublic.chronologic.active.all.includes(:topic, :user, :screenshot_files) + @page_title = "##{@topic.id} #{@topic.name}" + add_breadcrumb t(:tickets, default: 'Tickets'), tickets_path + add_breadcrumb @page_title + end + end + def new initialize_new_ticket_form_vars end @@ -166,7 +180,7 @@ def associate_with_doc private def initialize_new_ticket_form_vars - @topic = Topic.new #unless @topic + @topic = Topic.new(private: AppSettings['settings.default_private']) #unless @topic @user = @topic.build_user unless user_signed_in? @topic.posts.build #unless @topic.posts get_all_teams @@ -188,4 +202,11 @@ def get_public_forums @forums = Forum.ispublic.all end + def check_anonymous_ticket_access + unless AppSettings['settings.anonymous_access'] == '1' + redirect_to root_path + end + Hashid::Rails.configuration.salt=AppSettings['settings.anonymous_salt'] + end + end diff --git a/app/helpers/admin/topics_helper.rb b/app/helpers/admin/topics_helper.rb index 54151aa82..ef55ff860 100644 --- a/app/helpers/admin/topics_helper.rb +++ b/app/helpers/admin/topics_helper.rb @@ -17,13 +17,22 @@ def started_by(topic) end def topic_added_from - # <%= "#{@topic.kind} added from #{@topic.channel}" %><%= " on #{link_to(@doc.title, edit_admin_category_doc_path(@doc.category_id, @doc.id, lang: I18n.locale))}".html_safe if @doc.present? %> - content_tag :small, class: 'less-important' do + content_tag :small, class: 'less-important hidden-xs' do concat t(:topic_added_from, kind: @topic.kind, channel: @topic.channel) concat ": #{link_to(@doc.title, edit_admin_category_doc_path(@doc.category_id, @doc.id, lang: I18n.locale))}".html_safe if @doc.present? end end + def topic_anonymous_link + content_tag :div, class: 'pull-right' do + content_tag :small, class: 'less-important anonymous-link' do + link_to topic_path(id: @topic.hashid), target: 'blank' do + content_tag :i, nil, class: 'fas fa-link' + end + end + end if AppSettings['settings.anonymous_access'] == "1" + end + def ticket_status_label content_tag :span, class: "label #{status_class(@status)}", style: 'text-transform: uppercase' do status_label(@status) @@ -78,10 +87,41 @@ def ticket_priority_collection Topic.priorities.keys.map { |priority| [t("#{priority}_priority"), priority] } end + def ticket_tag_collection + ActsAsTaggableOn::Tagging.all.where(context: "tags", taggable_type: 'Topic').includes(:tag).map{|tagging| tagging.tag.name }.uniq + end + # id of opening or first post in the topic def first_post_id(topic) first_post = topic.posts.order(created_at: :asc).first first_post.present? ? first_post.id : nil end + # show topic tag form + def topic_tag_form + return nil if ticket_tag_collection.blank? + simple_form_for @topic, url: admin_update_topic_tags_path(id: @topic.id, status: params[:status]), remote: true, html: { class: 'form-inline tag-form' } do |f| + content_tag :div, class: 'row' do + content_tag :div, class: 'col-md-8' do + f.input :tag_list, collection: ticket_tag_collection, + as: :select, include_blank: false, label: false, + input_html: { multiple: true, class: '', placeholder: 'click to add tags...' }, + # wrapper_html: { style: 'width: 0' }, + data: { 'none-selected-text': t('tag_with', default: 'Tag Ticket'), 'live-search': false, 'width': '150px', dropdownAlignRight: false }, + style: 'width: 280px;' + + end + end + end + end + + # tag button label + def tags_button_label + if @topic.tag_list.count > 0 + pluralize @topic.tag_list.count, "Tag" + else + t(:tag_with, default: "Tag Ticket") + end + end + end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 035202d40..0209c0e1e 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -56,7 +56,7 @@ def i18n_icons(object) output = '
diff --git a/app/views/admin/docs/_form.html.erb b/app/views/admin/docs/_form.html.erb index 29637054b..d183c5b3f 100644 --- a/app/views/admin/docs/_form.html.erb +++ b/app/views/admin/docs/_form.html.erb @@ -91,81 +91,3 @@ <%= hidden_field_tag :obj_id, '' %>

<%= f.submit t('save_changes', default: "Save Changes"), class: 'btn btn-warning' %> - <%= summernote_lang_js %> - diff --git a/app/views/admin/docs/edit.html.erb b/app/views/admin/docs/edit.html.erb index 7b4f13884..5e34170de 100644 --- a/app/views/admin/docs/edit.html.erb +++ b/app/views/admin/docs/edit.html.erb @@ -9,12 +9,14 @@

<%= t(:edit, default:'Edit') %>: <%= @doc.title %>

<%= locale_select if AppSettings['i18n.available_locales'].count > 1 %> - <%= simple_form_for @doc, validate: true, url: admin_category_doc_path(category_id: @category.id, id: @doc.id), html: { method: :patch, id: :edit_doc } do |f| %> + <%= simple_form_for @doc, validate: true, url: admin_category_doc_path(category_id: @category.id, id: @doc.id), + html: { method: :patch, id: :edit_doc, name: "edit_doc_#{@doc.id}" } do |f| %> <% Globalize.with_locale(params['lang']) do %> <%= render 'shared/error_messages', object: f.object %> <%= render :partial => 'form', :object => @doc, :locals => {:f => f} %> <% end %> <% end %> + <% unless params[:lang].nil? %> + + + \ No newline at end of file diff --git a/app/views/admin/docs/new.html.erb b/app/views/admin/docs/new.html.erb index d9a338387..769418dd4 100644 --- a/app/views/admin/docs/new.html.erb +++ b/app/views/admin/docs/new.html.erb @@ -13,6 +13,87 @@ <% end %>
+<%= summernote_lang_js %> + + \ No newline at end of file diff --git a/app/views/admin/shared/_assistant.html.erb b/app/views/admin/shared/_assistant.html.erb new file mode 100644 index 000000000..d3a8bbfea --- /dev/null +++ b/app/views/admin/shared/_assistant.html.erb @@ -0,0 +1,39 @@ +<% if knowledgebase? %> + + +
+ <%= text_field_tag 't', nil, class:'agent-assist form-control ui-autocomplete-input', placeholder: 'Search the knowledgebase for an article to link to...' %> +
+
+
+ +<%= javascript_include_tag "agent_assistant", "data-turbolinks-track" => true %> +<% end %> \ No newline at end of file diff --git a/app/views/admin/tags/_form.html.erb b/app/views/admin/tags/_form.html.erb new file mode 100644 index 000000000..186136023 --- /dev/null +++ b/app/views/admin/tags/_form.html.erb @@ -0,0 +1,9 @@ +
+ <%= f.input 'name', label: t(:tag, default: "Tag"), placeholder: t(:tag, default: 'Tag Name') %> + <%= f.input 'description', label: t('description', default: "Description") %> + <%= f.input 'color', inline_label: t('color', default: "Color"), input_html: { class: 'pick-a-color' } %> + +
+
+ <%= f.button :submit, "Save Settings", class: 'btn btn-warning' %> +
diff --git a/app/views/admin/tags/_quick_add.html.erb b/app/views/admin/tags/_quick_add.html.erb new file mode 100644 index 000000000..79fdacdc5 --- /dev/null +++ b/app/views/admin/tags/_quick_add.html.erb @@ -0,0 +1,7 @@ +<%= simple_form_for(ActsAsTaggableOn::Tag.new, url: admin_tags_path, validate: true, remote: true, :html => { role: 'form'}) do |f| %> + +
+ <%= f.input 'name', label: false, placeholder: t(:tag, default: 'Quick Add Tag') %> +
+ <%= f.button :submit, t("save_changes"), class: 'btn btn-warning' %> +<% end %> diff --git a/app/views/admin/tags/_tag.html.erb b/app/views/admin/tags/_tag.html.erb new file mode 100644 index 000000000..79e42cdc8 --- /dev/null +++ b/app/views/admin/tags/_tag.html.erb @@ -0,0 +1,17 @@ +
+
+
+ <%= content_tag('span', tag.name) %> + <%= tag.description %> +
+
+
+ + +
+
+
diff --git a/app/views/admin/tags/create.js.erb b/app/views/admin/tags/create.js.erb new file mode 100644 index 000000000..bfa1a52cd --- /dev/null +++ b/app/views/admin/tags/create.js.erb @@ -0,0 +1,17 @@ +<% if @tag.persisted? %> + $('.tags').append('<%= j render partial: "tag", object: @tag %>'); + $('#acts_as_taggable_on_tag_name').val(''); + $('#acts_as_taggable_on_tag_name').focus(); + $('#tag_errors').html(''); +<% else %> + $('.acts_as_taggable_on_tag_name').addClass('has-error'); + $('#tag_errors').html('Tag is a duplicate or the same as a group name. Please choose a different name.'); +<% end %> + +// Update bootstrap_flash +$('.flash-wrapper').html("<%= j bootstrap_flash %>"); +// Autoclose alert messages in admin +$(".alert").delay(5000).slideUp(500, function(){ + $(".alert").alert('close'); +}); +<% flash[:notice] = '' %> diff --git a/app/views/admin/tags/destroy.js.erb b/app/views/admin/tags/destroy.js.erb new file mode 100644 index 000000000..a355d6c2f --- /dev/null +++ b/app/views/admin/tags/destroy.js.erb @@ -0,0 +1,9 @@ +$('#tag-<%= @tag.id %>').fadeOut(); + +// Update bootstrap_flash +$('.flash-wrapper').html("<%= j bootstrap_flash %>"); +// Autoclose alert messages in admin +$(".alert").delay(5000).slideUp(500, function(){ + $(".alert").alert('close'); +}); +<% flash[:notice] = '' %> diff --git a/app/views/admin/tags/edit.html.erb b/app/views/admin/tags/edit.html.erb new file mode 100644 index 000000000..e55ba0d56 --- /dev/null +++ b/app/views/admin/tags/edit.html.erb @@ -0,0 +1,17 @@ +<% title "#{admin_title} #{t('tag', default: "Tag")}" %> + +<% content_for :settings do %> +
+

+ <%= show_responsive_nav %> + <%= "#{@tag.name} #{t('tag', default: 'Tag')}" %> +

+
+
+
+ <%= simple_form_for @tag, url: admin_tag_path(@tag), method: 'put', validate: true, remote: false do |f| %> + <%= render "form", f: f %> + <% end %> +
+
+<% end %> diff --git a/app/views/admin/tags/index.html.erb b/app/views/admin/tags/index.html.erb new file mode 100644 index 000000000..ae71c6651 --- /dev/null +++ b/app/views/admin/tags/index.html.erb @@ -0,0 +1,38 @@ +<% title "#{admin_title} #{t('tags', default: "Tags")}" %> + +<% content_for :settings do %> + + + +
+

+ <%= show_responsive_nav %> + <%= t('tags', default: 'Tags') %> + +

+
+ +
+
+
+ Tag +
+
+ +
+ <%= render partial: 'admin/tags/tag', collection: @tags if @tags %> +
+
+ +
+ <%= render partial: 'quick_add' %> +
+ + +<% end %> diff --git a/app/views/admin/tags/new.html.erb b/app/views/admin/tags/new.html.erb new file mode 100644 index 000000000..796676dee --- /dev/null +++ b/app/views/admin/tags/new.html.erb @@ -0,0 +1,17 @@ +<% title "#{admin_title} #{t('tag', default: "Tag")}" %> + +<% content_for :settings do %> +
+

+ <%= show_responsive_nav %> + <%= "New #{t('tag', default: 'Tag')}" %> +

+
+
+
+ <%= simple_form_for @tag, url: admin_tags_path, method: 'post', validate: true, remote: false do |f| %> + <%= render "form", f: f %> + <% end %> +
+
+<% end %> diff --git a/app/views/admin/topics/_hidden_notes.html.erb b/app/views/admin/topics/_hidden_notes.html.erb index 2702e9d52..f73f0ad4f 100644 --- a/app/views/admin/topics/_hidden_notes.html.erb +++ b/app/views/admin/topics/_hidden_notes.html.erb @@ -1,3 +1,4 @@ +
@@ -17,7 +18,7 @@
- <%= t(:notes_hidden, hidden_notes: @topic.posts.notes.count) %> + <%= t(:notes_hidden, hidden_notes: @topic.posts.notes.size) %>
diff --git a/app/views/admin/topics/_new_ticket.html.erb b/app/views/admin/topics/_new_ticket.html.erb index 19c184ada..f83fcdc09 100644 --- a/app/views/admin/topics/_new_ticket.html.erb +++ b/app/views/admin/topics/_new_ticket.html.erb @@ -1,6 +1,6 @@
<%= content_tag :h3, "#{t(:open_new_discussion, default: 'New Ticket')}", class: 'view-header' %> - <%= simple_form_for(@topic, url: admin_topics_path, remote: true, validate: true, multipart: true, authenticity_token: true, html: { class: 'form-vertical new-ticket-loader', method: 'post' }) do |f| -%> + <%= simple_form_for(@topic, url: admin_topics_path, remote: true, validate: true, multipart: true, authenticity_token: true, html: { class: 'form-vertical new-ticket-loader', method: 'post', data: { persist: 'garlic'} }) do |f| -%>
@@ -22,6 +22,7 @@ <%= i.input :bcc, label: false, placeholder: 'bcc:' %> <%= i.input :body, as: :summernote, label: false, class: 'disable-empty' %> + <%= render partial: "admin/shared/assistant" %> <%= label_tag 'post_reply_id', "#{t(:select_common, default: 'Insert Common Reply')} (#{link_to(t(:edit, default: 'Edit'), admin_category_path(1), target: 'blank')})".html_safe, class: 'control-label' %> <%= f.select_tag 'post[reply_id]', grouped_options_for_select(i18n_reply_grouped_options, nil, prompt: ''), class: 'form-control', id: 'post_reply_id' %>
@@ -47,7 +48,8 @@ <%= f.input :priority, collection: ticket_priority_collection , as: :select, include_blank: true, selected: 'normal' %> <%= f.input :current_status, collection: statuses_collection, as: :select, include_blank: false, selected: 'new' %> <%= f.input :assigned_user_id, collection: agents_for_select, as: :select, include_blank: true, selected: current_user.id %> - <%= f.input :tag_list %> + <%= f.input :tag_list, collection: ticket_tag_collection, as: :select, include_blank: false, + input_html: { multiple: true, class: 'selectpicker', data: { 'none-selected-text': 'Any'} } unless ticket_tag_collection.blank? %>
<% end -%> @@ -89,6 +91,11 @@ $('[data-provider="summernote"]').each(function(){ } return ''; } + }, + callbacks: { + onInit: function (e) { + $("#topic_post_body").summernote("code", localStorage.getItem("new_ticket_draft")); + } } }); }); @@ -107,4 +114,24 @@ $.ajax({ $('.submit-ticket').off('click').on('click', function(){ $('#topic_post_kind').val('first'); }); + + $("#new_topic").sisyphus(); + $(document).off('keyup').on('keyup', '.note-editable', function () { + localStorage.setItem("new_ticket_draft", $("#topic_post_body").summernote("code")); + }); + $('#new_post').on('submit', function () { + localStorage.removeItem("new_ticket_draft"); + }); + + //$(function () { + // $("#new_topic").sisyphus(); + // setInterval(function () { + // localStorage.setItem("new_ticket", $("#topic_post_body").summernote("code")); + // }, 3000); // every 5 second interval + + // $('#new_topic').on('new_ticket', function(){ + // localStorage.removeItem("summernotedata"). + // }); + //}); + diff --git a/app/views/admin/topics/_post.html.erb b/app/views/admin/topics/_post.html.erb index 1d4cc8468..f7def5cec 100644 --- a/app/views/admin/topics/_post.html.erb +++ b/app/views/admin/topics/_post.html.erb @@ -23,7 +23,7 @@ <%= content_tag :div, post.bcc.present? ? "bcc: #{post.bcc}" : "", class: 'post-bcc post-cc-style' %>
<%= post.html_formatted_body -%> - <%= render partial: 'posts/thumbnail', locals: { :model_name => post } %> + <%= render partial: 'posts/thumbnail', locals: { :model_name => post } if cloudinary_enabled? %> <%= render partial: 'posts/attachment', locals: { :model_name => post } %>
diff --git a/app/views/admin/topics/_ticket.html.erb b/app/views/admin/topics/_ticket.html.erb index c37f592be..73d05dcc0 100644 --- a/app/views/admin/topics/_ticket.html.erb +++ b/app/views/admin/topics/_ticket.html.erb @@ -4,42 +4,28 @@
- -
<%= render :partial => 'admin/topics/topic_options' %> - -
- - <%= content_tag :span, formatted_tags(@topic), class: ' hidden-xs' %> - -

<%= show_responsive_nav %> #<%= @topic.id %>- <%= best_in_place @topic, :name, url: admin_topic_path(@topic), :class => "edit-topic-name" -%>  - <%= topic_added_from %> + <%= topic_added_from %><%= topic_anonymous_link %>

- <% if @posts %> <% end %>
<%= render :partial => 'admin/topics/post', :collection => @posts %>
- <%= render partial: 'admin/topics/hidden_notes' if @topic.posts.notes.count > 0 %> + <%= render partial: 'admin/topics/hidden_notes' if @topic.posts.notes.size > 0 %>

<%= t(:reply, default: 'Reply to this Topic') %>:

@@ -63,6 +49,7 @@ <%= hidden_field_tag :client_id %> <%= hidden_field_tag :from, 'admin' %> <%= hidden_field_tag :status, params[:status] %> + <%= render partial: "admin/shared/assistant" %> <%= f.input :body, as: :summernote, label: false, class: 'disable-empty' %> <%= label_tag 'post_reply_id', "#{t(:select_common, default: 'Insert Common Reply')} (#{link_to(t(:edit, default: 'Edit'), admin_category_path(1), target: 'blank')})".html_safe, class: 'control-label' %> @@ -90,6 +77,11 @@ $('[data-provider="summernote"]').each(function(){ $(this).summernote({ lang: '<%= summernote_locale %>', + callbacks: { + onInit: function (e) { + $("#post_body").summernote("code", localStorage.getItem("topic_reply_draft_<%= @topic.id %>")); + } + }, height: 150, prettifyHtml: true, toolbar: [ @@ -149,7 +141,21 @@ $.ajax({ window.emojiUrls = data; }); + diff --git a/app/views/admin/topics/_tiny_topic.html.erb b/app/views/admin/topics/_tiny_topic.html.erb index 93dec10a9..03e8b427a 100644 --- a/app/views/admin/topics/_tiny_topic.html.erb +++ b/app/views/admin/topics/_tiny_topic.html.erb @@ -2,7 +2,5 @@ diff --git a/app/views/admin/topics/_topic.html.erb b/app/views/admin/topics/_topic.html.erb index 7cda678f8..11de9ab51 100644 --- a/app/views/admin/topics/_topic.html.erb +++ b/app/views/admin/topics/_topic.html.erb @@ -11,7 +11,7 @@ <%= link_to "##{topic.id}- #{topic.name}", admin_topic_path(topic, status: @status), remote: true, class: "click-loader topic-link topic-link-#{topic.id}" -%> <%= badge_for_status(topic.current_status) %> <%= badge_for_public if forums? and !topic.private? %> - <% tag_listing(topic.team_list) %> + <%# tag_listing(topic.team_list) %> <%= started_by(topic) %> diff --git a/app/views/admin/topics/_topic_options.html.erb b/app/views/admin/topics/_topic_options.html.erb index f380fd8c9..ac7239888 100644 --- a/app/views/admin/topics/_topic_options.html.erb +++ b/app/views/admin/topics/_topic_options.html.erb @@ -6,7 +6,6 @@ <%= link_to content_tag(:span, '', class: 'fas fa-ellipsis-v ticket-ellipsis btn'), '#', class: 'dropdown-toggle', data: { toggle: 'dropdown' }%> + + + + + <% end %> diff --git a/app/views/admin/users/_tickets.html.erb b/app/views/admin/users/_tickets.html.erb index 6e35a2bb9..0206c50b9 100644 --- a/app/views/admin/users/_tickets.html.erb +++ b/app/views/admin/users/_tickets.html.erb @@ -26,7 +26,7 @@
<%#= content_tag :h4, t(:discussions, default: "Discussions") %> - <% if @topics.present? %> + <% if @topics.exists? %> diff --git a/app/views/admin/users/_user.html.erb b/app/views/admin/users/_user.html.erb index e52113b13..1be73b4a2 100644 --- a/app/views/admin/users/_user.html.erb +++ b/app/views/admin/users/_user.html.erb @@ -9,13 +9,13 @@ <%= link_to user.name.titleize, admin_user_path(user), remote: true, class: 'user-link' unless user.name.nil? %> <%= user_priority(user) %> <%= content_tag('span', t("#{user.role}_role"), class: 'label label-default') if user.is_agent? %> - <% tag_listing(user.team_list, "user") %> + <%# tag_listing(user.team_list, "user") %>
<%= "#{t('last_seen', default: "Last Seen:")}" %> <%= last_active_time(user.last_sign_in_at) %>
- <%= user.topics.count %> + <%= user.topics.size %> diff --git a/app/views/layouts/_admin_footer.html.erb b/app/views/layouts/_admin_footer.html.erb index 5e2c66876..0e4ee51bf 100644 --- a/app/views/layouts/_admin_footer.html.erb +++ b/app/views/layouts/_admin_footer.html.erb @@ -3,11 +3,16 @@