diff --git a/Gemfile b/Gemfile index ad7f89cbb..070f32dd1 100644 --- a/Gemfile +++ b/Gemfile @@ -108,6 +108,7 @@ gem 'selectize-rails' gem "bootstrap-switch-rails" gem 'bootstrap-datepicker-rails' gem 'bootstrap-select-rails' +gem 'gemoji' gem 'config', '~> 1.1.0', git: 'https://github.com/railsconfig/config.git' @@ -124,6 +125,10 @@ gem 'griddler-postmark' gem 'griddler-mailin' gem 'griddler-sparkpost' +# html Email +gem 'inky-rb', require: 'inky' +gem 'premailer-rails' + gem 'rails-timeago' gem 'devise_invitable', '~> 1.6' diff --git a/Gemfile.lock b/Gemfile.lock index a2e70a975..65324539d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,9 @@ GEM coffee-script-source (1.10.0) commonjs (0.2.7) concurrent-ruby (1.0.5) - crass (1.0.3) + crass (1.0.4) + css_parser (1.6.0) + addressable daemons (1.2.3) debug_inspector (0.0.2) deep_merge (1.0.1) @@ -189,8 +191,10 @@ GEM faraday (0.9.2) multipart-post (>= 1.2, < 3) ffi (1.9.17) - font-awesome-sass (4.5.0) - sass (>= 3.2) + font-awesome-sass (5.0.13) + sassc (>= 1.11) + foundation_emails (2.2.1.0) + gemoji (3.0.0) globalid (0.4.1) activesupport (>= 4.2.0) globalize (5.0.1) @@ -259,12 +263,15 @@ GEM http-cookie (1.0.2) domain_name (~> 0.5) http_accept_language (2.0.5) - i18n (0.9.3) + i18n (0.9.5) concurrent-ruby (~> 1.0) i18n-country-translations (1.2.3) i18n (~> 0.5) railties (>= 3.0) ice_nine (0.11.2) + inky-rb (1.3.7.2) + foundation_emails (~> 2) + nokogiri jbuilder (2.4.1) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) @@ -275,14 +282,14 @@ GEM jquery-minicolors-rails (2.2.3.0) jquery-rails rails (>= 3.2.8) - jquery-rails (4.1.1) + jquery-rails (4.3.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-turbolinks (2.1.0) railties (>= 3.1.0) turbolinks - jquery-ui-rails (5.0.5) + jquery-ui-rails (6.0.1) railties (>= 3.2.16) js_regex (1.0.14) regexp_parser (= 0.3.3) @@ -386,6 +393,13 @@ GEM arel polyglot (0.3.5) powerpack (0.1.1) + premailer (1.11.1) + addressable + css_parser (>= 1.6.0) + htmlentities (>= 4.0.0) + premailer-rails (1.10.1) + actionmailer (>= 3, < 6) + premailer (~> 1.7, >= 1.7.9) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -477,13 +491,17 @@ GEM sexp_processor (~> 4.1) rubyzip (1.2.1) safe_yaml (1.0.4) - sass (3.4.22) + sass (3.4.25) sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + sassc (1.11.4) + bundler + ffi (~> 1.9.6) + sass (>= 3.3.0) scss-lint (0.38.0) rainbow (~> 2.0) sass (~> 3.4.1) @@ -525,7 +543,7 @@ GEM staccato (0.4.5) sucker_punch (2.0.1) concurrent-ruby (~> 1.0.0) - summernote-rails (0.8.3.0) + summernote-rails (0.8.10.0) railties (>= 3.1) temple (0.7.6) terminal-table (1.5.2) @@ -547,7 +565,7 @@ GEM less-rails (>= 2.5.0) railties (>= 3.1) twitter-bootstrap-rails-confirm (1.0.5) - tzinfo (1.2.4) + tzinfo (1.2.5) thread_safe (~> 0.1) uglifier (3.0.0) execjs (>= 0.3.0, < 3) @@ -612,6 +630,7 @@ DEPENDENCIES factory_girl_rails faker font-awesome-sass + gemoji globalize-accessors globalize-versioning grape @@ -633,6 +652,7 @@ DEPENDENCIES helpy_onboarding! http_accept_language i18n-country-translations + inky-rb jbuilder (~> 2.0) jquery-fileupload-rails jquery-minicolors-rails @@ -657,6 +677,7 @@ DEPENDENCIES permalink_fu pg pg_search + premailer-rails pry pry-byebug rack-cors diff --git a/README.md b/README.md index 8b371e436..957697704 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ -Helpy: A Modern Helpdesk Alternative +Helpy: A Modern Helpdesk Platform for the Web ==================================== -Helpy is a modern, "mobile first" helpdesk solution written in Ruby on Rails 4.2 and released under the MIT license. The goal of Helpy is to provide a full featured open source helpdesk platform. +Helpy is a modern omnichannel helpdesk platform written in Ruby on Rails 4.2 and released under the MIT license. The goal of Helpy is to power your support email and ticketing, integrate seamlessly with your app, and run an amazing customer helpcenter. [![Build Status](https://img.shields.io/travis/helpyio/helpy/master.svg)](https://travis-ci.org/helpyio/helpy) [![Code Climate](https://codeclimate.com/github/helpyio/helpy/badges/gpa.svg)](https://codeclimate.com/github/helpyio/helpy) ![](http://helpy.io/images/HelpyBrowser.png) +Sponsor/Support Helpy +======== + +Helpy is licensed under the MIT license, and is an open-core project. This means that the core functionality is 100% open source and fully hackable or even re-sellable under the MIT license. See the features comparison below to understand what is included. -Features +Helpy is a large system and cannot exist purely as a hobby project. If you use it in a money generating capacity, it makes good sense to support the project financially or by becoming an official sponsor. + +Open Source Features ======== Helpy is an integrated support solution- combining and leveraging synergies between support ticketing, Knowledgebase and a public community. Each feature is optional however, and can be easily disabled. @@ -23,10 +29,47 @@ Helpy is an integrated support solution- combining and leveraging synergies betw - **Multi-lingual:** Helpy is fully multi-lingual and can provide support in multiple languages at the same time. Currently the app includes translations for 19 languages and is easy to translate. - **Themeable:** Customize the look and functionality of your Helpy without disturbing the underlying system that makes it all work. Helpy comes with two additional themes, and we hope to add more and get more from the community as time goes on. -Hosting +What is new in Version 2.0 +========= +Version 2 includes a number of awesome improvements in the open source edition, and even more in the pro and cloud hosted versions: + +- 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 + +Cloud Version +========= + +We also offer a hosted version with additional features designed to make your helpcenter even more awesome. This is a turn-key SaaS and does not require any effort on your part to get it up and running. Proceeds go directly towards supporting the continued development of the project. Some of the things found in the hosted version: + +- Triggers: Insert events at any point in the ticket lifecycle. This includes an outbound JSON API. +- Notifications: Browser notifications when new tickets are received, you are assigned to a ticket, etc. +- Real time UI: When tickets arrive, they are automatically added to the UI +- Custom Views: Add additional Ticketing queues to highlight just the tickets you need to see +- Advanced reporting: A suite of additional reports on the performance of your ticketing and helpcenter knowledgebase +- Advanced search: Easily filter and find tickets or customers when you have thousands +- Customizable Request Forms: Easily Add questions to the ticket creation forms +- AI Support Chatbot: Create a chatbot for your website to answer up 90% of tier one questions autonomously + +On-Premise and Dedicated Cloud ========= -We offer a hosted version of Helpy that includes a variety of additional features for businesses that don't want to worry about self installing and maintaining their Helpy. You can get an instant free trial of the hosted version to see if Helpy is right for you: [Test it Out for Free](https://goo.gl/Jbrx0m) +You may prefer to run Helpy locally or in-country. You can still get access to the full cloud feature set with either an on-premise installation of the cloud hosted features, or a dedicated AWS instance in a regional data-center. Live Demo ========= @@ -114,6 +157,6 @@ Welcome, and thanks for contributing to Helpy. Together we are building the bes License ======= -Copyright 2017, Helpy.io, LLC, Scott Miller and Contributors. Helpy is released under the MIT open source license. Please contribute back any enhancements you make. Also, I would appreciate if you kept the "powered by Helpy" blurb in the footer. This helps me keep track of how many are using Helpy. +Copyright 2016-2018, Helpy.io, LLC, Scott Miller and Contributors. Helpy is released under the MIT open source license. Please contribute back any enhancements you make. Also, I would appreciate if you kept the "powered by Helpy" blurb in the footer. This helps me keep track of how many are using Helpy. [![Analytics](https://ga-beacon.appspot.com/UA-50151-28/helpy/readme?pixel)](https://github.com/igrigorik/ga-beacon) diff --git a/app/assets/images/14369.jpg b/app/assets/images/14369.jpg index 9bb07b386..1719631d0 100644 Binary files a/app/assets/images/14369.jpg and b/app/assets/images/14369.jpg differ diff --git a/app/assets/images/collage3.png b/app/assets/images/collage3.png new file mode 100644 index 000000000..94bbe4a3c Binary files /dev/null and b/app/assets/images/collage3.png differ diff --git a/app/assets/images/helpy2_logo.png b/app/assets/images/helpy2_logo.png new file mode 100644 index 000000000..6bc0e9a10 Binary files /dev/null and b/app/assets/images/helpy2_logo.png differ diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index dad6b73cb..224af0366 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -25,6 +25,10 @@ String.prototype.capitalize = function() { var Helpy = Helpy || {}; Helpy.admin = function(){ + $(".alert").delay(2000).slideUp(500, function(){ + $(".alert").alert('close'); + }); + $('div.sortable').sortable({ items: '.item', axis: 'y', @@ -84,15 +88,15 @@ Helpy.admin = function(){ theme: 'bootstrap' }); - $('.reports-menu-toggle').off().on('click', function(){ - var $reports_nav = $('.reports-nav'); - if ($reports_nav.is(":visible")) { - $reports_nav.addClass('hidden-xs').addClass('hidden-sm'); - } else { - $reports_nav.removeClass('hidden-xs').removeClass('hidden-sm'); - } - - }); + // $('.reports-menu-toggle').off().on('click', function(){ + // var $reports_nav = $('.reports-nav'); + // if ($reports_nav.is(":visible")) { + // $reports_nav.addClass('hidden-xs').addClass('hidden-sm'); + // } else { + // $reports_nav.removeClass('hidden-xs').removeClass('hidden-sm'); + // } + // + // }); $('.bs-toggle').bootstrapSwitch(); @@ -169,6 +173,47 @@ Helpy.admin = function(){ $('#user_search').focus(); }); + // Highlight the last clicked view + $('.nav-item').off().on('click', function(){ + var $this = $(this); + $('.nav-item').removeClass('nav-active'); + $this.addClass('nav-active'); + }); + + // Highlight the last clicked view + $('.nav-item').on('mouseover', function(){ + var $this = $(this); + $this.addClass('nav-over'); + }); + // Highlight the last clicked view + $('.nav-item').on('mouseout', function(){ + var $this = $(this); + $this.removeClass('nav-over'); + }); + + Helpy.ticketMenu(); + +}; + +Helpy.ticketMenu = function() { + // Show/hide ticket menu + $('.show-ticket-menu').on('click', function(){ + var $ticketNav = $('#admin-left-nav'); + + if ($ticketNav.hasClass('open')) { + $ticketNav.removeClass('open').addClass('hidden-xs').addClass('hidden-sm').removeClass('left-dropdown'); + } else { + $ticketNav.addClass('open'); + $ticketNav.removeClass('hidden-xs').removeClass('hidden-sm').addClass('left-dropdown'); + } + }); + + $('.show-ticket-menu.open').on('click', function(){ + var $ticketNav = $('#admin-left-nav'); + + $ticketNav.removeClass('open'); + $ticketNav.addClass('hidden-xs'); + }); }; Helpy.showPanel = function(panel) { diff --git a/app/assets/javascripts/app.js b/app/assets/javascripts/app.js index 0753f514e..3091267ff 100644 --- a/app/assets/javascripts/app.js +++ b/app/assets/javascripts/app.js @@ -243,16 +243,11 @@ Helpy.ready = function(){ // Use or append common reply $('#post_reply_id').on('change', function(){ - var post_body = $('#post_body'); var common_reply = $('#post_reply_id option:selected'); - // append new line if some text already exists - if( post_body.val() && common_reply.val() ) { - post_body.val(post_body.val() + "\n\n"); - } - - // add content of selected reply - post_body.val( post_body.val() + common_reply.val() ); + // set value of summernote with existing value + common reply + $('#post_body').summernote('code', $('#post_body').summernote('code') + common_reply.val()); + $('#topic_post_body').summernote('code', $('#topic_post_body').summernote('code') + common_reply.val()); $('.disableable').attr('disabled', false); }); @@ -411,11 +406,11 @@ Helpy.ready = function(){ // Add hoversort icon $('.hoversort').off().on('mouseover', function(){ - $(this).prepend(''); + $(this).prepend(''); $(this).css("cursor","move"); $(this).on('mouseout', function(){ - $(this).find('span.fa-arrows-v').remove(); + $(this).find('span.fa-arrows-alt-v').remove(); }); }); }; @@ -469,7 +464,7 @@ Helpy.showGroup = function() { }; Helpy.loader = function(){ - $('#tickets').html("
"); + $('#tickets').html("
"); }; $(document).ready(Helpy.ready); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index bff1b51a7..c01320498 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -11,11 +11,9 @@ // about supported directives. // //= require jquery +//= require jquery-ui //= require best_in_place //= require jquery_ujs -//= require jquery-ui/sortable -//= require jquery-ui/effect-highlight -//= require jquery-ui/autocomplete //= require jquery-fileupload //= require bootstrap-sprockets //= require turbolinks diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 513872297..7554a08c3 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -45,7 +45,7 @@ Helpy.initShortcuts = function() { $('.pipeline-closed').click(); }); Mousetrap.bind('n', function() { - $('.new-discussion').click(); + $('.new-discussion a').click(); }); Mousetrap.bind('f', function() { $('.topic-search').show(); @@ -136,12 +136,20 @@ Helpy.initShortcuts = function() { Mousetrap.bind('.', function() { var $currentSelected = $('.selected'); + // startTopic is where the list selector starts out- either first or the selected one + var $startingTopic; + if ($('.tiny-topic-active').length > 0) { + $startingTopic = $('.tiny-topic-active').first(); + } else { + $startingTopic = $('.topic').first(); + } if ($('.selected').size() === 0) { - $('.topic').first().addClass('selected'); + $startingTopic.addClass('selected'); + // $('.topic').first().addClass('selected'); pressEnter($('.topic').first().find('a.topic-link')); } else { - var $nowSelected = $('.selected').next('tr.topic'); + var $nowSelected = $('.selected').next('.topic'); $nowSelected.addClass('selected'); $currentSelected.removeClass('selected'); @@ -151,12 +159,19 @@ Helpy.initShortcuts = function() { Mousetrap.bind(',', function() { var $currentSelected = $('.selected'); + // startTopic is where the list selector starts out- either first or the selected one + var $startingTopic; + if ($('.tiny-topic-active').length > 0) { + $startingTopic = $('.tiny-topic-active').first(); + } else { + $startingTopic = $('.topic').last(); + } if ($('.selected').size() === 0) { - $('.topic').last().addClass('selected'); + $startingTopic.addClass('selected'); pressEnter($('.topic').last().find('a.topic-link')); } else { - var $nowSelected = $('.selected').prev('tr.topic'); + var $nowSelected = $('.selected').prev('.topic'); $nowSelected.addClass('selected'); $currentSelected.removeClass('selected'); pressEnter($nowSelected.find('a.topic-link')); diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss new file mode 100644 index 000000000..594e29e3f --- /dev/null +++ b/app/assets/stylesheets/_settings.scss @@ -0,0 +1,146 @@ +// Foundation for Emails Settings +// ------------------------------ +// +// Table of Contents: +// +// 1. Global +// 2. Grid +// 3. Block Grid +// 4. Typography +// 5. Button +// 6. Callout +// 7. Menu +// 8. Thumbnail + + +// 1. Global +// --------- + +$primary-color: #68CCFA; +$secondary-color: #777777; +$success-color: #3adb76; +$warning-color: #ffae00; +$alert-color: #ec5840; +$light-gray: #f3f3f3; +$medium-gray: #cacaca; +$dark-gray: #8a8a8a; +$black: #0a0a0a; +$white: #fefefe; +$pre-color: #ff6908; +$global-width: 580px; +$global-width-small: 95%; +$global-gutter: 16px; +$body-background: $light-gray; +$container-background: $white; +$global-padding: 16px; +$global-margin: 16px; +$global-radius: 3px; +$global-rounded: 500px; +$global-breakpoint: $global-width + $global-gutter; + +// 2. Grid +// ------- + +$grid-column-count: 12; +$column-padding-bottom: $global-padding; +$container-radius: 0; + +// 3. Block Grid +// ------------- + +$block-grid-max: 8; +$block-grid-gutter: $global-gutter; + +// 4. Typography +// ------------- + +$global-font-color: $black; +$body-font-family: "Droid Sans", Arial, sans-serif; +$global-font-weight: normal; +$header-color: inherit; +$global-line-height: 1.6; +$global-font-size: 16px; +$body-line-height: $global-line-height; +$header-font-family: $body-font-family; +$header-font-weight: $global-font-weight; +$h1-font-size: 34px; +$h2-font-size: 30px; +$h3-font-size: 28px; +$h4-font-size: 24px; +$h5-font-size: 20px; +$h6-font-size: 18px; +$header-margin-bottom: 10px; +$paragraph-margin-bottom: 10px; +$small-font-size: 80%; +$small-font-color: $medium-gray; +$lead-font-size: $global-font-size * 1.25; +$lead-line-height: 1.6; +$text-padding: 10px; +$subheader-lineheight: 1.4; +$subheader-color: $dark-gray; +$subheader-font-weight: $global-font-weight; +$subheader-margin-top: 4px; +$subheader-margin-bottom: 8px; +$hr-width: $global-width; +$hr-border: 1px solid $black; +$hr-margin: 20px auto; +$anchor-text-decoration: none; +$anchor-color: $primary-color; +$anchor-color-visited: $anchor-color; +$anchor-color-hover: darken($primary-color, 10%); +$anchor-color-active: $anchor-color-hover; +$stat-font-size: 40px; + +// 5. Button +// --------- + +$button-padding: ( + tiny: 4px 8px 4px 8px, + small: 5px 10px 5px 10px, + default: 8px 16px 8px 16px, + large: 10px 20px 10px 20px, +); +$button-font-size: ( + tiny: 10px, + small: 12px, + default: 16px, + large: 20px, +); +$button-color: $white; +$button-color-alt: $medium-gray; +$button-font-weight: bold; +$button-margin: 0 0 $global-margin 0; +$button-background: $primary-color; +$button-border: 2px solid $button-background; +$button-radius: $global-radius; +$button-rounded: $global-rounded; + +// 6. Callout +// ---------- + +$callout-background: $white; +$callout-background-fade: 85%; +$callout-padding: 10px; +$callout-margin-bottom: $global-margin; +$callout-border: 1px solid darken($callout-background, 20%); +$callout-border-secondary: 1px solid darken($secondary-color, 20%); +$callout-border-success: 1px solid darken($success-color, 20%); +$callout-border-warning: 1px solid darken($warning-color, 20%); +$callout-border-alert: 1px solid darken($alert-color, 20%); + +// 7. Menu +// ------- + +$menu-item-padding: 10px; +$menu-item-gutter: 10px; +$menu-item-color: $primary-color; + +// 8. Thumbnail +// ------------ + +$thumbnail-border: solid 4px $white; +$thumbnail-margin-bottom: $global-margin; +$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2); +$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5); +$thumbnail-transition: box-shadow 200ms ease-out; +$thumbnail-radius: $global-radius; diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 63595fd7b..52d05486d 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -5,7 +5,7 @@ // === ADMIN GLOBAL OVERRIDES === body { - background-color: #fff; + background-color: #f4f4f4; } h1{ @@ -18,6 +18,11 @@ h2 { margin-top: 0px; } +h3 { + font-weight: bold; + margin-bottom: 30px; +} + table { margin-top: 20px; } @@ -36,8 +41,139 @@ td > img { margin-bottom: 20px; } +// === LEFT NAV === + +ul.settings-menu { + margin-top: 40px; +} + +.settings-menu-item { + margin-top: 5px; + margin-bottom: 5px; +} + +.active-settings-link { + font-weight: bold; +} + +#ticket-nav, +#user-nav, +#admin-left-nav { + height: 100%; +} + +#admin-left-nav.open { + padding-top: 30px; + opacity: 0.97; + background-color: #f4f4f4; +} + +.submit-section, +.nav-new-button { + margin-left: 10px; + + a { + border-radius: 20px; + padding-left: 15px; + padding-right: 15px; + } +} + +.show-ticket-menu { + margin-top: -10px; + margin-left: -8px; +} +.close-ticket-menu { + margin-right: -10px; + margin-top: -4px; +} + +.nav-items, +.user-nav-items { + margin-top: 0; + margin-bottom: 30px; + + .nav-item { + padding: 5px; + text-transform: capitalize; + + a { + display: block; + } + } + .nav-over { + background-color: #efefef; + } +} + +.left-nav-header { + padding: 5px; + margin-bottom: 20px; + border-bottom: 1px solid #ddd; +} + +.nav-header, +.settings-nav-header { + padding: 5px; + margin-bottom: 10px; + border-bottom: 1px solid #ddd; +} + +.nav-active { + font-weight: bold; +} + +.left-dropdown { + position: absolute; + z-index: 1000; + width: 50%; + height: 50%; + border: 1px solid #ddd; +} // === ADMIN LAYOUT === +.flash-wrapper { + position: fixed; + width: 100%; + z-index: 999999999; + + div { + width: 50%; + text-align: center; + margin-left: auto; + margin-right: auto; + margin-top: 5px; + } +} + +.main-panel { + background-color: white; + border: 1px solid #ddd; + margin-bottom: 20px; + padding-left: 20px; + padding-right: 20px; + padding-top: 15px; + padding-bottom: 20px; + border-radius: 4px; + -webkit-box-shadow: 0px 0px 40px -15px rgba(0,0,0,0.75); + -moz-box-shadow: 0px 0px 40px -15px rgba(0,0,0,0.75); + box-shadow: 0px 0px 40px -15px rgba(0,0,0,0.75); +} + +.col-md-12.main-panel { + margin-left: 10px; +} + +#body-wrapper { + position: relative; +} + +#main-content { + margin-top: 40px; + padding-right: 20px; + background-color: transparent; +} + #upper-wrapper { background-color: #222; margin-bottom: 0; @@ -49,11 +185,11 @@ td > img { #left-col-user-info { margin-top: 30px; padding-right: 30px; - border-right: 1px solid #eee; + border-right: 1px solid #f4f4f4; } #pipeline-wrapper { - background-color: #eee; + background-color: #f4f4f4; height: 160px; } @@ -151,8 +287,16 @@ td > img { // === ADMIN STYLES === +ul.breadcrumb { + background-color: transparent; + margin-bottom: 0px; + padding-left: 0px; + padding-bottom: 0px; +} + + .white-bg { - background-color: white; + background-color: transparent; } .admin-avatar img { @@ -213,6 +357,7 @@ ul.dropdown-menu { .navbar-default .navbar-brand { color: white; font-weight: normal; + margin-top: 10px; } .navbar-brand { @@ -222,8 +367,10 @@ ul.dropdown-menu { .navbar-right { margin-right: -28px; + margin-top: 10px; } + .mailbox { padding-right: 10px; padding-bottom: 8px; @@ -249,6 +396,7 @@ ul.dropdown-menu { .no-tickets { margin-top: 20%; + margin-bottom: 20%; span { color: #ccc; @@ -288,19 +436,6 @@ ul.dropdown-menu { } } -.settings-menu-item { - margin-top: 5px; - margin-bottom: 5px; -} - -.active-settings-link { - font-weight: bold; -} - -ul.settings-menu { - margin-top: 40px; -} - .settings-icon { font-size: 20px; color: #333; @@ -420,130 +555,132 @@ ul.settings-menu { -moz-opacity:0.50; //* FireFox */ } -// === PIPELINE STATS STYLES === - -.dummy { - margin-top: 110%; -} - -.stats { - display: block; - padding: 4px 5px; - margin-bottom: 18px; - line-height: 1.42857143; - background-color: #ffffff; - border: 1px solid #ddd; - border-radius: 4px; - -webkit-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - position: absolute; - top: 0px; - bottom: 0px; - left: 5px; - right: 5px; - height: 100px; - max-height: 100px; - text-align:center; - - .hover-stats { - cursor: pointer; - border: 1px solid #666; - box-shadow: 0px 0px 10px #eee; - } - - .selected-stats { - color: #000; - cursor: pointer; - border: 2px solid #666; - box-shadow: 0px 0px 10px #eee; - } -} - -a.stats:hover, -a.stats:focus, -a.stats.active { - border-color: #004084; -} - -.has-arrow { - position: relative; - border: 0px solid #333; - height: 100px; - - &:after, - &:before { - border: solid transparent; - content: ' '; - height: 0; - left: 95%; - top: 40%; - position: absolute; - width: 0; - } - - &:after { - border-width: 10px; - border-left-color: #fff; - top: 40%; - } - - &:before { - border-width: 11px; - border-left-color: #ddd; - top: calc(40% - 1px); - -webkit-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - } - - &.over:before { - border-width: 11px; - border-left-color: #333; - top: calc(40% - 1px); - -webkit-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - } -} - -.no-arrow { - position: relative; - border: 0px solid #333; - height: 100px; -} - -.pipeline-row { - padding: 0; - margin: 0; - list-style: none; - display: -webkit-box; - display: -moz-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -webkit-flex-flow: row wrap; - justify-content: center; - - -ms-flex-align: center; - -webkit-align-items: center; - -webkit-box-align: center; - - align-items: center; - -} -.pipeline-box { - padding: 5px; - margin-top: 10px; +// === PIPELINE STATS STYLES === - line-height: 150px; - color: white; - text-align: center; - flex-grow: .2; - flex-basis: auto; -} +// .dummy { +// margin-top: 110%; +// } +// +// .stats { +// display: block; +// padding: 4px 5px; +// margin-bottom: 18px; +// line-height: 1.42857143; +// background-color: #ffffff; +// border: 1px solid #ddd; +// border-radius: 4px; +// -webkit-transition: all 0.2s ease-in-out; +// -o-transition: all 0.2s ease-in-out; +// transition: all 0.2s ease-in-out; +// position: absolute; +// top: 0px; +// bottom: 0px; +// left: 5px; +// right: 5px; +// height: 100px; +// max-height: 100px; +// text-align:center; +// +// .hover-stats { +// cursor: pointer; +// border: 1px solid #666; +// box-shadow: 0px 0px 10px #eee; +// } +// +// .selected-stats { +// color: #000; +// cursor: pointer; +// border: 2px solid #666; +// box-shadow: 0px 0px 10px #eee; +// } +// } +// +// a.stats:hover, +// a.stats:focus, +// a.stats.active { +// border-color: #004084; +// } + +// .has-arrow { +// position: relative; +// border: 0px solid #333; +// height: 100px; +// +// &:after, +// &:before { +// border: solid transparent; +// content: ' '; +// height: 0; +// left: 95%; +// top: 40%; +// position: absolute; +// width: 0; +// } +// +// &:after { +// border-width: 10px; +// border-left-color: #fff; +// top: 40%; +// } +// +// &:before { +// border-width: 11px; +// border-left-color: #ddd; +// top: calc(40% - 1px); +// -webkit-transition: all 0.2s ease-in-out; +// -o-transition: all 0.2s ease-in-out; +// transition: all 0.2s ease-in-out; +// } +// +// &.over:before { +// border-width: 11px; +// border-left-color: #333; +// top: calc(40% - 1px); +// -webkit-transition: all 0.2s ease-in-out; +// -o-transition: all 0.2s ease-in-out; +// transition: all 0.2s ease-in-out; +// } +// } +// +// .no-arrow { +// position: relative; +// border: 0px solid #333; +// height: 100px; +// } +// +// .pipeline-row { +// padding: 0; +// margin: 0; +// list-style: none; +// +// display: -webkit-box; +// display: -moz-box; +// display: -ms-flexbox; +// display: -webkit-flex; +// display: flex; +// +// -webkit-flex-flow: row wrap; +// justify-content: center; +// +// -ms-flex-align: center; +// -webkit-align-items: center; +// -webkit-box-align: center; +// +// align-items: center; +// +// } +// .pipeline-box { +// padding: 5px; +// margin-top: 10px; +// +// line-height: 150px; +// color: white; +// text-align: center; +// flex-grow: .2; +// flex-basis: auto; +// } // === KEYBOARD SHORTCUTS === @@ -603,7 +740,7 @@ a.stats.active { // === REPORTS ==== .report-link { - border-bottom: 1px solid #eee; + border-bottom: 1px solid #f4f4f4; padding-bottom: 5px; padding-top: 5px; } @@ -652,6 +789,12 @@ a.stats.active { .checkbox-col { width: 10px; } + #main-content { + margin-top: 10px; + padding-right: 10px; + padding-left: 20px; + } + #status-mobile-dropdown { margin-left: 5px; margin-top: -5px; @@ -682,9 +825,20 @@ a.stats.active { height: 30px; } } + #header-wrapper > nav > div > ul > li > a { + text-align: left; + + span { + display: none; + } + br { + display: none; + } + } } @media(min-width:768px){ + h1 { font-size: 190%; } @@ -692,6 +846,11 @@ a.stats.active { h2 { margin-top: 45px; } + #main-content { + margin-top: 20px; + padding-right: 20px; + padding-left: 20px; + } .stats { padding-top: 20%; } @@ -729,6 +888,12 @@ a.stats.active { h1 { font-size: 200%; } + #main-content { + margin-top: 20px; + padding-right: 20px; + padding-left: 5px; + } + .big-stats { margin-top: -40px; } @@ -762,6 +927,12 @@ a.stats.active { h1 { font-size: 220%; } + #main-content { + margin-top: 20px; + padding-right: 20px; + padding-left: 5px; + } + .big-stats { margin-top: -40px; } diff --git a/app/assets/stylesheets/base.scss b/app/assets/stylesheets/base.scss index c60ba690a..8a4452e50 100644 --- a/app/assets/stylesheets/base.scss +++ b/app/assets/stylesheets/base.scss @@ -282,6 +282,20 @@ div.admin-tools { border-radius: .25em; } +.label-public { + border: 2px dashed #666; + color: #666; + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + .label-team { border: 2px dashed gray; color: #666; @@ -445,6 +459,11 @@ div.admin-tools { background-color: #fff !important; } +div.note-hint-item.active { + // scss-lint:disable ImportantRule + background-color: #444 !important; +} + .author { font-weight: normal; font-size: 85%; diff --git a/app/assets/stylesheets/bootstrap-overrides.scss b/app/assets/stylesheets/bootstrap-overrides.scss index dcd38a2d0..87a39b993 100644 --- a/app/assets/stylesheets/bootstrap-overrides.scss +++ b/app/assets/stylesheets/bootstrap-overrides.scss @@ -29,6 +29,12 @@ $font-family-serif: 'Droid Sans', serif; $font-size-base: 13px; +// Override button styles +$btn-primary-color: #fff !default; +$btn-primary-bg: $brand-primary !default; +$btn-primary-border: darken($btn-primary-bg, 5%) !default; +$btn-border-radius-base: 5px !default; + // LEGACY LESS CODE FOLLOWS: // TODO: Replace as necessary with SCSS equivalents diff --git a/app/assets/stylesheets/foundation_emails.scss b/app/assets/stylesheets/foundation_emails.scss new file mode 100644 index 000000000..4abfadcda --- /dev/null +++ b/app/assets/stylesheets/foundation_emails.scss @@ -0,0 +1,16 @@ +@import 'settings'; +@import "foundation-emails"; + +p { + line-height: 1.6; + color: $dark-gray; +} + +.panel { + border: 1px solid $light-gray; +} + +.intro { + color: $dark-gray; + font-weight: bold; +} diff --git a/app/assets/stylesheets/shared.scss b/app/assets/stylesheets/shared.scss index e13c57713..57698581e 100644 --- a/app/assets/stylesheets/shared.scss +++ b/app/assets/stylesheets/shared.scss @@ -25,6 +25,12 @@ } } +#right-menu { + h2 { + font-size: 14px; + } +} + .select-image { margin-top: 15px; margin-bottom: 30px; diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 8f8edeff8..11f92ee25 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -3,7 +3,34 @@ class Admin::BaseController < ApplicationController layout 'admin' before_action :authenticate_user! + def convert_to_brightness_value(background_hex_color) + (background_hex_color.scan(/../).map {|color| color.hex}).sum + end + + def contrasting_text_color(background_hex_color) + convert_to_brightness_value(background_hex_color) > 382.5 ? '#000' : '#fff' + end + helper_method :contrasting_text_color + + def lighten_color(hex_color, amount=0.6) + hex_color = hex_color.gsub('#','') + rgb = hex_color.scan(/../).map {|color| color.hex} + rgb[0] = [(rgb[0].to_i + 255 * amount).round, 255].min + rgb[1] = [(rgb[1].to_i + 255 * amount).round, 255].min + rgb[2] = [(rgb[2].to_i + 255 * amount).round, 255].min + "#%02x%02x%02x" % rgb + end + helper_method :lighten_color + def darken_color(hex_color, amount=0.4) + hex_color = hex_color.gsub('#','') + rgb = hex_color.scan(/../).map {|color| color.hex} + rgb[0] = (rgb[0].to_i * amount).round + rgb[1] = (rgb[1].to_i * amount).round + rgb[2] = (rgb[2].to_i * amount).round + "#%02x%02x%02x" % rgb + end + helper_method :darken_color protected @@ -27,9 +54,11 @@ def remote_search end def check_current_user_is_allowed? topic - return if !topic.private || current_user.is_admin? || current_user.team_list.include?(topic.team_list.first) + return true if !topic.private || current_user.is_admin? || current_user.team_list.include?(topic.team_list.first) if topic.team_list.count > 0 && current_user.is_restricted? && (topic.team_list + current_user.team_list).count > 0 - render status: :forbidden + false + else + true end end @@ -47,6 +76,36 @@ def date_from_params end end + # Get a list of topics for a passed in status + # Used by topics#index and #show and called from other methods that need to + # refresh the UI + def get_tickets_by_status + @status = params[:status] || "active" + if current_user.is_restricted? && teams? + topics_raw = Topic.all.tagged_with(current_user.team_list, any: true) + 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 + + get_all_teams + + case @status + when 'new' + topics_raw = topics_raw.unread + when 'active' + topics_raw = topics_raw.active + when 'mine' + topics_raw = Topic.active.mine(current_user.id).chronologic + when 'pending' + topics_raw = Topic.pending.mine(current_user.id).chronologic + else + topics_raw = topics_raw.where(current_status: @status) + end + @topics = topics_raw.page params[:page] + end + + def fetch_counts if current_user.is_restricted? && teams? topics = Topic.tagged_with(current_user.team_list, :any => true) @@ -60,9 +119,15 @@ def fetch_counts @pending = Topic.mine(current_user.id).pending.count @open = topics.open.count @active = topics.active.count - @mine = Topic.mine(current_user.id).count - @closed = topics.closed.count - @spam = topics.spam.count + @mine = Topic.active.mine(current_user.id).count + # @closed = topics.closed.count + # @spam = topics.spam.count + 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 end end diff --git a/app/controllers/admin/categories_controller.rb b/app/controllers/admin/categories_controller.rb index 4ec00c9d1..297050920 100644 --- a/app/controllers/admin/categories_controller.rb +++ b/app/controllers/admin/categories_controller.rb @@ -4,9 +4,11 @@ class Admin::CategoriesController < Admin::BaseController respond_to :js, only: ['destroy'] # Make the instance vars available for when the create action fails - before_action :set_categories_and_non_featured, only: [:index, :create] + before_action :set_categories_and_non_featured#, only: [:index, :show, :create] before_action :verify_editor + layout 'admin-content' + def index end @@ -26,6 +28,7 @@ def edit def create @category = Category.new(category_params) if @category.save + flash[:notice] = t(:model_created, default: "%{object_name} was saved", object_name: @category.name) redirect_to(admin_categories_path) else render :new @@ -36,6 +39,7 @@ def update I18n.locale = params['lang'] @category = Category.find(params[:id]) if @category.update(category_params) + flash[:notice] = t(:model_updated, default: "%{object_name} was updated", object_name: @category.name) redirect_to admin_categories_path else render :edit @@ -45,6 +49,7 @@ def update def destroy @category = Category.find(params[:id]) @category.destroy + flash[:notice] = t(:model_destroyed, default: "%{object_name} was deleted", object_name: @category.name) end private @@ -65,11 +70,4 @@ def category_params ) 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 - end - - end diff --git a/app/controllers/admin/docs_controller.rb b/app/controllers/admin/docs_controller.rb index 338df6d4d..7d18dca0f 100644 --- a/app/controllers/admin/docs_controller.rb +++ b/app/controllers/admin/docs_controller.rb @@ -1,9 +1,13 @@ class Admin::DocsController < Admin::BaseController before_action :verify_editor + before_action :set_categories_and_non_featured + respond_to :html, only: ['new','edit','create'] respond_to :js, only: ['destroy'] + layout 'admin-content' + def new @doc = Doc.new @doc.category_id = params[:category_id] @@ -21,6 +25,7 @@ def create @doc = Doc.new(doc_params) @doc.user_id = current_user.id if @doc.save + flash[:notice] = t(:model_created, default: "%{object_name} was saved", object_name: @doc.title) redirect_to(admin_category_path(@doc.category.id)) else render 'new' @@ -35,6 +40,7 @@ def update @category = @doc.category # @doc.tag_list = params[:doc][:tag_list] if @doc.update_attributes(doc_params) + flash[:notice] = t(:model_updated, default: "%{object_name} was updated", object_name: @doc.title) respond_to do |format| format.html { redirect_to(admin_category_path(@category.id)) @@ -54,11 +60,9 @@ def update def destroy @doc = Doc.find(params[:id]) + object_name = @doc.title @doc.destroy - render js:" - $('#doc-#{@doc.id}').fadeOut(); - Helpy.ready(); - Helpy.track();" + flash[:notice] = t(:model_destroyed, default: "%{object_name} was deleted", object_name: object_name) end private diff --git a/app/controllers/admin/posts_controller.rb b/app/controllers/admin/posts_controller.rb index 84189545d..ef88bd6ed 100644 --- a/app/controllers/admin/posts_controller.rb +++ b/app/controllers/admin/posts_controller.rb @@ -22,7 +22,10 @@ def create @post = Post.new(post_params) @post.topic_id = @topic.id @post.user_id = current_user.id + + # refresh collections for UI get_all_teams + get_tickets_by_status respond_to do |format| if @post.save @@ -62,6 +65,8 @@ def update fetch_counts get_all_teams + get_tickets_by_status + @topic = @post.topic @posts = @topic.posts.chronologic @@ -153,6 +158,5 @@ def update_topic_owner(old_owner, post) body: I18n.t('change_owner_note', old: old_owner.name, new: post.user.name, default: "The creator of this topic was changed from #{old_owner.name} to #{post.user.name}"), kind: "note", ) - end end diff --git a/app/controllers/admin/search_controller.rb b/app/controllers/admin/search_controller.rb index 27f3da8ed..562929a03 100644 --- a/app/controllers/admin/search_controller.rb +++ b/app/controllers/admin/search_controller.rb @@ -8,6 +8,9 @@ class Admin::SearchController < Admin::BaseController respond_to :html, :js + include ActionView::Helpers::NumberHelper + include ActionView::Helpers::TagHelper + # simple search tickets by # and user def topic_search @@ -32,9 +35,12 @@ def topic_search tracker("Agent: #{current_user.name}", "Viewed User Profile", @user.name) else @users = users.page params[:page] - template = 'admin/users/users' + @roles = [[t('team'), 'team'], [t(:admin_role), 'admin'], [t(:agent_role), 'agent'], [t(:editor_role), 'editor'], [t(:user_role), 'user']] + template = 'admin/users/index' tracker("Admin Search", "User Search", params[:q]) end + result_count = @topics.present? && @topics.total_count > 0 ? @topics.total_count : 0 + @header = "#{t(:results_found, count: result_count)} #{content_tag(:span, params[:q], class: 'more-important')}" render template end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 6b02516f0..2f799dc70 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -72,14 +72,33 @@ def update_settings flash[:success] = t(:settings_changes_saved, site_url: AppSettings['settings.site_url'], default: "The changes you have been saved. Some changes are only visible on your helpcenter site: #{AppSettings['settings.site_url']}") + + case params[:return_to] + when "design" + url = admin_design_settings_path + when "general" + url = admin_general_settings_path + when "email" + url = admin_email_settings_path + when "theme" + url = admin_theme_settings_path + when "i18n" + url = admin_i18n_settings_path + when "integration" + url = admin_integration_settings_path + when "widget" + url = admin_widget_settings_path + else + url = admin_general_settings_path + end + respond_to do |format| format.html { - redirect_to admin_settings_path + redirect_to url } format.js { } end end - end diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb index cf34308f4..f02e5cd76 100644 --- a/app/controllers/admin/topics_controller.rb +++ b/app/controllers/admin/topics_controller.rb @@ -35,47 +35,32 @@ class Admin::TopicsController < Admin::BaseController respond_to :js def index - @status = params[:status] || "pending" - if current_user.is_restricted? && teams? - topics_raw = Topic.all.tagged_with(current_user.team_list, any: true) - 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 - get_all_teams - case @status - when 'all' - topics_raw = topics_raw.all - when 'new' - topics_raw = topics_raw.unread - when 'active' - topics_raw = topics_raw.active - when 'unread' - topics_raw = topics_raw.unread.all - when 'assigned' - topics_raw = Topic.mine(current_user.id) - when 'pending' - topics_raw = Topic.pending.mine(current_user.id) - else - topics_raw = topics_raw.where(current_status: @status) - end - @topics = topics_raw.page params[:page] + get_tickets_by_status + team_tag_ids = ActsAsTaggableOn::Tagging.all.where(context: "teams").includes(:tag).map{|tagging| tagging.tag.id }.uniq + @teams = ActsAsTaggableOn::Tag.where("id IN (?)", team_tag_ids) tracker("Admin-Nav", "Click", @status.titleize) end def show + get_tickets_by_status @topic = Topic.where(id: params[:id]).first - - # REVIEW: Try not opening message on view unless assigned - check_current_user_is_allowed? @topic - if @topic.current_status == 'new' && @topic.assigned? - tracker("Agent: #{current_user.name}", "Opened Ticket", @topic.to_param, @topic.id) - @topic.open + @doc = Doc.find(@topic.doc_id) if @topic.doc_id.present? && @topic.doc_id != 0 + + if check_current_user_is_allowed? @topic + # REVIEW: Try not opening message on view unless assigned + # if @topic.current_status == 'new' && @topic.assigned? + # tracker("Agent: #{current_user.name}", "Opened Ticket", @topic.to_param, @topic.id) + # @topic.open + # end + get_all_teams + @posts = @topic.posts.chronologic.includes(:user) + tracker("Agent: #{current_user.name}", "Viewed Ticket", @topic.to_param, @topic.id) + fetch_counts + @include_tickets = false + else + @post = @topic.posts.new + render status: :forbidden end - get_all_teams - @posts = @topic.posts.chronologic.includes(:user) - tracker("Agent: #{current_user.name}", "Viewed Ticket", @topic.to_param, @topic.id) - fetch_counts end def new @@ -85,75 +70,14 @@ def new @user = params[:user_id].present? ? User.find(params[:user_id]) : User.new end - # TODO: Still need to refactor this method and the update methods into one def create @page_title = t(:start_discussion, default: "Start a New Discussion") @title_tag = "#{AppSettings['settings.site_name']}: #{@page_title}" - @forum = Forum.find(1) - @user = User.where("lower(email) = ?", params[:topic][:user][:email].downcase).first - - @topic = @forum.topics.new( - name: params[:topic][:name], - private: true, - team_list: params[:topic][:team_list], - channel: params[:topic][:channel], - tag_list: params[:topic][:tag_list], - priority: params[:topic][:priority] - ) - - if @user.nil? - - @token, enc = Devise.token_generator.generate(User, :reset_password_token) - - @user = @topic.build_user - @user.reset_password_token = enc - @user.reset_password_sent_at = Time.now.utc - - @user.name = params[:topic][:user][:name] - @user.login = params[:topic][:user][:email].split("@")[0] - @user.email = params[:topic][:user][:email] - @user.home_phone = params[:topic][:user][:home_phone] - @user.password = User.create_password - - @user.save + if params[:topic][:kind] == 'internal' + create_internal_ticket else - @topic.user_id = @user.id - end - - fetch_counts - respond_to do |format| - if (@user.save || !@user.nil?) && @topic.save - @post = @topic.posts.create( - body: params[:topic][:post][:body], - user_id: @user.id, - kind: 'first', - screenshots: params[:topic][:screenshots], - attachments: params[:topic][:post][:attachments] - ) - - # Send email - UserMailer.new_user(@user.id, @token).deliver_later - - # track event in GA - tracker('Request', 'Post', 'New Topic') - tracker('Agent: Unassigned', 'New', @topic.to_param) - - # Now that we are rendering show, get the posts (just one) - # TODO probably can refactor this - @posts = @topic.posts.chronologic.includes(:user) - format.js { - render action: 'show', id: @topic - - } - format.html { - render action: 'show', id: @topic - } - else - format.html { - render action: 'new' - } - end + create_customer_conversation end end @@ -168,9 +92,8 @@ def update_topic bulk_post_attributes = [] if params[:change_status].present? - + user_id = current_user.id || 2 if ["closed", "reopen", "trash"].include?(params[:change_status]) - user_id = current_user.id || 2 @topics.each do |topic| # prepare bulk params bulk_post_attributes << {body: I18n.t("#{params[:change_status]}_message", user_name: User.find(user_id).name), kind: 'note', user_id: user_id, topic_id: topic.id} @@ -189,6 +112,7 @@ def update_topic end @action_performed = "Marked #{params[:change_status].titleize}" + flash[:notice] = I18n.t("#{params[:change_status]}_message", user_name: User.find(user_id).name) # Calls to GA for close, reopen, assigned. tracker("Agent: #{current_user.name}", @action_performed, @topics.to_param, 0) @@ -201,10 +125,10 @@ def update_topic fetch_counts get_all_teams + get_tickets_by_status respond_to do |format| format.js { if params[:topic_ids].count > 1 - get_tickets render 'admin/topics/index' else render 'admin/topics/update_ticket', id: @topic.id @@ -235,7 +159,7 @@ def assign_agent @topics.bulk_agent_assign(bulk_post_attributes, assigned_user.id) if bulk_post_attributes.present? if params[:topic_ids].count > 1 - get_tickets + get_tickets_by_status else @topic = Topic.find(@topics.first.id) @posts = @topic.posts.chronologic @@ -245,11 +169,14 @@ def assign_agent fetch_counts get_all_teams + get_tickets_by_status + flash[:notice] = I18n.t(:assigned_message, assigned_to: assigned_user.name) + respond_to do |format| format.html #render action: 'ticket', id: @topic.id format.js { if params[:topic_ids].count > 1 - get_tickets + get_tickets_by_status render 'index' else render 'update_ticket', id: @topic.id @@ -269,8 +196,10 @@ def toggle_privacy @topics.each do |topic| if topic.forum_id == 1 bulk_post_attributes << {body: I18n.t(:converted_to_ticket), kind: 'note', user_id: current_user.id, topic_id: topic.id} + flash[:notice] = I18n.t(:converted_to_ticket) else bulk_post_attributes << {body: I18n.t(:converted_to_topic, forum_name: topic.forum.name), kind: 'note', user_id: current_user.id, topic_id: topic.id} + flash[:notice] = I18n.t(:converted_to_topic, forum_name: topic.forum.name) end # Calls to GA @@ -285,6 +214,9 @@ def toggle_privacy fetch_counts get_all_teams + get_tickets_by_status + + # respond_to do |format| # format.js { if params[:topic_ids].count > 1 @@ -299,7 +231,6 @@ def toggle_privacy def update @topic = Topic.find(params[:id]) - if @topic.update_attributes(topic_params) respond_to do |format| format.html { @@ -322,6 +253,7 @@ def update_tags fetch_counts get_all_teams + get_tickets_by_status @topic.posts.create( @@ -330,12 +262,13 @@ def update_tags kind: 'note', ) + 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 + render 'update_ticket', id: @topic.id, status: params[:status] } end else @@ -344,7 +277,7 @@ def update_tags end def assign_team - assigned_group = params[:team] + assigned_group = params[:assign_team] @topics = Topic.where(id: params[:topic_ids]) bulk_post_attributes = [] unless assigned_group.blank? @@ -359,8 +292,10 @@ def assign_team @topics.bulk_group_assign(bulk_post_attributes, assigned_group) if bulk_post_attributes.present? + flash[:notice] = I18n.t(:assigned_group, assigned_group: assigned_group) + if params[:topic_ids].count > 1 - get_tickets + get_tickets_by_status else @topic = Topic.find(@topics.first.id) @posts = @topic.posts.chronologic @@ -368,11 +303,13 @@ def assign_team fetch_counts get_all_teams + get_tickets_by_status + + respond_to do |format| format.html #render action: 'ticket', id: @topic.id format.js { if params[:topic_ids].count > 1 - get_tickets render 'index' else render 'update_ticket', id: @topic.id @@ -397,6 +334,8 @@ def unassign_team fetch_counts get_all_teams + get_tickets_by_status + render 'update_ticket', id: @topic.id end @@ -434,6 +373,8 @@ def split_topic fetch_counts get_all_teams + get_tickets_by_status + respond_to do |format| format.html { redirect_to admin_topic_path(@topic) } @@ -448,6 +389,7 @@ def merge_tickets fetch_counts get_all_teams + respond_to do |format| format.js { render 'show', id: @topic } end @@ -457,6 +399,141 @@ def shortcuts render layout: 'admin-plain' end + protected + + def create_customer_conversation + @forum = Forum.find(1) + @user = User.where("lower(email) = ?", params[:topic][:user][:email].downcase).first + + @topic = @forum.topics.new( + name: params[:topic][:name], + private: true, + team_list: params[:topic][:team_list], + channel: params[:topic][:channel], + tag_list: params[:topic][:tag_list], + priority: params[:topic][:priority], + current_status: params[:topic][:current_status], + assigned_user_id: params[:topic][:assigned_user_id] + ) + if @user.nil? + create_ticket_user + else + @topic.user_id = @user.id + end + + fetch_counts + respond_to do |format| + if (@user.save || !@user.nil?) && @topic.save + @post = @topic.posts.create( + body: params[:topic][:post][:body], + user_id: current_user.id, + kind: 'first', + screenshots: params[:topic][:screenshots], + attachments: params[:topic][:post][:attachments], + cc: params[:topic][:post][:cc], + bcc: params[:topic][:post][:bcc] + ) + + # Send copy of message to user + PostMailer.new_post(@post .id).deliver_later + + # track event in GA + tracker('Request', 'Post', 'New Topic') + tracker('Agent: Unassigned', 'New', @topic.to_param) + + # Now that we are rendering show, get the posts (just one) + # TODO probably can refactor this + @posts = @topic.posts.chronologic.includes(:user) + format.js { + render action: 'show', id: @topic + + } + format.html { + render action: 'show', id: @topic + } + else + format.html { + render action: 'new' + } + end + end + + end + + + # Internal ticket is created by the agent. Does not send messages to customer + def create_internal_ticket + @forum = Forum.find(1) + @user = current_user + + @topic = @forum.topics.new( + name: params[:topic][:name], + private: true, + team_list: params[:topic][:team_list], + channel: params[:topic][:channel], + tag_list: params[:topic][:tag_list], + priority: params[:topic][:priority], + current_status: params[:topic][:current_status], + assigned_user_id: params[:topic][:assigned_user_id], + kind: 'internal' + ) + @topic.user_id = @user.id + + fetch_counts + respond_to do |format| + if (@user.save || !@user.nil?) && @topic.save + @post = @topic.posts.create( + body: params[:topic][:post][:body], + user_id: @user.id, + kind: 'first', + screenshots: params[:topic][:screenshots], + attachments: params[:topic][:post][:attachments] + ) + + # track event in GA + tracker('Request', 'Post', 'New Internal Topic') + tracker('Agent: Unassigned', 'New', @topic.to_param) + + # Now that we are rendering show, get the posts (just one) + # TODO probably can refactor this + @posts = @topic.posts.chronologic.includes(:user) + format.js { + render action: 'show', id: @topic + + } + format.html { + render action: 'show', id: @topic + } + else + format.html { + render action: 'new' + } + end + end + + + end + + def create_ticket_user + @token, enc = Devise.token_generator.generate(User, :reset_password_token) + + @user = @topic.build_user + @user.reset_password_token = enc + @user.reset_password_sent_at = Time.now.utc + + @user.name = params[:topic][:user][:name] + @user.login = params[:topic][:user][:email].split("@")[0] + @user.email = params[:topic][:user][:email] + @user.home_phone = params[:topic][:user][:home_phone] + @user.password = User.create_password + + @user.save + + # Send welcome email + UserMailer.new_user(@user.id, @token).deliver_later + end + + private def get_tickets @@ -468,15 +545,15 @@ def get_tickets case @status - when 'all' - @topics = Topic.all.page params[:page] + # when 'all' + # @topics = Topic.all.page params[:page] when 'new' @topics = Topic.unread.page params[:page] when 'active' @topics = Topic.active.page params[:page] - when 'unread' - @topics = Topic.unread.all.page params[:page] - when 'assigned' + # when 'unread' + # @topics = Topic.unread.all.page params[:page] + when 'mine' @topics = Topic.mine(current_user.id).page params[:page] when 'pending' @topics = Topic.pending.mine(current_user.id).page params[:page] @@ -486,7 +563,10 @@ def get_tickets end def topic_params - params.require(:topic).permit(:name) - params.require(:topic).permit(:tag_list) + params.require(:topic).permit( + :name, + :tag_list + ) end + end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f7967e1e3..94810ed09 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -52,8 +52,10 @@ class Admin::UsersController < Admin::BaseController before_action :get_all_teams respond_to :html, :js + include ActionView::Helpers::TagHelper + def index - @roles = [['Team', 'team'], [t(:admin_role), 'admin'], [t(:agent_role), 'agent'], [t(:editor_role), 'editor'], [t(:user_role), 'user']] + @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.all.page params[:page] @@ -73,6 +75,7 @@ def show # We still have to grab the first topic for the user to use the same user partial @topic = Topic.where(user_id: @user.id).first + @header = content_tag :span, "#{@user.name.titleize}#{view_context.user_priority(@user)}".html_safe tracker("Agent: #{current_user.name}", "Viewed User Profile", @user.name) end @@ -87,7 +90,7 @@ def update fetch_counts # update team list if provided - @user.team_list = params[:user][:team_list] if params[:user][:team_list].present? + @user.team_list = params[:user][:team_list] if params[:user][:team_list].present? if @user.update(user_params) @@ -100,6 +103,8 @@ def update @topic = Topic.where(user_id: @user.id).first tracker("Agent: #{current_user.name}", "Edited User Profile", @user.name) + flash[:notice] = "#{@user.name} has been saved" + # TODO: Refactor this to use an index method/view on the users model respond_to do |format| format.html { @@ -108,6 +113,9 @@ def update format.js { render 'admin/users/show' } + format.json { + respond_with_bip(@user) + } end else render :profile diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 18e562819..688558649 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,6 +10,8 @@ class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? around_action :set_time_zone, if: :current_user + force_ssl if: :ssl_configured? + def url_options { locale: I18n.locale, theme: params[:theme] }.merge(super) end @@ -47,6 +49,10 @@ def tracker(ga_category, ga_action, ga_label, ga_value=nil) end end + def ssl_configured? + AppSettings["settings.enforce_ssl"] == '1' && Rails.env.production? + end + def google_analytics_enabled? AppSettings['settings.google_analytics_enabled'] == '1' end @@ -79,10 +85,15 @@ def tickets? helper_method :tickets? def teams? - AppSettings['settings.teams'] == "1" || AppSettings['settings.teams'] == true + true end helper_method :teams? + def display_branding? + AppSettings['branding.display_branding'] == "1" || AppSettings['branding.display_branding'] == true + end + helper_method :display_branding? + def forums_enabled? raise ActionController::RoutingError.new('Not Found') unless forums? end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 1723dfaff..3909f92f3 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -98,6 +98,8 @@ def create team_list: params[:topic][:team_list], channel: 'web') + associate_with_doc + if recaptcha_enabled? && !user_signed_in? unless verify_recaptcha(model: @topic) initialize_new_ticket_form_vars @@ -153,6 +155,14 @@ def tag @topics = Topic.ispublic.tag_counts_on(:tags) end + protected + + def associate_with_doc + return unless params[:topic][:doc_id].present? + doc = Doc.find(params[:topic][:doc_id]) + @topic.tag_list = "Feedback, #{doc.category.name}" if doc.present? && doc.category.present? + end + private def initialize_new_ticket_form_vars diff --git a/app/helpers/admin/import_helper.rb b/app/helpers/admin/import_helper.rb new file mode 100644 index 000000000..09f6fe50b --- /dev/null +++ b/app/helpers/admin/import_helper.rb @@ -0,0 +1,14 @@ +module Admin::ImportHelper + + def importable_models_collection + [ + [t("file_users"),"User"], + [t("file_tickets"),"Topic"], + [t("file_replies"),"Post"], + [t("file_docs"),"Doc"], + [t("file_categories"),"Category"], + [t("file_forums"), "Forum"] + ] + end + +end diff --git a/app/helpers/admin/layout_helper.rb b/app/helpers/admin/layout_helper.rb new file mode 100644 index 000000000..6b352f315 --- /dev/null +++ b/app/helpers/admin/layout_helper.rb @@ -0,0 +1,26 @@ +# Helpers for styling the admin layout and views + +module Admin::LayoutHelper + + def settings_header(name) + content_tag :div, class: 'admin-header' do + content_tag :h3 do + concat show_responsive_nav + concat "#{t(:settings, default: "Settings")}: #{t(name.to_sym, default: name.capitalize)}" + end + end + end + + def show_responsive_nav + content_tag :small do + content_tag :span, nil, class: 'fas fa-caret-square-left btn show-ticket-menu hidden-lg hidden-md' + end + end + + def hide_responsive_nav + content_tag :div, class: "pull-right hidden-lg hidden-md" do + content_tag :span, nil, class: "fas fa-times btn show-ticket-menu close-ticket-menu" + end + end + +end diff --git a/app/helpers/admin/topics_helper.rb b/app/helpers/admin/topics_helper.rb index 8456821ec..db49b3457 100644 --- a/app/helpers/admin/topics_helper.rb +++ b/app/helpers/admin/topics_helper.rb @@ -10,6 +10,14 @@ def user_page_title 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 + 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 ticket_status_label content_tag :span, class: "label #{status_class(@status)}", style: 'text-transform: uppercase' do status_label(@status) @@ -33,4 +41,35 @@ def user_priority(user) end end + def agents_for_select + User.agents.all.map { |user| [user.name, user.id] } + end + + def channels_collection + [ + [t('activerecord.attributes.user.email'), 'email'], + [t('activerecord.attributes.user.home_phone'), 'phone'], + [t(:channel_in_person, default: 'In Person'), 'person'] + ] + end + + def statuses_collection + statuses = [] + ['new','open','pending','closed'].each do |s| + statuses << [t(s.to_sym), s] + end + statuses + end + + def ticket_types_collection + [ + [t('customer_conversation', default: "Customer Conversation"), 'ticket'], + [t('internal_ticket', default: "Internal Ticket"), 'internal'] + ] + end + + def ticket_priority_collection + Topic.priorities.keys.map { |priority| [t("#{priority}_priority"), priority] } + end + end diff --git a/app/helpers/admin/users_helper.rb b/app/helpers/admin/users_helper.rb index d2a1e5318..8f9da8d03 100644 --- a/app/helpers/admin/users_helper.rb +++ b/app/helpers/admin/users_helper.rb @@ -2,10 +2,10 @@ module Admin::UsersHelper def user_header content_tag :h2, id: 'ticket-page-title' do - concat render 'admin/topics/ticket_nav_dropdown' + # concat render 'admin/topics/ticket_nav_dropdown' - concat new_user_ticket_button - concat edit_user_button + # concat new_user_ticket_button + # concat edit_user_button concat user_name concat user_account_number end @@ -35,4 +35,17 @@ def user_account_number end end + def priority_collection + [[t('low_priority', default: 'Low'),'low'],[t('normal_priority', default: 'Normal'),'normal'],[t('high_priority', default: 'High'),'high'],[t('vip_priority', default: 'VIP'),'vip']] + end + + def roles_collection + [ + [t('admin_role'),'admin'], + [t('agent_role'),'agent'], + [t('editor_role'),'editor'], + [t('user_role'),'user'] + ] + end + end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index ed247d672..6d920beba 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -13,6 +13,21 @@ def admin_title "[Helpy Admin]" end + def upper_nav_item(label, path, controllers, actions, icon="") + # classname = controller_name == controller ? 'navbar-active' : '' + if controllers.include?(controller_name) && actions.include?(action_name) + classname = 'navbar-active' + else + classname = '' + end + + content_tag(:li, class: classname) do + link_to path, class: 'text-center' do + "#{content_tag(:span, nil, class: "#{icon}")}
#{label}".html_safe + end + end + end + def i18n_reply_grouped_options grouped_options = {} AppSettings['i18n.available_locales'].each do |locale| @@ -28,7 +43,7 @@ def i18n_reply_grouped_options end val = [] Doc.replies.with_translations(locale).all.each do |doc| - body = (strip_tags(doc.body)).gsub(/\'/, ''') + body = ((doc.body))#.gsub(/\'/, ''') val.push([doc.title, body]) end grouped_options[key] = val @@ -52,6 +67,14 @@ def i18n_icons(object) output.html_safe end + def new_active_class + if controller_name == "topics" && action_name == "new" + 'navbar-active' + else + '' + end + end + def default_locale_options options = {} options[t('select_default_locale', default: "Select Default Locale...")] = '' @@ -64,7 +87,9 @@ def default_locale_options end def navbar_expanding_link(url, icon, text, target="", remote=false) - link_to(content_tag(:span, '', class: "#{icon} hidden-lg hidden-md", title: text) + content_tag(:span, text, class: "hidden-sm hidden-xs"), url, remote: remote, target: target) + link_to url, remote: remote, target: target do + content_tag(:span, '', class: "#{icon} hidden-lg hidden-md", title: text) + content_tag(:span, text, class: "hidden-sm hidden-xs") + end end def settings_item(icon, title, description, link = "") @@ -88,10 +113,10 @@ def settings_title_link(title, link = "#") end def settings_menu_item(icon, title, link='#') - content_tag(:li, class: 'settings-menu-item') do + content_tag(:li, class: 'nav-item') do link_to(link, class: "#{settings_link(link)} #{'active-settings-link' if current_page?(link)}", "data-target" => title) do - concat content_tag(:span, '', class: "#{icon} settings-menu-icon") - concat content_tag(:span, t(title, default: title.capitalize), class: 'hidden-xs') + # concat content_tag(:span, '', class: "#{icon} settings-menu-icon") + concat content_tag(:span, t(title, default: title.capitalize)) end end end @@ -117,6 +142,8 @@ def help_items concat content_tag(:li, link_to(t(:report_bug, default: "Report a Bug"), "http://github.com/helpyio/helpy/issues"), target: "blank") concat content_tag(:li, link_to(t(:suggest_feature, default: "Suggest a Feature"), "http://support.helpy.io/en/community/4-feature-requests/topics"), target: "blank") concat content_tag(:li, link_to(t(:shortcuts, default: "Keyboard Shortcuts"), "#", class: 'keyboard-shortcuts-link'), target: "blank") if current_user.is_agent? + concat content_tag :hr + concat content_tag(:li, link_to("Sponsors", "https://helpy.io/sponsors", target: 'blank')) end end @@ -128,8 +155,8 @@ def helpcenter_menu end def helpcenter_link - link_to '#', class: 'dropdown-toggle', data: { toggle: 'dropdown' }, role: 'button' do - concat t(:helpcenter, default: 'Helpcenter') + link_to '#', class: 'dropdown-toggle text-center', data: { toggle: 'dropdown' }, role: 'button' do + concat "#{content_tag :span, nil, class: 'fas fa-book'}
#{t(:helpcenter, default: 'Helpcenter')}".html_safe concat content_tag(:span, '', class: 'caret') end end @@ -158,7 +185,7 @@ def admin_avatar_menu_link def admin_avatar_menu_items content_tag :ul, class: 'dropdown-menu' do concat content_tag(:li, link_to(t(:your_profile, default: 'Your Profile'), admin_profile_settings_path(mode: 'settings')), class: 'visible-lg visible-md visible-sm hidden-xs') - concat content_tag(:li, link_to(t(:settings, default: 'Settings'), admin_settings_path), class: 'visible-lg visible-md visible-sm hidden-xs') if current_user.is_admin? + concat content_tag(:li, link_to(t(:settings, default: 'Settings'), admin_general_settings_path), class: 'visible-lg visible-md visible-sm hidden-xs') if current_user.is_admin? concat content_tag(:li, link_to(t('api_keys', default: "API Keys"), admin_api_keys_path), class: 'visible-lg visible-md visible-sm hidden-xs') if current_user.is_agent? @@ -172,23 +199,23 @@ def settings_link(link) end def attachment_icon(filename) - return 'fa fa-file-text-o' unless filename.include?('.') + return 'far fa-file-text' unless filename.include?('.') extension = filename.split(".").last.downcase case extension when 'pdf' - return 'fa fa-file-pdf-o' + return 'far fa-file-pdf' when 'doc', 'docx' - return "fa fa-file-word-o" + return "far fa-file-word" when 'xls', 'xlsx' - "fa fa-file-excel-o" + "far fa-file-excel" when 'zip', 'tar' - "fa fa-file-archive-o" + "far fa-file-archive" when 'ppt', 'pptx' - "fa fa-file-powerpoint-o" + "far fa-file-powerpoint" when 'html', 'htm' - "fa fa-file-code-o" + "far fa-file-code" else - "fa fa-file-o" + "far fa-file" end end @@ -252,7 +279,7 @@ def list_tags(topic) def add_tag_link content_tag :li do - content_tag(:span, '', class: 'fa fa-tag add-tag-link') + content_tag(:span, '', class: 'fas fa-tag add-tag-link') end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b4e7c95b0..c2c166042 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -97,7 +97,7 @@ def tag_listing(tags, tagging_type = "message") def login_with(with, redirect_to = "/#{I18n.locale}") provider = (with == "google_oauth2") ? "google" : with link_to(user_omniauth_authorize_path(with.to_sym, origin: redirect_to), class: ["btn","btn-block","btn-social","oauth","btn-#{provider}"], style: "color:white;", data: {provider: "#{provider}"}) do - content_tag(:span, '', {class: ["fa", "fa-#{provider}"]}).html_safe + I18n.t("devise.shared.links.sign_in_with_provider", provider: provider.titleize) + content_tag(:span, '', {class: ["fab", "fa-#{provider}"]}).html_safe + I18n.t("devise.shared.links.sign_in_with_provider", provider: provider.titleize) end end @@ -131,4 +131,11 @@ def css_injector def get_path(screenshot) screenshot.format == "pdf" ? "#{screenshot.public_id}.png" : screenshot.path end + + def summernote_lang_js + if I18n.locale != :en + "".html_safe + end + end + end diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb new file mode 100644 index 000000000..9df18482c --- /dev/null +++ b/app/helpers/email_helper.rb @@ -0,0 +1,6 @@ +module EmailHelper + def email_image_tag(image, **options) + attachments[image] = File.read(Rails.root.join("app/assets/images/#{image}")) + image_tag attachments[image].url, **options + end +end diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb new file mode 100644 index 000000000..b8e43a74f --- /dev/null +++ b/app/helpers/emoji_helper.rb @@ -0,0 +1,11 @@ +module EmojiHelper + def emojify(content) + h(content).to_str.gsub(/:([\w+-]+):/) do |match| + if (emoji = Emoji.find_by_alias($1)) + %(#$1) + else + match + end + end.html_safe if content.present? + end +end diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index a97acc780..5cd301dcd 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -37,6 +37,10 @@ def badge_for_private content_tag(:span, t(:private, default: 'PRIVATE'), class: 'hidden-xs pull-right status-label label label-private') end + def badge_for_public + content_tag(:span, t(:public, default: 'PUBLIC'), class: 'hidden-xs pull-right status-label label label-public') + end + def control_for_status(status) content_tag(:span, "#{status_label(status)} ".html_safe, class: "change-status btn status-label-button label #{status_class(status)}") end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 000000000..a009ace51 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/jobs/backup_job.rb b/app/jobs/backup_job.rb index ce8192878..84fff1c9d 100644 --- a/app/jobs/backup_job.rb +++ b/app/jobs/backup_job.rb @@ -1,4 +1,4 @@ -class BackupJob < ActiveJob::Base +class BackupJob < ApplicationJob queue_as :default def perform(params, user_id) @@ -13,7 +13,7 @@ def perform(params, user_id) else logger.info("backup error: #{backup.errors}") end - end + end end def to_csv(records, options = {}) diff --git a/app/jobs/delete_user_job.rb b/app/jobs/delete_user_job.rb index e7508073e..51f74d351 100644 --- a/app/jobs/delete_user_job.rb +++ b/app/jobs/delete_user_job.rb @@ -1,4 +1,4 @@ -class DeleteUserJob < ActiveJob::Base +class DeleteUserJob < ApplicationJob queue_as :default def perform(user_id) diff --git a/app/jobs/import_job.rb b/app/jobs/import_job.rb index 4f85aa664..375dcdf34 100644 --- a/app/jobs/import_job.rb +++ b/app/jobs/import_job.rb @@ -1,4 +1,4 @@ -class ImportJob < ActiveJob::Base +class ImportJob < ApplicationJob queue_as :import require 'roo' STATUS = {error: "Error", in_progress: "In Progress", completed: "Completed"} diff --git a/app/jobs/rebuild_search_job.rb b/app/jobs/rebuild_search_job.rb index 410a7f5d2..6182a246a 100644 --- a/app/jobs/rebuild_search_job.rb +++ b/app/jobs/rebuild_search_job.rb @@ -1,4 +1,4 @@ -class RebuildSearchJob < ActiveJob::Base +class RebuildSearchJob < ApplicationJob queue_as :default # Calls a search rebuild, used when categories are updated diff --git a/app/jobs/tracker_job.rb b/app/jobs/tracker_job.rb index b19838db9..970274b84 100644 --- a/app/jobs/tracker_job.rb +++ b/app/jobs/tracker_job.rb @@ -1,4 +1,4 @@ -class TrackerJob < ActiveJob::Base +class TrackerJob < ApplicationJob queue_as :default def perform(category, action, label, value, client_id, ga_id) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d25d8892d..5719986e8 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,6 @@ class ApplicationMailer < ActionMailer::Base + add_template_helper(EmailHelper) + default from: "from@example.com" layout 'mailer' end diff --git a/app/mailers/backup_mailer.rb b/app/mailers/backup_mailer.rb index 94fd81f58..c41d57b9b 100644 --- a/app/mailers/backup_mailer.rb +++ b/app/mailers/backup_mailer.rb @@ -1,4 +1,4 @@ -class BackupMailer < ActionMailer::Base +class BackupMailer < ApplicationMailer def notify_backup_complition(user, model_name) @user = user diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index 3c699300d..17ec68cab 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -1,8 +1,10 @@ class DeviseMailer < Devise::Mailer + add_template_helper(EmailHelper) helper :application # gives access to all helpers defined within `application_helper`. include Devise::Controllers::UrlHelpers # Optional. eg. `confirmation_url` - default template_path: 'devise/mailer' # to make sure that your mailer uses the devise + # default template_path: 'devise/mailer' # to make sure that your mailer uses the devise + layout 'mailer' def confirmation_instructions(record, token, opts={}) # code to be added here later diff --git a/app/mailers/import_mailer.rb b/app/mailers/import_mailer.rb index 951452355..714d3ae92 100644 --- a/app/mailers/import_mailer.rb +++ b/app/mailers/import_mailer.rb @@ -1,5 +1,6 @@ -class ImportMailer < ActionMailer::Base - +class ImportMailer < ApplicationMailer + layout 'mailer' + def notify_import_complition(user, model_name, notes) @user = user @notes = notes diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index f3071fe73..53c702929 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -1,4 +1,6 @@ -class NotificationMailer < ActionMailer::Base +class NotificationMailer < ApplicationMailer + layout 'notification' + add_template_helper(PostsHelper) def new_private(topic_id) new_notification(topic_id, User.notifiable_on_private) @@ -19,6 +21,7 @@ def new_notification(topic_id, notifiable_users) return if notifiable_users.count == 0 @topic = Topic.find(topic_id) + @posts = @topic.posts.where.not(id: @topic.posts.last.id).reverse @user = @topic.user @recipient = notifiable_users.first @bcc = notifiable_users.last(notifiable_users.count-1).collect {|u| u.email} diff --git a/app/mailers/post_mailer.rb b/app/mailers/post_mailer.rb index ad050f822..a834dcee5 100644 --- a/app/mailers/post_mailer.rb +++ b/app/mailers/post_mailer.rb @@ -1,12 +1,23 @@ class PostMailer < ActionMailer::Base + + MAXIMUM_EMAIL_POSTS_PER_MINUTE = 5 + add_template_helper(ApplicationHelper) + add_template_helper(PostsHelper) def new_post(post_id) @post = Post.find(post_id) @topic = @post.topic + @posts = @topic.posts.where.not(id: @post.id).ispublic.active.reverse + # Do not send if internal + return if @topic.kind == 'internal' + # block autoresponder loops + return if @topic.posts_in_last_minute > MAXIMUM_EMAIL_POSTS_PER_MINUTE # Do not send to temp email addresses return if @topic.user.email.split("@")[0] == "change" + # Return if topic status is not sendable + return if ['trash','spam'].include?(@topic.current_status) email_with_name = %("#{@topic.user_name}" <#{@topic.user.email}>) @post.attachments.each do |att| diff --git a/app/mailers/topic_mailer.rb b/app/mailers/topic_mailer.rb index 1a020e43d..c48d76389 100644 --- a/app/mailers/topic_mailer.rb +++ b/app/mailers/topic_mailer.rb @@ -1,4 +1,4 @@ -class TopicMailer < ActionMailer::Base +class TopicMailer < ApplicationMailer add_template_helper(ApplicationHelper) def new_ticket(topic_id) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index bf451732f..f51592170 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,5 +1,6 @@ -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer add_template_helper(ApplicationHelper) + layout 'mailer' def new_user(user_id, token) return unless (AppSettings['settings.welcome_email'] == "1" || AppSettings['settings.welcome_email'] == true) @@ -7,7 +8,7 @@ def new_user(user_id, token) # Do not send to temp email addresses return if @user.email.split("@")[0] == "change" - + @token = token @locale = I18n.locale.to_s email_with_name = %("#{@user.name}" <#{@user.email}>) diff --git a/app/models/post.rb b/app/models/post.rb index 7109051af..56f839710 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -19,12 +19,15 @@ class Post < ActiveRecord::Base - attr_accessor :reply_id - # This is used to skip the callbacks when importing (ie. we don't want to send # emails to everyone while importing) attr_accessor :importing attr_accessor :resolved + attr_accessor :reply_id + + # Whitelist tags and attributes that are allowed in posts + ALLOWED_TAGS = %w(strong em a p br b img ul li) + ALLOWED_ATTRIBUTES = %w(href src class style width height target) belongs_to :topic, counter_cache: true, touch: true belongs_to :user, touch: true @@ -33,8 +36,8 @@ class Post < ActiveRecord::Base has_many :flags mount_uploaders :attachments, AttachmentUploader - validates :body, length: { maximum: 10_000 } - before_validation :truncate_body + # validates :body, length: { maximum: 100_000 } + # before_validation :truncate_body validates :kind, :user, :user_id, :body, presence: true @@ -51,6 +54,17 @@ class Post < ActiveRecord::Base scope :by_votes, -> { order('points DESC')} scope :notes, -> { where(kind: 'note') } + def self.new_with_cc(topic) + if topic.posts.count == 0 + topic.posts.new + else + topic.posts.new( + cc: topic.posts.chronologic.last.cc, + bcc: topic.posts.chronologic.last.bcc + ) + end + end + #updates the last post date for both the forum and the topic #updates the waiting on cache def update_waiting_on_cache @@ -129,6 +143,14 @@ def first? self.topic.posts.first == self end + def html_formatted_body + "#{ActionController::Base.helpers.sanitize(body.gsub(/(?:\n\r?|\r\n?)/, '
'), tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES)}".html_safe + end + + def text_formatted_body + "#{ActionView::Base.full_sanitizer.sanitize(body)}".html_safe + end + private def truncate_body diff --git a/app/models/topic.rb b/app/models/topic.rb index 81e3dc752..a43732fe0 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -42,7 +42,7 @@ class Topic < ActiveRecord::Base has_many :votes, :as => :voteable has_attachments :screenshots, accept: [:jpg, :png, :gif, :pdf, :txt, :rtf, :doc, :docx, :ppt, :pptx, :xls, :xlsx, :zip] - paginates_per 25 + paginates_per 15 include PgSearch multisearchable :against => [:id, :name, :post_cache], @@ -67,7 +67,7 @@ class Topic < ActiveRecord::Base scope :chronologic, -> { order('updated_at DESC') } scope :reverse, -> { order('updated_at ASC') } scope :by_popularity, -> { order('points DESC') } - scope :active, -> { where(current_status: %w(open pending)) } + scope :active, -> { where(current_status: %w(new open pending)) } scope :undeleted, -> { where.not(current_status: 'trash') } scope :front, -> { limit(6) } scope :for_doc, -> { where("doc_id= ?", doc)} @@ -268,6 +268,10 @@ def from_email_address end end + def posts_in_last_minute + self.posts.where(created_at: Time.now-1.minutes..Time.now, kind: 'reply').count + end + private def cache_user_name diff --git a/app/themes/flat/assets/stylesheets/flat/flat.scss b/app/themes/flat/assets/stylesheets/flat/flat.scss index 7ad33f2c5..46f725af3 100644 --- a/app/themes/flat/assets/stylesheets/flat/flat.scss +++ b/app/themes/flat/assets/stylesheets/flat/flat.scss @@ -445,6 +445,20 @@ div.admin-tools { border-radius: .25em; } +.label-public { + border: 2px dashed #666; + color: #666; + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + .label-collapsed { position: relative; top: -0.7em; diff --git a/app/themes/flat/locales/en.yml b/app/themes/flat/locales/en.yml index ad6817490..223e4a516 100644 --- a/app/themes/flat/locales/en.yml +++ b/app/themes/flat/locales/en.yml @@ -126,12 +126,12 @@ en: # Discussion controls change_status: Change Status - status: Status + status: &status Status mark_closed: Mark Resolved reopen: Reopen mark_spam: Mark Spam mark_new: Mark New - assign_agent: Assign Agent + assign_agent: &assign_agent Assign Agent selected_messages: one: "1 selected message" two: "2 selected messages" diff --git a/app/themes/flat/views/home/index.html.erb b/app/themes/flat/views/home/index.html.erb index e2268482a..d30b962dd 100644 --- a/app/themes/flat/views/home/index.html.erb +++ b/app/themes/flat/views/home/index.html.erb @@ -45,7 +45,7 @@
<%= link_to(new_topic_path) do %> - + <%= content_tag :h3, "#{t(:new_ticket, default: 'New ticket')}" %> <% end %> @@ -55,7 +55,7 @@
<%= link_to(tickets_path) do %> - + <%= content_tag :h3, "#{t(:your_tickets, default: 'Your tickets')}" %> <% end %> diff --git a/app/themes/flat/views/layouts/clean.html.erb b/app/themes/flat/views/layouts/clean.html.erb index 497189672..bc606a62e 100644 --- a/app/themes/flat/views/layouts/clean.html.erb +++ b/app/themes/flat/views/layouts/clean.html.erb @@ -62,7 +62,7 @@ <% else %> <%= content_tag :li, link_to(t('devise.sessions.new.sign_in'), '#', class: 'login-link', data: { toggle: "modal", target: "#login-modal" }, class: 'hidden-xs') %> <% end %> -
  • +
  • diff --git a/app/themes/flat/views/layouts/flat.html.erb b/app/themes/flat/views/layouts/flat.html.erb index 9a2fa6bdf..96793ff6c 100644 --- a/app/themes/flat/views/layouts/flat.html.erb +++ b/app/themes/flat/views/layouts/flat.html.erb @@ -65,7 +65,7 @@ <% else %> <%= content_tag :li, link_to(t('devise.sessions.new.sign_in'), '#', class: 'login-link hidden-xs', data: { toggle: "modal", target: "#login-modal" }) %> <% end %> -
  • +
  • diff --git a/app/themes/light/assets/stylesheets/light/light.scss b/app/themes/light/assets/stylesheets/light/light.scss index 7a2ee1e92..8eaf97227 100644 --- a/app/themes/light/assets/stylesheets/light/light.scss +++ b/app/themes/light/assets/stylesheets/light/light.scss @@ -445,6 +445,20 @@ div.admin-tools { border-radius: .25em; } +.label-public { + border: 2px dashed #666; + color: #666; + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + .label-collapsed { position: relative; top: -0.7em; diff --git a/app/themes/light/views/home/index.html.erb b/app/themes/light/views/home/index.html.erb index e2268482a..d30b962dd 100644 --- a/app/themes/light/views/home/index.html.erb +++ b/app/themes/light/views/home/index.html.erb @@ -45,7 +45,7 @@
    <%= link_to(new_topic_path) do %> - + <%= content_tag :h3, "#{t(:new_ticket, default: 'New ticket')}" %> <% end %> @@ -55,7 +55,7 @@
    <%= link_to(tickets_path) do %> - + <%= content_tag :h3, "#{t(:your_tickets, default: 'Your tickets')}" %> <% end %> diff --git a/app/themes/light/views/layouts/clean.html.erb b/app/themes/light/views/layouts/clean.html.erb index fc4ede9e8..db5ccb056 100644 --- a/app/themes/light/views/layouts/clean.html.erb +++ b/app/themes/light/views/layouts/clean.html.erb @@ -62,7 +62,7 @@ <% else %> <%= content_tag :li, link_to(t('devise.sessions.new.sign_in'), '#', class: 'login-link', data: { toggle: "modal", target: "#login-modal" }, class: 'hidden-xs') %> <% end %> -
  • +
  • diff --git a/app/themes/light/views/layouts/light.html.erb b/app/themes/light/views/layouts/light.html.erb index 89106d14a..272588a26 100644 --- a/app/themes/light/views/layouts/light.html.erb +++ b/app/themes/light/views/layouts/light.html.erb @@ -65,7 +65,7 @@ <% else %> <%= content_tag :li, link_to(t('devise.sessions.new.sign_in'), '#', class: 'login-link hidden-xs', data: { toggle: "modal", target: "#login-modal" }) %> <% end %> -
  • +
  • diff --git a/app/themes/singular/about.markdown b/app/themes/singular/about.markdown new file mode 100644 index 000000000..aacb53b4d --- /dev/null +++ b/app/themes/singular/about.markdown @@ -0,0 +1 @@ +Singular is a tiled, single column layout for your helpcenter. diff --git a/app/themes/singular/assets/images/singular/.gitkeep b/app/themes/singular/assets/images/singular/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/app/themes/singular/assets/javascripts/singular/all.js b/app/themes/singular/assets/javascripts/singular/all.js new file mode 100644 index 000000000..02b7a7f1a --- /dev/null +++ b/app/themes/singular/assets/javascripts/singular/all.js @@ -0,0 +1,7 @@ +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +//= require_tree . diff --git a/app/themes/singular/assets/javascripts/singular/menu.js b/app/themes/singular/assets/javascripts/singular/menu.js new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/app/themes/singular/assets/javascripts/singular/menu.js @@ -0,0 +1 @@ + diff --git a/app/themes/singular/assets/javascripts/singular/sidr.js b/app/themes/singular/assets/javascripts/singular/sidr.js new file mode 100644 index 000000000..f20b87dfe --- /dev/null +++ b/app/themes/singular/assets/javascripts/singular/sidr.js @@ -0,0 +1,307 @@ +// jshint ignore: start + +/*! sidr - v2.2.1 - 2016-02-17 + * http://www.berriart.com/sidr/ + * Copyright (c) 2013-2016 Alberto Varela; Licensed MIT */ +!function() { + "use strict"; + function a(a, b, c) { + var d = new o(b); + switch (a) { + case"open": + d.open(c); + break; + case"close": + d.close(c); + break; + case"toggle": + d.toggle(c); + break; + default: + p.error("Method " + a + " does not exist on jQuery.sidr") + } + } + function b(a) { + return "status" === a ? h : s[a] ? s[a].apply(this, Array.prototype.slice.call(arguments, 1)) : "function" != typeof a && "string" != typeof a && a ? void q.error("Method " + a + " does not exist on jQuery.sidr") : s.toggle.apply(this, arguments) + } + function c(a, b) { + if ("function" == typeof b.source) { + var c = b.source(name); + a.html(c) + } else if ("string" == typeof b.source && i.isUrl(b.source)) + u.get(b.source, function(b) { + a.html(b) + }); + else if ("string" == typeof b.source) { + var d = "", e = b.source.split(","); + if (u.each(e, function(a, b) { + d += '
    ' + u(b).html() + "
    " + }), b.renaming) { + var f = u("
    ").html(d); + f.find("*").each(function(a, b) { + var c = u(b); + i.addPrefixes(c) + }), d = f.html() + } + a.html(d) + } else + null !== b.source && u.error("Invalid Sidr Source"); + return a + } + function d(a) { + var d = i.transitions, e = u.extend({ + name: "sidr", + speed: 200, + side: "left", + source: null, + renaming: !0, + body: "body", + displace: !0, + timing: "ease", + method: "toggle", + bind: "touchstart click", + onOpen: function() {}, + onClose: function() {}, + onOpenEnd: function() {}, + onCloseEnd: function() {} + }, a), f = e.name, g = u("#" + f); + return 0 === g.length && (g = u("
    ").attr("id", f).appendTo(u("body"))), d.supported && g.css(d.property, e.side + " " + e.speed / 1e3 + "s " + e.timing), g.addClass("sidr").addClass(e.side).data({ + speed: e.speed, + side: e.side, + body: e.body, + displace: e.displace, + timing: e.timing, + method: e.method, + onOpen: e.onOpen, + onClose: e.onClose, + onOpenEnd: e.onOpenEnd, + onCloseEnd: e.onCloseEnd + }), g = c(g, e), this.each(function() { + var a = u(this), c = a.data("sidr"), d=!1; + c || (h.moving=!1, h.opened=!1, a.data("sidr", f), a.bind(e.bind, function(a) { + a.preventDefault(), d || (d=!0, b(e.method, f), setTimeout(function() { + d=!1 + }, 100)) + })) + }) + } + var e = {}; + e.classCallCheck = function(a, b) { + if (!(a instanceof b)) + throw new TypeError("Cannot call a class as a function") + }, e.createClass = function() { + function a(a, b) { + for (var c = 0; c < b.length; c++) { + var d = b[c]; + d.enumerable = d.enumerable ||!1, d.configurable=!0, "value"in d && (d.writable=!0), Object.defineProperty(a, d.key, d) + } + } + return function(b, c, d) { + return c && a(b.prototype, c), d && a(b, d), b + } + }(); + var f, g, h = { + moving: !1, + opened: !1 + }, i = { + isUrl: function(a) { + var b = new RegExp("^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$", "i"); + return b.test(a)?!0 : !1 + }, + addPrefixes: function(a) { + this.addPrefix(a, "id"), this.addPrefix(a, "class"), a.removeAttr("style") + }, + addPrefix: function(a, b) { + var c = a.attr(b); + "string" == typeof c && "" !== c && "sidr-inner" !== c && a.attr(b, c.replace(/([A-Za-z0-9_.\-]+)/g, "sidr-" + b + "-$1")) + }, + transitions: function() { + var a = document.body || document.documentElement, b = a.style, c=!1, d = "transition"; + return d in b ? c=!0 : !function() { + var a = ["moz", "webkit", "o", "ms"], e = void 0, f = void 0; + d = d.charAt(0).toUpperCase() + d.substr(1), c = function() { + for (f = 0; f < a.length; f++) + if (e = a[f], e + d in b) + return !0; + return !1 + }(), d = c ? "-" + e.toLowerCase() + "-" + d.toLowerCase() : null + }(), { + supported: c, + property: d + } + }() + }, j = jQuery, k = "sidr-animating", l = "open", m = "close", n = "webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend", o = function() { + function a(b) { + e.classCallCheck(this, a), this.name = b, this.item = j("#" + b), this.openClass = "sidr" === b ? "sidr-open" : "sidr-open " + b + "-open", this.menuWidth = this.item.outerWidth(!0), this.speed = this.item.data("speed"), this.side = this.item.data("side"), this.displace = this.item.data("displace"), this.timing = this.item.data("timing"), this.method = this.item.data("method"), this.onOpenCallback = this.item.data("onOpen"), this.onCloseCallback = this.item.data("onClose"), this.onOpenEndCallback = this.item.data("onOpenEnd"), this.onCloseEndCallback = this.item.data("onCloseEnd"), this.body = j(this.item.data("body")) + } + return e.createClass(a, [{ + key: "getAnimation", + value: function(a, b) { + var c = {}, d = this.side; + return "open" === a && "body" === b ? c[d] = this.menuWidth + "px" : "close" === a && "menu" === b ? c[d] = "-" + this.menuWidth + "px" : c[d] = 0, c + } + }, { + key: "prepareBody", + value: function(a) { + var b = "open" === a ? "hidden": ""; + if (this.body.is("body")) { + var c = j("html"), d = c.scrollTop(); + c.css("overflow-x", b).scrollTop(d) + } + } + }, { + key: "openBody", + value: function() { + if (this.displace) { + var a = i.transitions, b = this.body; + if (a.supported) + b.css(a.property, this.side + " " + this.speed / 1e3 + "s " + this.timing).css(this.side, 0).css({ + width: b.width(), + position: "absolute" + }), b.css(this.side, this.menuWidth + "px"); + else { + var c = this.getAnimation(l, "body"); + b.css({ + width: b.width(), + position: "absolute" + }).animate(c, { + queue: !1, + duration: this.speed + }) + } + } + } + }, { + key: "onCloseBody", + value: function() { + var a = i.transitions, b = { + width: "", + position: "", + right: "", + left: "" + }; + a.supported && (b[a.property] = ""), this.body.css(b).unbind(n) + } + }, { + key: "closeBody", + value: function() { + var a = this; + if (this.displace) + if (i.transitions.supported) + this.body.css(this.side, 0).one(n, function() { + a.onCloseBody() + }); + else { + var b = this.getAnimation(m, "body"); + this.body.animate(b, { + queue: !1, + duration: this.speed, + complete: function() { + a.onCloseBody() + } + }) + } + } + }, { + key: "moveBody", + value: function(a) { + a === l ? this.openBody() : this.closeBody() + } + }, { + key: "onOpenMenu", + value: function(a) { + var b = this.name; + h.moving=!1, h.opened = b, this.item.unbind(n), this.body.removeClass(k).addClass(this.openClass), this.onOpenEndCallback(), "function" == typeof a && a(b) + } + }, { + key: "openMenu", + value: function(a) { + var b = this, c = this.item; + if (i.transitions.supported) + c.css(this.side, 0).one(n, function() { + b.onOpenMenu(a) + }); + else { + var d = this.getAnimation(l, "menu"); + c.css("display", "block").animate(d, { + queue: !1, + duration: this.speed, + complete: function() { + b.onOpenMenu(a) + } + }) + } + } + }, { + key: "onCloseMenu", + value: function(a) { + this.item.css({ + left: "", + right: "" + }).unbind(n), j("html").css("overflow-x", ""), h.moving=!1, h.opened=!1, this.body.removeClass(k).removeClass(this.openClass), this.onCloseEndCallback(), "function" == typeof a && a(name) + } + }, { + key: "closeMenu", + value: function(a) { + var b = this, c = this.item; + if (i.transitions.supported) + c.css(this.side, "").one(n, function() { + b.onCloseMenu(a) + }); + else { + var d = this.getAnimation(m, "menu"); + c.animate(d, { + queue: !1, + duration: this.speed, + complete: function() { + b.onCloseMenu() + } + }) + } + } + }, { + key: "moveMenu", + value: function(a, b) { + this.body.addClass(k), a === l ? this.openMenu(b) : this.closeMenu(b) + } + }, { + key: "move", + value: function(a, b) { + h.moving=!0, this.prepareBody(a), this.moveBody(a), this.moveMenu(a, b) + } + }, { + key: "open", + value: function(b) { + var c = this; + if (h.opened !== this.name&&!h.moving) { + if (h.opened!==!1) { + var d = new a(h.opened); + return void d.close(function() { + c.open(b) + }) + } + this.move("open", b), this.onOpenCallback() + } + } + }, { + key: "close", + value: function(a) { + h.opened !== this.name || h.moving || (this.move("close", a), this.onCloseCallback()) + } + }, { + key: "toggle", + value: function(a) { + h.opened === this.name ? this.close(a) : this.open(a) + } + } + ]), a + }(), p = jQuery, q = jQuery, r = ["open", "close", "toggle"], s = {}, t = function(b) { + return function(c, d) { + "function" == typeof c ? (d = c, c = "sidr") : c || (c = "sidr"), a(b, c, d) + } + }; + for (f = 0; f < r.length; f++) + g = r[f], s[g] = t(g); + var u = jQuery; + jQuery.sidr = b, jQuery.fn.sidr = d +}(); diff --git a/app/themes/singular/assets/stylesheets/singular/all.scss b/app/themes/singular/assets/stylesheets/singular/all.scss new file mode 100644 index 000000000..ee186f53a --- /dev/null +++ b/app/themes/singular/assets/stylesheets/singular/all.scss @@ -0,0 +1,32 @@ +// Import Helpy variables and boostrap overrides +@import "bootstrap-overrides"; + +// Import Bootstrap +// Note: "bootstrap-sprockets" must be imported before "bootstrap" and "bootstrap/variables" +@import "bootstrap-sprockets"; +@import "bootstrap"; + +// Import font-awesome +@import "font-awesome-sprockets"; +@import "font-awesome"; + +// Import other dependencies +@import "bootstrap-icon-chooser/css/icon-picker"; +@import "magnific-popup/dist/magnific-popup"; +@import "animate.css/animate.min"; + +// Note: need to specify .scss extension as there is also a .css file +// scss-lint:disable ImportPath +@import "bootstrap-social/bootstrap-social.scss"; +// scss-lint:enable ImportPath +@import "jquery-ui"; + +// Import all other custom Helpy stylesheets +@import "shared"; +@import "singular"; + +// Note: admin.scss is included separately via `layouts/admin.html.erb`. +@import "pick-a-color/build/1.2.3/css/pick-a-color-1.2.3.min"; + +// Import css for autocomplete +@import "autocomplete"; diff --git a/app/themes/singular/assets/stylesheets/singular/bootstrap-overrides.scss b/app/themes/singular/assets/stylesheets/singular/bootstrap-overrides.scss new file mode 100644 index 000000000..5ee4ba83c --- /dev/null +++ b/app/themes/singular/assets/stylesheets/singular/bootstrap-overrides.scss @@ -0,0 +1,81 @@ +// Overrides for Twitter Bootstrap Variables +// See: https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss +// Note: Only configure variables here if changed from the defaults linked above. +// This helps identify which Bootstrap variables we've actually overridden. + +// Colours: Brand Defaults +$brand-primary: #3cceff; +$brand-link: #004084; +$link-color: #666; +$table-border-color: #fff; +$breadcrumb-bg: #eee; +$body-bg: #efefef; + +// Navbar links +$navbar-default-link-color: #004084; +$navbar-default-color: #fff; +$navbar-default-bg: #fff; +$navbar-default-border: #fff; + +$navbar-default-link-color: $brand-link; +$navbar-default-link-active-color: $brand-link; +$navbar-default-link-active-bg: #fff; + +// Typography +$font-family-sans-serif: "Roboto", sans-serif; +$font-family-serif: 'Roboto', serif; + +// Theme Specific Colors +$header-bg: #eee; +$header-link-color: #666; +$font-size-base: 16px; +$main-panel-bg: #eee; +$main-panel-font-color: #333; +$main-panel-breadcrumb-color: $main-panel-font-color; +$icon-color: $main-panel-bg; +$icon-bg-color: #eee; + + +// LEGACY LESS CODE FOLLOWS: +// TODO: Replace as necessary with SCSS equivalents + +// // Set the correct sprite paths +// @iconSpritePath: image-url("twitter/bootstrap/glyphicons-halflings.png"); +// @iconWhiteSpritePath: image-url("twitter/bootstrap/glyphicons-halflings-white.png"); + +// // Set the Font Awesome (Font Awesome is default. You can disable by commenting below lines) +// //@fontAwesomeEotPath: font-url("fontawesome-webfont.eot"); +// //@fontAwesomeEotPath_iefix: font-url("fontawesome-webfont.eot?#iefix"); +// //@fontAwesomeWoffPath: font-url("fontawesome-webfont.woff"); +// //@fontAwesomeTtfPath: font-url("fontawesome-webfont.ttf"); +// //@fontAwesomeSvgPath: font-url("fontawesome-webfont.svg#fontawesomeregular"); + +// // Font Awesome +// //@import "fontawesome/font-awesome"; +// //@fa-font-path: "/assets"; +// //@import "font-awesome-sprockets"; +// //@import "font-awesome"; + +// // Glyphicons + +// // Glyphicons are not required by default, uncomment the following lines to enable them. +// @glyphiconsEotPath: font-url("glyphicons-halflings-regular.eot"); +// @glyphiconsEotPath_iefix: font-url("glyphicons-halflings-regular.eot?#iefix"); +// @glyphiconsWoffPath: font-url("glyphicons-halflings-regular.woff"); +// @glyphiconsTtfPath: font-url("glyphicons-halflings-regular.ttf"); +// @glyphiconsSvgPath: font-url("glyphicons-halflings-regular.svg#glyphicons_halflingsregular"); + +// @import "twitter/bootstrap/glyphicons.less"; + +// //** Load fonts from this directory. +// @icon-font-path: "../fonts/"; +// //** File name for all font files. +// @icon-font-name: "glyphicons-halflings-regular"; +// //** Element ID within SVG icon file. +// @icon-font-svg-id: "glyphicons_halflingsregular"; + +// @font-face { +// font-family: 'Glyphicons Halflings'; +// src: url('/assets/glyphicons-halflings-regular.eot'); +// src: url('/assets/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('/assets/glyphicons-halflings-regular.woff') format('woff'), url('/assets/glyphicons-halflings-regular.ttf') format('truetype'), url('/assets/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg'); +// } diff --git a/app/themes/singular/assets/stylesheets/singular/singular.scss b/app/themes/singular/assets/stylesheets/singular/singular.scss new file mode 100644 index 000000000..95971f711 --- /dev/null +++ b/app/themes/singular/assets/stylesheets/singular/singular.scss @@ -0,0 +1,1161 @@ +html { + height: 100%; +} + +html, +body { + max-width: 100%; + overflow-x: hidden; +} + +body { + width: 100%; + min-height: 100%; + position: relative; + background-color: $body-bg; +} + +.content { + margin-bottom: 100px; + height: 100%; + background-color: $body-bg; +} + +main { + background-color: $body-bg; +} + +input, +select { + min-height: 60px; +} + +a { + color: $link-color; + text-decoration: none; + overflow: hidden; + + &:hover { + color: $link-color; + font-weight: bold; + } + + &.list-group-item { + background-color: $body-bg; + border: 0px; + padding-left: 2px; + padding-top: 4px; + padding-bottom: 4px; + } +} + +tr { + height: 50px; + + &.even td { + border-top: 1px #ddd solid; + background-color: #fff; + padding-top:5px; + } + + &.odd td { + border-top: 1px #ddd solid; + padding-top:5px; + } +} + +th { + font-size: 100%; + color: #666; + border-bottom: #ddd solid 1px; + text-transform: uppercase; +} + +td, +th { + padding-left: 5px; + padding-right: 5px; + padding-top: 15px; + padding-bottom: 15px; +} + +tbody { + border: 0px solid #333; +} + +h1 { + font-family: $font-family-sans-serif; + padding-bottom: 0px; + padding-left: 0px; + padding-right: 5px; + padding-top: 5px; + margin-top: 0px; + margin-bottom: 0px; +} + +i.circle-icon { + color: $icon-color; +} + +@media(max-width:767px){ + .jumbotron h1, h1 { + font-size: 200%; + } + + h2 { + font-size: 150%; + } + + .article-block { + } + + i.circle-icon { + display: inline-block; + border-radius: 60px; + box-shadow: 0px 0px 2px #888; + font-size: 24px; + padding: 20px; + margin-bottom: 20px; + } + + i.main-icon { + font-size: 24px; + } + + .article-icon { + margin-bottom: 20px; + } +} + +@media(min-width:768px){ + .jumbotron h1, h1 { + font-size: 200%; + } + + i.circle-icon { + display: inline-block; + border-radius: 60px; + box-shadow: 0px 0px 2px #888; + font-size: 34px; + padding: 25px; + margin-bottom: 20px; + } + + .main-icon { + font-size: 34px; + } + + .article-icon { + margin-bottom: 20px; + } +} + +@media(min-width:992px){ + .jumbotron h1, h1 { + font-size: 300%; + } + .article-block { + min-height: 250px; + } + + i.circle-icon { + display: inline-block; + border-radius: 60px; + box-shadow: 0px 0px 2px #888; + font-size: 34px; + padding: 25px; + margin-bottom: 20px; + } + i.main-icon { + font-size: 34px; + } +} + +@media(min-width:1200px){ + .jumbotron h1, h1 { + font-size: 300%; + } + .article-block { + min-height: 250px; + } + + i.circle-icon { + display: inline-block; + border-radius: 60px; + box-shadow: 0px 0px 2px #888; + font-size: 44px; + padding: 30px; + margin-bottom: 20px; + } + i.main-icon { + font-size: 44px; + } +} + +textarea { + width:98%; + height:175px; + padding:5px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size:12px; + color:#333; + line-height:1.5; + border:1px solid #c4c4c4; + border-right:1px solid #e9e9e9; + border-bottom:1px solid #e9e9e9; +} + +// === GLOBAL STYLES === + +.doc-meta { + margin-top: 60px; +} + +.truncate-ellipsis { + display: table; + table-layout: fixed; + width: 100%; + white-space: nowrap; + + > * { + display: table-cell; + overflow: hidden; + text-overflow: ellipsis; + } +} + +#page-title { + padding-top: 30px; + padding-bottom: 10px; + margin-bottom: 15px; + max-height: 150px; + + h2 { + margin-top: 0; + } + + &.row { + padding-top: 10px; + } +} + +#page-title-right > div > form { + margin-top: 20px; +} + +ul.breadcrumb { + margin-bottom: 0px; + padding-left: 0px; + padding-bottom: 0px; +} + +#upper-wrapper { + height: 180px; + padding: 0px; + background-color: #333; + + > div { + padding-top: 10px; + padding-bottom: 0px; + } +} + +div.add-form { + padding: 10px; + margin-top: 40px; +} + +div.widget-form { + //background-color: rgba(192,255,193,0.25); + padding: 10px; + margin-top: 0px; +} + +.add-screenshots { + margin-top: 10px; + margin-bottom: 10px; +} + +.attachinary-thumbnails { + margin-top: 10px; +} + +.form-subhead { + margin-bottom: 25px; +} + +label { + font-weight: normal; + font-size: 100%; + color: #666; + &.required:after { + content: " *"; + } +} + +thead { + margin-bottom: 20px; +} + +div#tagged-with { + margin-top: 20px; + padding: 5px; +} + +div.admin-tools { + background-color: #eee; + padding: 5px; +} + +.border-bottom { + border-bottom: 1px solid #eee; +} + +.border-right { + border-right: 1px solid #eee; +} + +.tiny-header { + font-size: 100%; + color: #666; + font-weight: bold; + text-transform: uppercase; + padding-bottom: 5px; +} + +// === GLOBAL CLASSES === + +.logo { + padding-right: 5px; + margin-top: 10px; +} + +.label { + display: inline; + padding: .4em .6em .5em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.label-message-tagging { + margin-right: 2px; + margin-bottom: -15px; + text-transform: uppercase; +} + +.label-user-tagging { + margin-right: 2px; + text-transform: uppercase; +} + +.label-doc-tagging { + margin-left: 2px; + margin-right: 2px; + text-transform: uppercase; +} + +// Tag background colors +.label-a { + background-color: #1abc9c; +} +.label-b { + background-color: #16a085; +} +.label-c { + background-color: #f1c40f; +} +.label-d { + background-color: #f39c12; +} +.label-e { + background-color: #2ecc71; +} +.label-f { + background-color: #27ae60; +} +.label-g { + background-color: #e67e22; +} +.label-h { + background-color: #d35400; +} +.label-i { + background-color: #3498db; +} +.label-j { + background-color: #2980b9; +} +.label-k { + background-color: #e74c3c; +} +.label-l { + background-color: #e74c3c; +} +.label-m { + background-color: #9b59b6; +} +.label-n { + background-color: #8e44ad; +} +.label-o { + background-color: #bdc3c7; +} +.label-p { + background-color: #34495e; +} +.label-q { + background-color: #2c3e50; +} +.label-r { + background-color: #95a5a6; +} +.label-s { + background-color: #7f8c8d; +} +.label-t { + background-color: #ec87bf; +} +.label-u { + background-color: #d870ad; +} +.label-v { + background-color: #f69785; +} +.label-w { + background-color: #9ba37e; +} +.label-x { + background-color: #b49255; +} +.label-y { + background-color: #b49255; +} +.label-z { + background-color: #a94136; +} + +.label-private { + border: 2px dashed #666; + color: #666; + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.label-collapsed { + position: relative; + top: -0.7em; + border: 1px solid #eee; + background-color: #fff; + color: #999; + display: inline; + padding: .2em .6em .3em; + font-size: 100%; + font-weight: normal; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + cursor: pointer; +} + +.label-count { + position: relative; + top: .5em; + border: 1px solid #999; + background-color: #fff; + color: #999; + display: inline; + font-size: 100%; + font-weight: normal; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.active { + // scss-lint:disable ImportantRule + background-color: #fff !important; +} + +.author { + font-weight: normal; + font-size: 85%; +} + +.more-important { + font-weight: bold; + font-size: 105%; +} + +span.more-important a { + font-weight: bold; + font-size: 105%; +} + +span.more-important a:link { + font-weight: bold; + font-size: 105%; +} + +.less-important { + color: #666; +} + +span.category-icon { + color: #666; + margin-bottom: 15px; + font-size: 120%; +} + +.user-thumbnail { + padding-right:10px; +} + +.status-label { + margin-left: 5px; + margin-bottom: -15px; +} + +.no-side-pad { + padding-left: 0px; + padding-right: 0px; +} + +.space-h2 { + margin-bottom: 50px; +} + +.verticalness { + padding-top: 150px; + padding-bottom: 150px; +} + +.last-active { + white-space: nowrap; +} + +.full-width { + width: 100%; +} + +.content-row { + margin-top: 40px; +} + +ul.dropdown-menu li a.strong { + font-weight: bolder; +} + +.active-item-shadow { + box-shadow: rgba(133, 133, 133, 0.1) 0 0 5px 2px, rgba(133, 133, 133, 0.3) 0 0 2px; +} + +// These styles hide the placeholder text for the web version, and they are are overridden on the widget: + +.topic-placeholder::-webkit-input-placeholder { + color: white; +} + +.topic-placeholder:-moz-placeholder { + color: white; +} + +.topic-placeholder::-moz-placeholder { + color: white; +} + +.topic-placeholder:-ms-input-placeholder { + color: white; +} + +// == MODAL === + +.modal-header { + border-bottom: 0; +} + +.login-button, +.reset-button { + margin-top: 30px; + margin-bottom: 40px; +} + +.badge-light { + background-color: #eee; +} + +.locale-badges { + padding-right: 20px; + padding-top: 10px; +} + +// === LAYOUT === + +header { + height: 50px; + background-color: $body-bg; + + a { + color: $header-link-color; + + &:hover { + color: $header-link-color; + font-weight: bold; + } + + } +} + +#breadcrumbs { + margin-bottom: 20px; +} + +#top-bar { + height: 5px; + background-color: #3cceff; +} + +#left-nav ul { + list-style-image: none; + + li { + font-size: 120%; + background-color: #fff; + list-style-image: none; + display: block; + + a { + font-weight: normal; + text-decoration: none; + display: block; + margin-right: 5px; + padding: 8px; + list-style-image: none; + margin-bottom: 5px; + + // scss-lint:disable NestingDepth + + &:hover { + font-weight: normal; + text-decoration: none; + display: block; + background-color: #eee; + padding: 8px; + list-style-image: none; + } + + &.current { + font-weight: normal; + text-decoration: none; + display: block; + margin-right: 5px; + padding: 8px; + background-color: #555; + } + } + } +} + +#body-wrapper { + margin-top: 30px; + margin-bottom: 80px; +} + +#did-this-help { + margin-top: 80px; + + h3 { + margin-top: 20px; + } +} + +#did-this-help-buttons { + margin-top: 20px; +} + +.search-btn { + width: 100%; + + span { + color: #333 !important; + } +} + +.nav { + padding-top: 30px; + + .search-li { + padding-left: 15px; + padding-right: 15px; + margin-top: 30px; + } +} + +// === HEADER === + +.flat-nav-container { + background-color: $main-panel-bg; +} + +.toggle-button { + color: $header-link-color; +} + + +// === BOOTSTRAP OVERRIDES == + +.jumbotron { + background-color: #fff; + margin-bottom: 0px; +} + +.navbar-brand { + color: $header-link-color; + font-family: 'Raleway', sans-serif; + padding:0px; + font-size:120%; + line-height: 50px; + font-weight: bold; + + a { + color: #666; + } +} + +.navbar { + margin-bottom: 0px; + background-color: #fff; + border: 0px; + padding-bottom: 5px; + + > li > a { + background-color: #fff; + } +} + +.navbar-form { + padding-top: 0; + padding-bottom: 0; +} + +.navbar-form .input-group .form-control { +} + +.navbar-right { + margin-right: -45px; +} + +.navbar-toggle { + margin-right: 0px; +} + +.form-group { + width:100%; +} + +.navbar-nav .form-control { + width: 86%; + margin-left: 14px; +} + +.pagination>.active>a, +.pagination>.active>span, +.pagination>.active>a:hover, +.pagination>.active>span:hover, +.pagination>.active>a:focus, +.pagination>.active>span:focus { + background-color: #666; + border-color: #666; +} + +// ul.breadcrumb { +// background-color: $main-panel-bg; +// } + +.breadcrumb > li + li:before { + content: ">\00a0"; + padding: 0 5px; + color: $main-panel-breadcrumb-color; +} + +.breadcrumb > .active { + color: $main-panel-breadcrumb-color; +} + +.dropdown-menu { + min-width: 300px; +} + +.label-tagging { + background-color: #6ECBEC; + margin-right: 2px; +} + +// === HOME PAGE === + +h1#how-help { + padding-top:50px; + padding-bottom:70px; +} + +.flat-main-panel { + background-color: $main-panel-bg; + color: $main-panel-font-color; + + a { + color: $main-panel-breadcrumb-color; + } +} + +#home-search { + background-color: $main-panel-bg; + padding-bottom: 50px; +} +input#search-field { + height: 60px; + opacity: 0.6; +} +#featured-categories, +#featured-team { + margin-top: 80px; + margin-bottom: 80px; + // background-color: #fff; + margin-left: 20px; + margin-right: 20px; +} + +.dummy { + margin-top: 110%; +} +.thumbnail { + position: absolute; + top: 0px; + bottom: 0px; + left: 5px; + right: 5px; + padding-left: 5px; + padding-right: 5px; + text-align:center; + padding-top:calc(40% - 30px); +} + +.right-inner-addon { + position: relative; + + input { + padding-right: 30px; + } + + i { + position: absolute; + right: 0px; + padding: 10px 12px; + pointer-events: none; + } +} + +li.team-member { + padding-left: 10px; + padding-right: 10px; +} + +ul.locale-list li { + border-bottom: 1px solid #eee; + padding-top: 5px; + padding-bottom: 5px; +} + +.doc-panel, +.category-panel { + border: 1px solid #ddd; + background-color: #fff; + margin-bottom: 20px; + min-height: 120px +} + +.doc-show-panel { + padding: 50px; +} + +.home-icon { + font-size: 48px; + color: #ddd; +} + +// === KNOWLEDGEBASE === + + +#left-col.affix-top { + position: static; + margin-top:22px; + width:228px; + } + +ul.articles { + padding-bottom: 20px; +} + +li.article { + padding-top: 5px; + + > span { + font-size: 70%; + } +} + +li.article-more { + padding-left: 12px; +} + +h5.category-header { + font-size: 110%; + font-weight: 600; + + > a { + color: #333; + } +} + +.doc-meta { + margin-bottom: 30px; +} + +.meta-header { + margin-bottom: 5px; +} + +.text-content { + a { + color: #004084; + } + img { + max-width: 90%; + height: auto; + margin-left: 5%; + margin-right: 5%; + } +} + +// === SEARCH === + +div#results-found { + margin-top: 15px; + margin-bottom: 30px; +} + +// === HEADER STYLES === + +#above-header { + font-size: 85%; + padding-top: 0px; + margin-bottom: 5px; + margin-top: 0px; + text-align: right; +} + +// #main-body { +// margin-top: 10px; +// } +// +// div#main-body { +// height: 100%; +// } + +#buttons ul.buttons li img { + margin-right: 10px; +} + +div.status { + float: right; + margin-left: 30px; +} + +// === FORUM === + +table#forums, +table#topics, +table#posts, +table#users { + width: 100%; + margin-bottom: 20px; +} + +#forums tbody tr, +#users tbody tr, +#forums tbody tr td { + border-bottom: 1px solid #eee; +} + +// === DISCUSSION === + +.collapsed-posts { + margin-top: 40px; + margin-bottom: 40px; + height: 1px; + background-color: #eee; +} + +.post-container { + padding: 5px; +} + +.post-body { + padding-top: 15px; + padding-left: 0px; +} + +.kind-reply, +.kind-note { + margin-left: 50px; +} + +.kind-note { + color: #999; +} + +.voting-widget { + text-align: center; +} + +.vote-icon { + font-size: 80%; +} + +.grid-vote { + margin-top: 20px; +} + +.topic-points { + font-size: 140%; + font-weight: bold; + margin-top: -5px; +} + +// === MAIN PANEL === + +// #main-content { +// // font-size: 110%; +// background-color: #fff; +// } + +#flash-message { + height: 20px; + font-size: 110%; +} + +// === FOOTER === + +#get-help-wrapper { + background-color: #FFDF91; + padding-top: 20px; + padding-bottom: 20px; +} + +#footer { + color: #777; + background-color: #222; + padding-top: 20px; + padding-bottom: 20px; + padding-left: 10px; + + a { + color: #888; + text-decoration: none; + } + + h5 { + color: #fff; + } +} + +#footer-wrapper { + position: absolute; + width: 100%; + bottom: 0; + background-color: #222; +} + +// === IDEABOARD === + +.idea-box { + max-height: 120px; + + .thumbnail { + position: absolute; + top: 0px; + bottom: 0px; + left: 5px; + right: 5px; + padding-left: 5px; + padding-right: 5px; + text-align:center; + padding-top: 10px; + max-height: 100px; + } +} + +// === Q&A === + +.asked-question { + margin-bottom: 5px; + font-weight: normal; +} + +div.asked-question span.more-important { + font-weight: normal; + font-size: 100%; +} + +h4.qna { + margin-top: 0px; + font-weight: bold; + margin-bottom: 15px; +} + +// === OTHER MEDIA QUERIES === + +@media(max-width:767px){ + .dummy { + margin-top: 110px; + } + .thumbnail { + position: absolute; + top: 0px; + bottom: 0px; + left: 5px; + right: 5px; + padding-left: 5px; + padding-right: 5px; + text-align:center; + padding-top: 10px; + max-height: 100px; + } +} + +.bo-kb-section { + padding-left: 100px; + padding-right: 100px; +} + +.main-option-container { + border: 1px solid #ccc; + margin-left: 10px; + margin-right: 10px; + margin-bottom: 30px; +} diff --git a/app/themes/singular/assets/stylesheets/singular/slideout.scss b/app/themes/singular/assets/stylesheets/singular/slideout.scss new file mode 100644 index 000000000..c92031ee7 --- /dev/null +++ b/app/themes/singular/assets/stylesheets/singular/slideout.scss @@ -0,0 +1,37 @@ +// .slideout-menu { +// position: fixed; +// left: auto; +// top: 0; +// bottom: 0; +// right: 0; +// z-index: 0; +// width: 256px; +// overflow-y: auto; +// -webkit-overflow-scrolling: touch; +// display: none; +// background-color: #000; +// +// a { +// color: white; +// +// &:hover { +// color: black; +// } +// } +// } +// +// .slideout-panel { +// position:relative; +// z-index: 1; +// will-change: transform; +// } +// +// .slideout-open, +// .slideout-open body, +// .slideout-open .slideout-panel { +// overflow: hidden; +// } +// +// .slideout-open .slideout-menu { +// display: block; +// } diff --git a/app/themes/singular/locales/.gitkeep b/app/themes/singular/locales/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/app/themes/singular/singular.png b/app/themes/singular/singular.png new file mode 100644 index 000000000..ab2d34f4d Binary files /dev/null and b/app/themes/singular/singular.png differ diff --git a/app/themes/singular/views/categories/_category_panel.html.erb b/app/themes/singular/views/categories/_category_panel.html.erb new file mode 100644 index 000000000..f2b22552e --- /dev/null +++ b/app/themes/singular/views/categories/_category_panel.html.erb @@ -0,0 +1,14 @@ +<% category = category_panel %> +
    +
    + <%= content_tag :i, nil, class: "glyphicon glyphicon-#{category.icon} home-icon" %> +
    +
    +

    <%= link_to category.name, category_path(category), id: "category-#{category.id}" %>

    +

    + <% category.docs.with_translations(I18n.locale).ordered.active.limit(5).each_with_index do |doc| -%> + <%= link_to doc.title, doc_path(doc) %> + <% end %> +

    +
    +
    diff --git a/app/themes/singular/views/categories/index.html.erb b/app/themes/singular/views/categories/index.html.erb new file mode 100644 index 000000000..29ad860ff --- /dev/null +++ b/app/themes/singular/views/categories/index.html.erb @@ -0,0 +1,26 @@ +<% title "#{AppSettings['settings.site_name']}: #{@page_title}" %> +<% meta_tag :description, "Knowledgebase for #{AppSettings['settings.site_name']}" %> +<% meta_tag :keywords, "Knowledgebase, Knowledge base, support, articles, documentation, how-to, faq, frequently asked questions" %> + +
    + <% if @categories.count == 0 %> + +
    + <%= t(:nothing_here) %> +
    + + <% else %> +
    + <% @categories.each do |category| %> + <%# if category.docs.all.with_translations(I18n.locale).count > 0 %> + <%= render partial: 'categories/category_panel', object: category %> + <%# end %> + <% end %> +
    + <% if Doc.all.with_translations(I18n.locale).count == 0 %> +
    + <%= t(:nothing_here) %> +
    + <% end %> + <% end %> +
    diff --git a/app/themes/singular/views/categories/show.html.erb b/app/themes/singular/views/categories/show.html.erb new file mode 100644 index 000000000..7ebfd9bc7 --- /dev/null +++ b/app/themes/singular/views/categories/show.html.erb @@ -0,0 +1,23 @@ +<% title "#{AppSettings['settings.site_name']}: #{@page_title}" %> +<% meta_tag :description, "#{@category.meta_description}" %> +<% meta_tag :keywords, "#{@category.keywords}" %> + +
    + <% @docs.each do |doc| %> + <%= render "docs/doc_panel", doc: doc %> + <% end %> + <% if @docs.count == 0 %> + +
    + <%= t(:nothing_here) %> +
    + + <% end %> + +
    +
    + <%= paginate @docs %> +
    +
    + +
    diff --git a/app/themes/singular/views/devise/confirmations/new.html.erb b/app/themes/singular/views/devise/confirmations/new.html.erb new file mode 100644 index 000000000..2f2ca836c --- /dev/null +++ b/app/themes/singular/views/devise/confirmations/new.html.erb @@ -0,0 +1,12 @@ +<% content_for :page_title do %> +

    <%= t(:resend) %>

    +<% end %> +<%= bootstrap_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= devise_error_messages! %> + + <%= f.email_field :email, autofocus: true %> + <%= f.submit "Resend confirmation instructions" %> + +<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/themes/singular/views/devise/invitations/edit.html.erb b/app/themes/singular/views/devise/invitations/edit.html.erb new file mode 100644 index 000000000..855a78c64 --- /dev/null +++ b/app/themes/singular/views/devise/invitations/edit.html.erb @@ -0,0 +1,14 @@ +<% content_for :page_title do %> +

    <%= t 'devise.invitations.edit.header' %>

    +<% end %> + +<%= simple_form_for resource, as: resource_name, url: invitation_path(resource_name), html: { method: :put } do |f| %> + <%= devise_error_messages! %> + <%= f.hidden_field :invitation_token %> + + <%= f.input :name, input_html: { value: nil } %> + <%= f.input :password %> + <%= f.input :password_confirmation %> + + <%= f.button :submit, t("devise.invitations.edit.submit_button") %> +<% end %> diff --git a/app/themes/singular/views/devise/mailer/confirmation_instructions.html.erb b/app/themes/singular/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 000000000..0e5c147e7 --- /dev/null +++ b/app/themes/singular/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

    <%= I18n.t('devise.mailer.confirmation_instructions.greeting', recipient: @email) %>

    # @email + +

    <%= I18n.t('devise.mailer.confirmation_instructions.instruction') %>

    + +

    <%= link_to I18n.t('devise.mailer.confirmation_instructions.action'), confirmation_url(@resource, host: AppSettings['settings.site_url'], confirmation_token: @token) %>

    diff --git a/app/themes/singular/views/devise/mailer/invitation_instructions.fa.html.erb b/app/themes/singular/views/devise/mailer/invitation_instructions.fa.html.erb new file mode 100644 index 000000000..d63a607ee --- /dev/null +++ b/app/themes/singular/views/devise/mailer/invitation_instructions.fa.html.erb @@ -0,0 +1,15 @@ +
    +

    <%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %>

    +

    <%= t("devise.mailer.invitation_instructions.someone_invited_you", url: AppSettings['settings.site_name']) %>

    +

    <%= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, :invitation_token => @token, host: AppSettings['settings.site_url']) %>

    + + <% if @resource.invitation_due_at %> +

    <%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %>

    + <% end %> + + <% if @resource.invitation_message %> +

    <%= @resource.invitation_message %>

    + <% end %> + +

    <%= t("devise.mailer.invitation_instructions.ignore").html_safe %>

    +
    diff --git a/app/themes/singular/views/devise/mailer/invitation_instructions.html.inky b/app/themes/singular/views/devise/mailer/invitation_instructions.html.inky new file mode 100644 index 000000000..bf30f47f3 --- /dev/null +++ b/app/themes/singular/views/devise/mailer/invitation_instructions.html.inky @@ -0,0 +1,23 @@ + + + + + +

    <%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %>

    + +

    <%= t("devise.mailer.invitation_instructions.someone_invited_you", url: AppSettings['settings.site_name']) %>

    + +

    <%= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, :invitation_token => @token, host: AppSettings['settings.site_url']) %>

    + + <% if @resource.invitation_due_at %> +

    <%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %>

    + <% end %> + + <% if @resource.invitation_message %> +

    <%= @resource.invitation_message %>

    + <% end %> + +

    <%= t("devise.mailer.invitation_instructions.ignore").html_safe %>

    +
    +
    +
    diff --git a/app/themes/singular/views/devise/mailer/reset_password_instructions.fa.html.erb b/app/themes/singular/views/devise/mailer/reset_password_instructions.fa.html.erb new file mode 100644 index 000000000..dbeac5d07 --- /dev/null +++ b/app/themes/singular/views/devise/mailer/reset_password_instructions.fa.html.erb @@ -0,0 +1,9 @@ +
    +

    <%= I18n.t('devise.mailer.reset_password_instructions.greeting', recipient: @resource.email) %>

    +

    <%= I18n.t('devise.mailer.reset_password_instructions.instruction') %>

    + +

    <%= link_to I18n.t('devise.mailer.reset_password_instructions.action'), edit_password_url(@resource, host: AppSettings['settings.site_url'], reset_password_token: @token) %>

    + +

    <%= I18n.t('devise.mailer.reset_password_instructions.instruction_2') %>

    +

    <%= I18n.t('devise.mailer.reset_password_instructions.instruction_3') %>

    +
    diff --git a/app/themes/singular/views/devise/mailer/reset_password_instructions.html.inky b/app/themes/singular/views/devise/mailer/reset_password_instructions.html.inky new file mode 100644 index 000000000..37ab043c0 --- /dev/null +++ b/app/themes/singular/views/devise/mailer/reset_password_instructions.html.inky @@ -0,0 +1,21 @@ + + + + + +

    <%= I18n.t('devise.mailer.reset_password_instructions.greeting', recipient: @resource.email) %>

    +

    <%= I18n.t('devise.mailer.reset_password_instructions.instruction') %>

    + + +
    + +
    + + +

    <%= I18n.t('devise.mailer.reset_password_instructions.instruction_2') %>

    +

    <%= I18n.t('devise.mailer.reset_password_instructions.instruction_3') %>

    +
    +
    +
    diff --git a/app/themes/singular/views/devise/mailer/unlock_instructions.html.erb b/app/themes/singular/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 000000000..5332e08db --- /dev/null +++ b/app/themes/singular/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,6 @@ +

    <%= I18n.t('devise.mailer.unlock_instructions.greeting', recipient: @resource.email) %>

    + +

    <%= I18n.t('devise.mailer.unlock_instructions.message') %>

    + +

    <%= I18n.t('devise.mailer.unlock_instructions.instruction') %>

    +

    <%= link_to I18n.t('devise.mailer.unlock_instructions.action'), unlock_url(@resource, host: AppSettings['settings.site_url'], unlock_token: @token) %>

    diff --git a/app/themes/singular/views/devise/passwords/edit.html.erb b/app/themes/singular/views/devise/passwords/edit.html.erb new file mode 100644 index 000000000..2bfac23fa --- /dev/null +++ b/app/themes/singular/views/devise/passwords/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :page_title do %> +

    <%= params[:first] == 'true' ? t('devise.sessions.new.sign_in') : t('devise.passwords.edit.change_my_password') %>

    +<% end %> + +<%= bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= devise_error_messages! %> + <%= f.hidden_field :reset_password_token %> + + <%= f.password_field :password, autofocus: true, autocomplete: "off", label:I18n.t('devise.passwords.edit.new_password') %> + <%= f.password_field :password_confirmation, autocomplete: "off", label: I18n.t('devise.passwords.edit.confirm_new_password') %> + <%= f.submit I18n.t('devise.passwords.edit.change_my_password'), class: 'btn btn-warning' %> +<% end %> diff --git a/app/themes/singular/views/devise/passwords/new.html.erb b/app/themes/singular/views/devise/passwords/new.html.erb new file mode 100644 index 000000000..5ff4fd1f8 --- /dev/null +++ b/app/themes/singular/views/devise/passwords/new.html.erb @@ -0,0 +1,13 @@ +<% content_for :page_title do %> +

    <%= t('devise.passwords.new.forgot_your_password') %>

    +<% end %> + +<%= bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= devise_error_messages! %> + + <%= f.email_field :email, autofocus: true %> + +
    + <%= f.submit t('devise.passwords.new.send_me_reset_password_instructions'), class: 'btn btn-warning' %> +
    +<% end %> diff --git a/app/themes/singular/views/devise/registrations/edit.html.erb b/app/themes/singular/views/devise/registrations/edit.html.erb new file mode 100644 index 000000000..662e6be88 --- /dev/null +++ b/app/themes/singular/views/devise/registrations/edit.html.erb @@ -0,0 +1,49 @@ +<% content_for :page_title do %> +

    <%= t(:your_profile) %>

    +<% end %> + +
    + <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), validate: true, multipart: true, html: { method: :put }) do |f| %> + +
    + <%= avatar_image(@user, size=150) %> +
    + <% if @user.provider.nil? %> + <%= f.attachinary_file_field :avatar if cloudinary_enabled? %> + <% end %> + <%= f.file_field :profile_image unless cloudinary_enabled? %> +
    +
    + +
    + <%= devise_error_messages! %> + + <%= f.input :email, autofocus: true %> + <%= f.input :name %> + <%= f.input :bio, input_html: {:rows => 4, :cols => 60} %> + <%= f.input :company %> + <%= f.input :title %> + <%= f.input :time_zone %> + <%= f.input :signature, input_html: {:rows => 4, :cols => 60} if current_user.is_admin? %> + + + <% if @user.provider.nil? %> + <%= f.input :current_password, autocomplete: "off" %> + +

    +

    <%= t('devise.passwords.edit.change_your_password') %>

    +
    + + <%= f.input :password, autocomplete: "off" %> + <%= f.input :password_confirmation, autocomplete: "off" %> + + <% end %> + + <%= f.submit t("save_changes"), class: 'btn btn-warning' %> +
    + + + <% end %> + + +
    diff --git a/app/themes/singular/views/devise/registrations/new.html.erb b/app/themes/singular/views/devise/registrations/new.html.erb new file mode 100644 index 000000000..8e925ac32 --- /dev/null +++ b/app/themes/singular/views/devise/registrations/new.html.erb @@ -0,0 +1,26 @@ +<% content_for :page_title do %> +

    <%= I18n.t('devise.registrations.new.sign_up') %>

    +<% end %> + +<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), validate: true) do |f| %> + <%= devise_error_messages! %> + + <%= f.input :email, autofocus: true %> + <%= f.input :name %> +
    + <%= f.attachinary_file_field :avatar if cloudinary_enabled? %> + <%= f.file_field :profile_image, label: "Avatar" unless cloudinary_enabled? %> +
    + <%= f.input :bio, input_html: {:rows => 4, :cols => 60} %> + <%= f.input :company %> + <%= f.input :title %> + <%= f.input :signature, input_html: {:rows => 4, :cols => 60} %> + + <% if @validatable %> + + <% end %> + <%= f.input :password, autocomplete: "off" %> + <%= f.input :password_confirmation, autocomplete: "off" %> + + <%= f.submit I18n.t('devise.registrations.new.sign_up'), class: 'btn btn-warning' %> +<% end %> diff --git a/app/themes/singular/views/devise/sessions/new.html.erb b/app/themes/singular/views/devise/sessions/new.html.erb new file mode 100644 index 000000000..e48f096f0 --- /dev/null +++ b/app/themes/singular/views/devise/sessions/new.html.erb @@ -0,0 +1,7 @@ +<% content_for :page_title do %> +

    <%= t('devise.sessions.new.sign_in') %>

    +<% end %> + +
    + <%= render 'layouts/login' %> +
    diff --git a/app/themes/singular/views/devise/shared/_links.html.erb b/app/themes/singular/views/devise/shared/_links.html.erb new file mode 100644 index 000000000..92f1db27f --- /dev/null +++ b/app/themes/singular/views/devise/shared/_links.html.erb @@ -0,0 +1,29 @@ + diff --git a/app/themes/singular/views/devise/unlocks/new.html.erb b/app/themes/singular/views/devise/unlocks/new.html.erb new file mode 100644 index 000000000..968efd57e --- /dev/null +++ b/app/themes/singular/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

    Resend unlock instructions

    + +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= devise_error_messages! %> + +
    + <%= f.label :email %>
    + <%= f.email_field :email, autofocus: true %> +
    + +
    + <%= f.submit "Resend unlock instructions" %> +
    +<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/themes/singular/views/docs/_doc_panel.html.erb b/app/themes/singular/views/docs/_doc_panel.html.erb new file mode 100644 index 000000000..5ac265f33 --- /dev/null +++ b/app/themes/singular/views/docs/_doc_panel.html.erb @@ -0,0 +1,8 @@ +
    +
    +

    <%= link_to doc.title, doc_path(doc), id: "doc#{doc.id}" %>

    +

    + <%= truncate(strip_tags(doc.body), length: 150) %> +

    +
    +
    diff --git a/app/themes/singular/views/docs/show.html.erb b/app/themes/singular/views/docs/show.html.erb new file mode 100644 index 000000000..d38746553 --- /dev/null +++ b/app/themes/singular/views/docs/show.html.erb @@ -0,0 +1,90 @@ +<% +################################################################################ +# NOTE: It is STRONGLY suggested that if you wish to customize the look and feel +# of your Helpy, that you create a custom theme instead. This will allow +# you to override any views or styles you wish to, without compromising your +# ability to upgrade in the future +################################################################################ +%> + +<% title "#{AppSettings['settings.site_name']}: #{renderize_doc_title(@doc.title_tag, @doc.title)}" %> +<% meta_tag :description, "#{@doc.meta_description}" %> +<% meta_tag :keywords, "#{@doc.keywords}" %> + + + + + + +
    +
    + <%= sanitize_doc_content(@doc.content) %> +
    +
    + +
    + + +<% if @doc.allow_comments && forums? %> +
    +
    +
    +

    <%= @posts.blank? ? "" : t(:topic) %>

    +
    + +
    + + + + <%= render :partial => 'comment', :collection => @posts %> +
    + + <% if user_signed_in? # show a form to reply or start discussion %> + <%= render partial: 'docs/comment_form', locals: {post: @post} %> + <% else # show a button to reply or start the discussion %> +
    + <% if @posts.blank? %> + + <% else %> + + <% end %> +
    + <% end %> +
    +
    +<% end %> + +
    + <%= render 'layouts/did_this_help' %> +
    diff --git a/app/themes/singular/views/home/index.html.erb b/app/themes/singular/views/home/index.html.erb new file mode 100644 index 000000000..adbbc67fe --- /dev/null +++ b/app/themes/singular/views/home/index.html.erb @@ -0,0 +1,35 @@ +<% title "#{AppSettings['settings.site_name']}: #{@page_title}" %> + +<% if knowledgebase? %> +
    + <% @categories.each do |category| %> + <%# if category.docs.all.with_translations(I18n.locale).count > 0 %> + <%= render partial: 'categories/category_panel', object: category %> + <%# end %> + <% end %> +
    +<% else %> +
    +
    +
    +
    + <%= link_to(new_topic_path) do %> + + <%= content_tag :h3, "#{t(:new_ticket, default: 'New ticket')}" %> + + <% end %> +
    +
    +
    +
    +
    + <%= link_to(tickets_path) do %> + + <%= content_tag :h3, "#{t(:your_tickets, default: 'Your tickets')}" %> + + <% end %> +
    +
    +
    +
    +<% end %> diff --git a/app/themes/singular/views/layouts/_above_header.html.erb b/app/themes/singular/views/layouts/_above_header.html.erb new file mode 100644 index 000000000..3659cd088 --- /dev/null +++ b/app/themes/singular/views/layouts/_above_header.html.erb @@ -0,0 +1,22 @@ +<%= link_to t(:your_tickets), tickets_path, Settings.tickets if user_signed_in? %> | +<%= link_to t(:knowledgebase), categories_path, class:'kblink' if Settings.knowledgebase %> | +<%= link_to t(:community), forums_path if Settings.forums %> | +<%= link_to t(:ask_a_question), new_topic_path %> | +<% unless user_signed_in? %> + <%= link_to t('devise.sessions.new.sign_in'), '#', class: 'login-link', data: {toggle: "modal", target: "#login-modal"} %> +<% else %> + <%= link_to t(:your_profile, :username => current_user.name), edit_user_registration_path %> | <%= link_to('Admin', admin_root_path) + " | " if current_user.admin? %><%= link_to t(:logout), destroy_user_session_path %> +<% end %> +<% if AppSettings['i18n.available_locales'].count > 1 %> + | + + + + +<% end %> diff --git a/app/themes/singular/views/layouts/_did_this_help.html.erb b/app/themes/singular/views/layouts/_did_this_help.html.erb new file mode 100644 index 000000000..5966ef3e9 --- /dev/null +++ b/app/themes/singular/views/layouts/_did_this_help.html.erb @@ -0,0 +1,9 @@ +
    + +

    <%= t(:did_this_help) %>

    +
    + + + + +
    diff --git a/app/themes/singular/views/layouts/_head.html.erb b/app/themes/singular/views/layouts/_head.html.erb new file mode 100644 index 000000000..87c0912cc --- /dev/null +++ b/app/themes/singular/views/layouts/_head.html.erb @@ -0,0 +1,31 @@ + +<%= content_for(:title) ? content_for(:title) : AppSettings['settings.site_name'] %> + + + + + + + + + + + +<%#= stylesheet_link_tag 'application', "data-turbolinks-track" => true %> +<%= stylesheet_link_tag 'singular/all', "data-turbolinks-track" => true %> +<%= javascript_include_tag "application", "data-turbolinks-track" => true %> +<%= javascript_include_tag "singular/all", "data-turbolinks-track" => true %> +<%= timeago_script_tag %> + +<%= @feed_link.html_safe if @feed_link %> + + +<%= csrf_meta_tags %> +<%= favicon_link_tag "#{AppSettings['design.favicon']}" %> +<%= cloudinary_js_config %> + +<%= render 'layouts/google_analytics' %> + + diff --git a/app/themes/singular/views/layouts/_header.html.erb b/app/themes/singular/views/layouts/_header.html.erb new file mode 100644 index 000000000..174943cce --- /dev/null +++ b/app/themes/singular/views/layouts/_header.html.erb @@ -0,0 +1,24 @@ + +
    + <%= nav_bar brand: image_tag("#{AppSettings['design.header_logo']}", width: 40, class: "pull-left logo") + "#{AppSettings['settings.product_name'] + " " if AppSettings['settings.product_name']}" "#{AppSettings['settings.site_name']}", responsive: true do %> + <%= menu_group pull: :right do %> + + <%= menu_divider %> + + <%= menu_item t(:your_tickets), tickets_path, Settings.tickets if user_signed_in? %> + <%= menu_item t(:knowledgebase), categories_path, class:'kblink' if Settings.knowledgebase %> + <%= menu_item t(:community), forums_path if Settings.forums %> + <%= menu_item t(:ask_a_question), new_topic_path %> + <%= menu_divider %> + <%= menu_item "#{t(:back_to).titleize} #{AppSettings['settings.parent_company']}", AppSettings['settings.parent_site'], :class => 'hidden-lg hidden-md hidden-sm border-bottom' %> + + <%= menu_item t(:locale), select_locale_path, :class => 'header-login hidden-lg hidden-md hidden-sm' %> + <% if user_signed_in? %> + <%= menu_item t(:your_profile), edit_user_registration_path, :class => 'header-login hidden-lg hidden-md hidden-sm' %> + <%= menu_item t(:logout), destroy_user_session_path, :class => 'hidden-lg hidden-md hidden-sm' %> + <% else %> + <%= menu_item t(:login), new_user_session_path, :class => 'hidden-lg hidden-md hidden-sm' %> + <% end %> + <% end %> + <% end %> +
    diff --git a/app/themes/singular/views/layouts/_page_title.html.erb b/app/themes/singular/views/layouts/_page_title.html.erb new file mode 100644 index 000000000..3ae335a62 --- /dev/null +++ b/app/themes/singular/views/layouts/_page_title.html.erb @@ -0,0 +1,10 @@ +
    + + <%= render_breadcrumbs('') %> + + <% if @page_title.present? %> +

    <%= @page_title %>

    + <% else %> + <%= yield :page_title %> + <% end %> +
    diff --git a/app/themes/singular/views/layouts/clean.html.erb b/app/themes/singular/views/layouts/clean.html.erb new file mode 100644 index 000000000..db5ccb056 --- /dev/null +++ b/app/themes/singular/views/layouts/clean.html.erb @@ -0,0 +1,121 @@ + + + + <%= render partial: 'layouts/head' %> + + + + + + +
    +
    +
    +
    +
    + <%= link_to image_tag("#{AppSettings['design.header_logo']}", width: 40, class: "pull-left logo") + "#{AppSettings['settings.product_name'] + " " if AppSettings['settings.product_name']}" "#{AppSettings['settings.site_name']}", root_path, class: 'navbar-brand', responsive: true %> +
    +
    +
      + <%= content_tag :li, link_to(t(:ask_a_question), new_topic_path), data: { hook: 'question_nav' }, class: 'hidden-xs' if forums? || tickets? %> + <% if user_signed_in? %> + <%= content_tag :li, link_to(t(:logout), destroy_user_session_path), class: 'hidden-xs' %> + <% else %> + <%= content_tag :li, link_to(t('devise.sessions.new.sign_in'), '#', class: 'login-link', data: { toggle: "modal", target: "#login-modal" }, class: 'hidden-xs') %> + <% end %> +
    • +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + <%= yield :home %> + <%= render 'layouts/page_title' unless params[:controller] == 'home' %> +
    +
    +
    +
    + +
    +
    + <%= bootstrap_flash %> + <%= yield %> +
    +
    +
    + +
    + + + + + + + diff --git a/app/themes/singular/views/layouts/singular.html.erb b/app/themes/singular/views/layouts/singular.html.erb new file mode 100644 index 000000000..75c2f9679 --- /dev/null +++ b/app/themes/singular/views/layouts/singular.html.erb @@ -0,0 +1,180 @@ + + + + <%= render partial: 'layouts/head' %> + + <%= theme_css %> + + <%= css_injector %> + <%= "#{AppSettings['design.header_js']}".html_safe %> + + +<%= controller_name %>-<%= action_name %>" data-locale="<%= I18n.locale %>"> + + + <% cache [params[:locale], current_user] do %> + + <% end %> + +
    + +
    +
    +
    +
    +
    + +
    +
    + <%= link_to image_tag("#{AppSettings['design.header_logo']}", width: 40, class: "pull-left logo") + "#{AppSettings['settings.product_name'] + " " if AppSettings['settings.product_name']}" "#{AppSettings['settings.site_name']}", root_path, class: 'navbar-brand', responsive: true %> +
    +
    +
      + <%= content_tag :li, link_to(t(:ask_a_question), new_topic_path), data: { hook: 'question_nav' }, class: 'hidden-xs' if forums? || tickets? %> + <% if user_signed_in? %> + <%= content_tag :li, link_to(t(:logout), destroy_user_session_path), class: 'hidden-xs' %> + <% else %> + <%= content_tag :li, link_to(t('devise.sessions.new.sign_in'), new_user_session_path, class: 'login-link hidden-xs') %> + <% end %> +
    • +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + <% unless params[:controller] == 'home' %> + + + <%= render_breadcrumbs('') %> + + <% if @page_title.present? %> +

    <%= @page_title %>

    + <% else %> + <%= yield :page_title %> + <% end %> + <% end %> + <%= content_tag :h3, AppSettings['settings.site_tagline'] if params[:controller] == 'home' %> + + <%= bootstrap_flash %> + <%= yield %> +
    +
    +
    +
    +
    + +
    + +
    + +
    + + +<%= "".html_safe if AppSettings['widget.show_on_support_site'] == '1' %> +<%= "#{AppSettings['design.footer_js']}".html_safe %> + + + + diff --git a/app/themes/singular/views/result/_search_form.html.erb b/app/themes/singular/views/result/_search_form.html.erb new file mode 100644 index 000000000..1841c6223 --- /dev/null +++ b/app/themes/singular/views/result/_search_form.html.erb @@ -0,0 +1,14 @@ +
    + <%= form_tag result_path, method: :get, id: 'search-form', remote: (params[:controller] == 'result') do %> +
    + <%= text_field_tag('q', nil, placeholder: t('how_can_we_help'), id: 'search-field', class: 'form-control ui-autocomplete-input autosearch', value: params[:q]) %> +
    +
    +
    + +
    +
    + <% end # form %> +
    diff --git a/app/themes/singular/views/users b/app/themes/singular/views/users new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/admin/api_keys/index.html.erb b/app/views/admin/api_keys/index.html.erb index 474d410be..48ea9f08c 100644 --- a/app/views/admin/api_keys/index.html.erb +++ b/app/views/admin/api_keys/index.html.erb @@ -3,9 +3,10 @@ <% content_for :settings do %>
    -

    +

    + <%= show_responsive_nav %> <%= t('api_keys', default: "API Keys") %> -

    +
    diff --git a/app/views/admin/backups/_backedup_files.html.erb b/app/views/admin/backups/_backedup_files.html.erb index 79a418562..5345607b9 100644 --- a/app/views/admin/backups/_backedup_files.html.erb +++ b/app/views/admin/backups/_backedup_files.html.erb @@ -6,8 +6,8 @@ - <%= t(:type, default: "Type") %> - <%= t(:created_at, default: "Created_at") %> + <%= t(:file_type, default: "Type") %> + <%= t(:file_date_created, default: "Created at") %> <%= t(:file_name, default: "File Name") %> <%= t(:download, default: "Download") %> <%= t(:delete, default: "Delete") %> @@ -20,4 +20,3 @@ <%= paginate @files, remote: true %>
    - diff --git a/app/views/admin/backups/_file.html.erb b/app/views/admin/backups/_file.html.erb index 38b8bfc0b..25c94f19c 100644 --- a/app/views/admin/backups/_file.html.erb +++ b/app/views/admin/backups/_file.html.erb @@ -12,10 +12,10 @@ - <%= link_to "download", admin_download_path(file_id: file, format: "csv") %> + <%= link_to t('file_download'), admin_download_path(file_id: file, format: "csv") %> - <%= link_to "delete", admin_delete_backup_path(file), method: :delete, data: {confirm: "Are you really want to delete this csv?"} %> + <%= link_to t('file_delete'), admin_delete_backup_path(file), method: :delete, data: {confirm: t('file_delete_confirm', default: "Are you sure you want to delete this csv?")} %> diff --git a/app/views/admin/backups/index.html.erb b/app/views/admin/backups/index.html.erb index 5978b35fe..a511e262f 100644 --- a/app/views/admin/backups/index.html.erb +++ b/app/views/admin/backups/index.html.erb @@ -1,32 +1,40 @@ <% content_for :settings do %> -

    - <%= t('exports', default: 'Exports') %> -

    +
    +

    + <%= show_responsive_nav %> + <%= t('exports', default: 'Exports') %> +

    +
    +
    + +
    +
    +
    -
    -
    <%= content_tag :span, t('what_to_export', default: 'Select what you would like to export'), class: 'more-important' %>
    <%= bootstrap_form_tag url: admin_export_backup_path, method: 'get', format: :csv do |f| %>
    - <%= f.check_box 'User', {label: t('select_for_export', model: 'users') } %> - <%= f.check_box 'Topic', {label: t('select_for_export', model: 'tickets') } %> - <%= f.check_box 'Post', {label: t('select_for_export', model: 'posts') } %> - <%= f.check_box 'Doc', {label: t('select_for_export', model: 'docs') } %> - <%= f.check_box 'Category', {label: t('select_for_export', model: 'categories') } %> - <%= f.check_box 'Forum', {label: t('select_for_export', model: 'forums') } %> + <%= f.check_box 'User', {label: t('select_for_export', model: t('users')) } %> + <%= f.check_box 'Topic', {label: t('select_for_export', model: t('tickets')) } %> + <%= f.check_box 'Post', {label: t('select_for_export', model: t('file_replies')) } %> + <%= f.check_box 'Doc', {label: t('select_for_export', model: t('file_docs')) } %> + <%= f.check_box 'Category', {label: t('select_for_export', model: t('file_categories')) } %> + <%= f.check_box 'Forum', {label: t('select_for_export', model: t('file_forums')) } %>
    <%= f.submit t('export_csv', default: "Export CSV"), class: 'btn btn-default' %>
    <% end %> +
    -
    +
    <%= render 'admin/backups/backedup_files' %>
    +
    <% end %> diff --git a/app/views/admin/categories/_add_buttons.html.erb b/app/views/admin/categories/_add_buttons.html.erb new file mode 100644 index 000000000..f91fa1f97 --- /dev/null +++ b/app/views/admin/categories/_add_buttons.html.erb @@ -0,0 +1,3 @@ + + <%= link_to t(:new_category, default: "New Category"), new_admin_category_path(lang: I18n.locale), class: 'btn btn-primary' %> + <%= link_to t(:new_content, default: "New Content"), new_admin_doc_path(lang: I18n.locale), class: 'btn btn-primary' %> diff --git a/app/views/admin/categories/_cat.html.erb b/app/views/admin/categories/_cat.html.erb index c6073b5c2..3eabebb3f 100644 --- a/app/views/admin/categories/_cat.html.erb +++ b/app/views/admin/categories/_cat.html.erb @@ -10,7 +10,7 @@
    - +
    - +