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))
+ %( )
+ 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')}" %>
<%= "#{t(:open_ticket, default: 'Open a new ticket')}" %>
<% end %>
@@ -55,7 +55,7 @@
<%= link_to(tickets_path) do %>
-
+
<%= content_tag :h3, "#{t(:your_tickets, default: 'Your tickets')}" %>
<%= "#{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')}" %>
<%= "#{t(:open_ticket, default: 'Open a new ticket')}" %>
<% end %>
@@ -55,7 +55,7 @@
<%= link_to(tickets_path) do %>
-
+
<%= content_tag :h3, "#{t(:your_tickets, default: 'Your tickets')}" %>
<%= "#{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') %>
+
+
+
+
+ <%= 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/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 @@
+
+
+<%- if controller_name != 'sessions' %>
+ <%= link_to "Log in", new_session_path(resource_name) %>
+<% end -%>
+
+<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
+ <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end -%>
+
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
+ <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end -%>
+
+<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
+ <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end -%>
+
+<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
+ <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end -%>
+
+<%#- if devise_mapping.omniauthable? %>
+ <%#- resource_class.omniauth_providers.each do |provider| %>
+ <%#= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %>
+ <%# end -%>
+<%# end -%>
+
+
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? %>
+ <%= link_to t(:start_discussion, default: "Start Discussion"), '#', data: {toggle: "modal", target: "#login-modal"}, class: 'btn btn-primary' %>
+ <% else %>
+ <%= link_to t(:reply, default: "Reply"), '#', data: {toggle: "modal", target: "#login-modal"}, class: 'btn btn-primary' %>
+ <% 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')}" %>
+ <%= "#{t(:open_ticket, default: 'Open a new ticket')}" %>
+ <% end %>
+
+
+
+
+
+ <%= link_to(tickets_path) do %>
+
+ <%= content_tag :h3, "#{t(:your_tickets, default: 'Your tickets')}" %>
+ <%= "#{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 %>
+ |
+
+ <%= "#{I18n.locale.upcase}" %>
+
+
+<% 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) %>
+
+
+ <%= I18n.translate(:yes_button, default: "Yes") %>
+ <%= I18n.translate(:no_button, default: "No") %>
+
+
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 @@
+
+
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 %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= render 'result/search_form' if knowledgebase? %>
+
+
+
+
+
+
+
+
+
+
+
+
+ <% 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 @@
+
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 %>
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') %>
-
+
+
+
+
+
+
-
+
<%= 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 @@