Compare commits
18 Commits
develop
...
stable-3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e3cf37194 | ||
|
|
234aaa9707 | ||
|
|
c101f73094 | ||
|
|
7e72474c07 | ||
|
|
16ad30bea6 | ||
|
|
dcfe5b011c | ||
|
|
577f813666 | ||
|
|
d1775ab0e6 | ||
|
|
617324d65d | ||
|
|
872bee0d55 | ||
|
|
e3ff2250e7 | ||
|
|
7dfad5976a | ||
|
|
ff3a289979 | ||
|
|
9898a875e4 | ||
|
|
8398bcf21c | ||
|
|
26a6632a18 | ||
|
|
a880c7f01a | ||
|
|
a96f3a7104 |
@@ -102,7 +102,7 @@ pre:bundle-audit:
|
||||
script:
|
||||
- gem install bundler-audit
|
||||
- bundle-audit update
|
||||
- bundle-audit
|
||||
- bundle-audit --ignore CVE-2015-9284
|
||||
|
||||
pre:github:
|
||||
<<: *pre_stage
|
||||
@@ -222,7 +222,7 @@ test:integration:es:
|
||||
alias: mysql
|
||||
- name: registry.znuny.com/docker/zammad-postgresql:latest
|
||||
alias: postgresql
|
||||
- name: registry.znuny.com/docker/zammad-elasticsearch:latest
|
||||
- name: registry.znuny.com/docker/zammad-elasticsearch:5.6
|
||||
alias: elasticsearch
|
||||
variables:
|
||||
RAILS_ENV: "test"
|
||||
@@ -320,7 +320,7 @@ browser:build:
|
||||
alias: mysql
|
||||
- name: registry.znuny.com/docker/zammad-postgresql:latest
|
||||
alias: postgresql
|
||||
- name: registry.znuny.com/docker/zammad-elasticsearch:latest
|
||||
- name: registry.znuny.com/docker/zammad-elasticsearch:5.6
|
||||
alias: elasticsearch
|
||||
- name: docker.io/elgalu/selenium:3.14.0-p17
|
||||
alias: selenium
|
||||
|
||||
44
CHANGELOG.md
44
CHANGELOG.md
@@ -1,14 +1,54 @@
|
||||
# Change Log
|
||||
|
||||
## [3.0.0](https://github.com/zammad/zammad/tree/3.0.0) (2019-xx-xx)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/2.9.0...3.0.0)
|
||||
## [3.0.1](https://github.com/zammad/zammad/tree/3.0.1) (2019-07-10)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/3.0.0...3.0.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
**Fixed bugs:**
|
||||
- Concern 'CanLatestChange' returns wrong updated_at [2624](https://github.com/zammad/zammad/issues/2624) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Tree Select shows empty value in ticket zoom after submit when value contains trailing spaces [2614](https://github.com/zammad/zammad/issues/2614) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)]
|
||||
- KB partly broken on IE11 - customer interface [2600](https://github.com/zammad/zammad/issues/2600) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)]
|
||||
- Wrong date format in Excel Exports of reporting download [2594](https://github.com/zammad/zammad/issues/2594) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
|
||||
- Edit-Buttons on customer interface (as agent) has wrong link (Blank-Page) [2604](https://github.com/zammad/zammad/issues/2604) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)]
|
||||
- Activation of Knowledgebase is impossible on IE11 - agent interface [2599](https://github.com/zammad/zammad/issues/2599) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)]
|
||||
- Existing scheduler email notification without `body` will raise an exception. [2541](https://github.com/zammad/zammad/issues/2541) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- trigger email notification UI defective on removal of recipient [2516](https://github.com/zammad/zammad/issues/2516) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)]
|
||||
|
||||
## [3.0.0](https://github.com/zammad/zammad/tree/3.0.0) (2019-06-06)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/2.9.0...3.0.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
- Implemented Zammad Knowledge Base.
|
||||
- Apply changes (updated or if channel is deleted) to email channel even if channel is already fetching emails. [2552](https://github.com/zammad/zammad/issues/2552) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
|
||||
- Not used SQL index with postgresql in certain cases on very large setups. [2530](https://github.com/zammad/zammad/issues/2530) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
|
||||
- Ensure Zammad only updates affected content (affects several) [2431](https://github.com/zammad/zammad/issues/2431) [[bug](https://github.com/zammad/zammad/labels/bug)] [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
|
||||
- Add information "Ticket merged" to History [2469](https://github.com/zammad/zammad/issues/2469) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
|
||||
|
||||
**Fixed bugs:**
|
||||
- Attached textfiles break "RAW"-Link if last attachment in list [2518](https://github.com/zammad/zammad/issues/2518) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
|
||||
- Zammad Customers shown as Agents in IE [2510](https://github.com/zammad/zammad/issues/2510) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Double-click timeout in ticket zoom too short [2586](https://github.com/zammad/zammad/issues/2586) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Every night tickets are updated (system need to use SLAs) [2574](https://github.com/zammad/zammad/issues/2574) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Agents should always be able to re-open closed tickets, regardless of group.follow_up_possible [2534](https://github.com/zammad/zammad/issues/2534) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Downloading of emails via IMAP takes longer as one minute. E-Mail seems to be lost. [2554](https://github.com/zammad/zammad/issues/2554) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- allow agents to always reopen tickets, regardless of group.follow_up_possible [2531](https://github.com/zammad/zammad/pull/2531)
|
||||
- Incorrect display count of a report profile if tree_select is used [2059](https://github.com/zammad/zammad/issues/2059) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- users_controller.rb: search: pagination broken [2539](https://github.com/zammad/zammad/issues/2539) [[API](https://github.com/zammad/zammad/labels/API)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)] [[search](https://github.com/zammad/zammad/labels/search)]
|
||||
- Existing scheduler email notification without `body` will raise an exception. [2541](https://github.com/zammad/zammad/issues/2541) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Attached text files get prepended on e-mail reply instead of appended. [2362](https://github.com/zammad/zammad/issues/2362) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail processing)]
|
||||
- timezone issue with report graphs not displaying or displaying $timezone hours out of step [2089](https://github.com/zammad/zammad/issues/2089) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
|
||||
- Zammad Webcam-Avatar-Feature broken [2514](https://github.com/zammad/zammad/issues/2514) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)]
|
||||
- Required field is not required on updating tickets [1259](https://github.com/zammad/zammad/issues/1259) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
|
||||
- #{article.body_as_html} now includes attachments (e.g. PDFs) [2483](https://github.com/zammad/zammad/issues/2483) [[bug](https://github.com/zammad/zammad/labels/bug)] [[trigger](https://github.com/zammad/zammad/labels/trigger)]
|
||||
- Missing business hours validation for calendars creates infinitive loop (also for background jobs) [2497](https://github.com/zammad/zammad/issues/2497) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Login into Zammad not possible, application server `puma` will raise 100% or more cpu [2504](https://github.com/zammad/zammad/issues/2504) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Fixes #2491 - Email is not processed if filename of attachement is just a "space" like `Content-Disposition: inline; filename="=?utf-8?b??="`. [2491](https://github.com/zammad/zammad/issues/2491) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Unable to process email on CentOS with large images but only 1px height [2486](https://github.com/zammad/zammad/issues/2486) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Mails send by older mailers with x-uuencode Content-Transfer-Encoding of attachments are not processable [2464](https://github.com/zammad/zammad/issues/2464) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail processing)]
|
||||
- Finnish translation to the chat [2480](https://github.com/zammad/zammad/pull/2480)
|
||||
- Unable to update ticket with IE11 [2478](https://github.com/zammad/zammad/issues/2478) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
4
Gemfile
4
Gemfile
@@ -63,7 +63,9 @@ gem 'doorkeeper'
|
||||
gem 'oauth2'
|
||||
|
||||
# authentication - third party
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-rails_csrf_protection'
|
||||
|
||||
# authentication - third party providers
|
||||
gem 'omniauth-facebook'
|
||||
gem 'omniauth-github'
|
||||
gem 'omniauth-gitlab'
|
||||
|
||||
@@ -330,6 +330,9 @@ GEM
|
||||
omniauth-oauth2 (1.6.0)
|
||||
oauth2 (~> 1.1)
|
||||
omniauth (~> 1.9)
|
||||
omniauth-rails_csrf_protection (0.1.2)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (>= 1.3.1)
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
@@ -585,7 +588,6 @@ DEPENDENCIES
|
||||
mysql2 (= 0.4.10)
|
||||
net-ldap
|
||||
oauth2
|
||||
omniauth
|
||||
omniauth-facebook
|
||||
omniauth-github
|
||||
omniauth-gitlab
|
||||
@@ -593,6 +595,7 @@ DEPENDENCIES
|
||||
omniauth-linkedin-oauth2
|
||||
omniauth-microsoft-office365
|
||||
omniauth-oauth2
|
||||
omniauth-rails_csrf_protection
|
||||
omniauth-twitter
|
||||
omniauth-weibo-oauth2
|
||||
pg (= 0.21.0)
|
||||
|
||||
@@ -66,7 +66,7 @@ class App.UiElement.ticket_perform_action
|
||||
item = $( App.view('generic/ticket_perform_action/index')( attribute: attribute ) )
|
||||
|
||||
# add filter
|
||||
item.on('click', '.js-add', (e) =>
|
||||
item.on('click', '.js-rowActions .js-add', (e) =>
|
||||
element = $(e.target).closest('.js-filterElement')
|
||||
placeholder = @placeholder(item, attribute, params, groups, elements)
|
||||
if element.get(0)
|
||||
@@ -78,7 +78,7 @@ class App.UiElement.ticket_perform_action
|
||||
)
|
||||
|
||||
# remove filter
|
||||
item.on('click', '.js-remove', (e) =>
|
||||
item.on('click', '.js-rowActions .js-remove', (e) =>
|
||||
return if $(e.currentTarget).hasClass('is-disabled')
|
||||
$(e.target).closest('.js-filterElement').remove()
|
||||
@updateAttributeSelectors(item)
|
||||
|
||||
@@ -24,5 +24,5 @@ class App.UiElement.timezone extends App.UiElement.ApplicationUiElement
|
||||
@selectedOptions(attribute, params)
|
||||
|
||||
attribute.tag = 'searchable_select'
|
||||
attribute.placeholder = App.i18n.translateInline('Enter timzone...')
|
||||
attribute.placeholder = App.i18n.translateInline('Enter timezone...')
|
||||
App.UiElement.searchable_select.render(attribute)
|
||||
|
||||
@@ -313,7 +313,7 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||
.map (model) ->
|
||||
newIds = Object.keys data[model]
|
||||
oldIds = App[model].all().map (elem) -> elem.id
|
||||
diff = oldIds.filter (elem) -> !newIds.includes(String(elem))
|
||||
diff = oldIds.filter (elem) -> !_.includes(newIds, String(elem))
|
||||
|
||||
{modelName: model, ids: diff}
|
||||
, {}
|
||||
|
||||
@@ -7,7 +7,7 @@ treeParams = (e, params) ->
|
||||
$(e.target).closest('.modal').find('.js-treeTable .js-key').each( ->
|
||||
$element = $(@)
|
||||
level = parseInt($element.attr('level'))
|
||||
name = $element.val()
|
||||
name = $element.val().trim()
|
||||
item =
|
||||
name: name
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class App.MultiLocales extends App.Controller
|
||||
.filter (elem) -> elem && elem != self_value
|
||||
|
||||
App.Locale.all().filter (elem) ->
|
||||
!takenCodes.includes(String(elem.id))
|
||||
!_.includes(takenCodes, String(elem.id))
|
||||
|
||||
remove: (e) ->
|
||||
domRow = $(e.currentTarget).closest('tr')[0]
|
||||
|
||||
@@ -40,29 +40,29 @@ class App.SearchableSelect extends Spine.Controller
|
||||
|
||||
@html App.view('generic/searchable_select')
|
||||
attribute: @attribute
|
||||
options: @renderAllOptions '', @attribute.options, 0
|
||||
submenus: @renderSubmenus @attribute.options
|
||||
options: @renderAllOptions('', @attribute.options, 0)
|
||||
submenus: @renderSubmenus(@attribute.options)
|
||||
|
||||
# initial data
|
||||
@currentMenu = @findMenuContainingValue @attribute.value
|
||||
@level = @getIndex @currentMenu
|
||||
@currentMenu = @findMenuContainingValue(@attribute.value)
|
||||
@level = @getIndex(@currentMenu)
|
||||
|
||||
renderSubmenus: (options) ->
|
||||
html = ''
|
||||
if options
|
||||
for option in options
|
||||
if option.children
|
||||
html += App.view('generic/searchable_select_submenu')
|
||||
html += App.view('generic/searchable_select_submenu')(
|
||||
options: @renderOptions(option.children)
|
||||
parentValue: option.value
|
||||
title: option.name
|
||||
|
||||
)
|
||||
if @hasSubmenu(option.children)
|
||||
html += @renderSubmenus option.children
|
||||
html += @renderSubmenus(option.children)
|
||||
html
|
||||
|
||||
updateAttributeValueName: ->
|
||||
firstSelected = _.find @attribute.options, (option) -> option.selected
|
||||
firstSelected = _.find(@attribute.options, (option) -> option.selected)
|
||||
|
||||
if firstSelected
|
||||
@attribute.valueName = firstSelected.name
|
||||
@@ -70,7 +70,7 @@ class App.SearchableSelect extends Spine.Controller
|
||||
else if @attribute.unknown && @attribute.value
|
||||
@attribute.valueName = @attribute.value
|
||||
else if @hasSubmenu @attribute.options
|
||||
@attribute.valueName = @getName @attribute.value, @attribute.options
|
||||
@attribute.valueName = @getName(@attribute.value, @attribute.options)
|
||||
|
||||
hasSubmenu: (options) ->
|
||||
return false if !options
|
||||
@@ -83,7 +83,7 @@ class App.SearchableSelect extends Spine.Controller
|
||||
if option.value is value
|
||||
return option.name
|
||||
if option.children
|
||||
name = @getName value, option.children
|
||||
name = @getName(value, option.children)
|
||||
return name if name isnt undefined
|
||||
undefined
|
||||
|
||||
@@ -103,30 +103,31 @@ class App.SearchableSelect extends Spine.Controller
|
||||
if level && level > 0
|
||||
className += ' is-hidden is-child'
|
||||
|
||||
html += App.view('generic/searchable_select_option')
|
||||
html += App.view('generic/searchable_select_option')(
|
||||
option: option
|
||||
class: className
|
||||
detail: parentName
|
||||
)
|
||||
|
||||
if option.children
|
||||
html += @renderAllOptions "#{parentName} — #{option.name}", option.children, level+1
|
||||
html += @renderAllOptions("#{parentName} — #{option.name}", option.children, level+1)
|
||||
html
|
||||
|
||||
onDropdownShown: =>
|
||||
@input.on 'click', @stopPropagation
|
||||
@input.on('click', @stopPropagation)
|
||||
@highlightFirst()
|
||||
if @level > 0
|
||||
@showSubmenu(@currentMenu)
|
||||
@isOpen = true
|
||||
|
||||
onDropdownHidden: =>
|
||||
@input.off 'click', @stopPropagation
|
||||
@input.off('click', @stopPropagation)
|
||||
@unhighlightCurrentItem()
|
||||
@isOpen = false
|
||||
|
||||
if !@input.val()
|
||||
@updateAttributeValueName()
|
||||
@input.val @attribute.valueName
|
||||
@input.val(@attribute.valueName)
|
||||
|
||||
onKeyUp: =>
|
||||
return if @input.val().trim() isnt ''
|
||||
@@ -141,13 +142,13 @@ class App.SearchableSelect extends Spine.Controller
|
||||
|
||||
navigate: (event) =>
|
||||
switch event.keyCode
|
||||
when 40 then @nudge event, 1 # down
|
||||
when 38 then @nudge event, -1 # up
|
||||
when 39 then @autocompleteOrNavigateIn event # right
|
||||
when 37 then @autocompleteOrNavigateOut event # left
|
||||
when 13 then @onEnter event
|
||||
when 27 then @onEscape event
|
||||
when 9 then @onTab event
|
||||
when 40 then @nudge(event, 1) # down
|
||||
when 38 then @nudge(event, -1) # up
|
||||
when 39 then @autocompleteOrNavigateIn(event) # right
|
||||
when 37 then @autocompleteOrNavigateOut(event) # left
|
||||
when 13 then @onEnter(event)
|
||||
when 27 then @onEscape(event)
|
||||
when 9 then @onTab(event)
|
||||
|
||||
onEscape: ->
|
||||
if @isOpen
|
||||
@@ -204,8 +205,8 @@ class App.SearchableSelect extends Spine.Controller
|
||||
# current position
|
||||
caretPosition = @invisiblePart.text().length + 1
|
||||
|
||||
@input.val @suggestion
|
||||
@shadowInput.val @suggestionValue
|
||||
@input.val(@suggestion)
|
||||
@shadowInput.val(@suggestionValue)
|
||||
@clearAutocomplete()
|
||||
@toggle()
|
||||
|
||||
@@ -250,7 +251,7 @@ class App.SearchableSelect extends Spine.Controller
|
||||
target = @currentItem.attr('data-value')
|
||||
target_menu = @optionsSubmenu.filter("[data-parent-value=\"#{target}\"]")
|
||||
else
|
||||
target_menu = @findMenuContainingValue @currentMenu.attr('data-parent-value')
|
||||
target_menu = @findMenuContainingValue(@currentMenu.attr('data-parent-value'))
|
||||
|
||||
@animateToSubmenu(target_menu, dir)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="js-setNotification flex"></div>
|
||||
<div class="js-setArticle flex"></div>
|
||||
</div>
|
||||
<div class="filter-controls">
|
||||
<div class="filter-controls js-rowActions">
|
||||
<div class="filter-control filter-control-remove js-remove" title="<%- @Ti('Remote') %>">
|
||||
<%- @Icon('minus-small') %>
|
||||
</div>
|
||||
|
||||
@@ -48,21 +48,24 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if !_.isEmpty(@auth_providers): %>
|
||||
</form>
|
||||
<% if !_.isEmpty(@auth_providers): %>
|
||||
<div class="separator">
|
||||
<span class="separator-text"><%- @T('or sign in using') %></span>
|
||||
</div>
|
||||
|
||||
<div class="auth-providers">
|
||||
<% for auth_provider in @auth_providers: %>
|
||||
<a class="auth-provider auth-provider--<%= auth_provider.class %>" href="<%= auth_provider.url %>">
|
||||
<%- @Icon("#{auth_provider.class}-button", 'provider-icon') %>
|
||||
<span class="provider-name"><%- @T(auth_provider.name) %></span>
|
||||
</a>
|
||||
<form method="post" action="<%= auth_provider.url %>">
|
||||
<input type="hidden" name="authenticity_token" value="<%= Spine.Ajax.defaults.headers['X-CSRF-Token'] %>">
|
||||
<button type="submit" class="auth-provider auth-provider--<%= auth_provider.class %>">
|
||||
<%- @Icon("#{auth_provider.class}-button", 'provider-icon') %>
|
||||
<span class="provider-name"><%- @T(auth_provider.name) %></span>
|
||||
</button>
|
||||
</form>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</form>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
@@ -85,4 +88,4 @@
|
||||
<%- @T('Powered by') %>
|
||||
<a href="https://zammad.org" target="_blank"><%- @Icon('logotype', 'logotype') %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
LanguageDetector.show = function(lang) {
|
||||
var elem = new LanguageBannerElement(lang)
|
||||
document.body.prepend(elem.el)
|
||||
document.querySelector('.js-wrapper').prepend(elem.el)
|
||||
}
|
||||
|
||||
LanguageDetector.checkIfBetterLanguageAvailable()
|
||||
|
||||
@@ -88,6 +88,10 @@ html {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: $light-color;
|
||||
|
||||
@include phone {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -95,32 +99,59 @@ body {
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.625em;
|
||||
line-height: 1;
|
||||
margin: 1em 0 .75em;
|
||||
font-size: 2.5em;
|
||||
line-height: 1.12;
|
||||
margin: .8em 0 .6em;
|
||||
font-weight: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@include phone {
|
||||
font-size: 2em;
|
||||
font-size: 28px;
|
||||
margin: 1em 0 .8em;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size: 2.125em;
|
||||
line-height: 1.2;
|
||||
margin: 1em 0 .5em;
|
||||
font-size: 1.8em;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
margin: 1em 0 .4em;
|
||||
|
||||
@include phone {
|
||||
font-size: 24px;
|
||||
margin: 1.2em 0 .5em;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.375em;
|
||||
font-weight: normal;
|
||||
line-height: 1.3;
|
||||
margin: 0 0 .25em;
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
margin: 1.33em 0 .55em;
|
||||
letter-spacing: .015em;
|
||||
|
||||
@include phone {
|
||||
font-size: 19px;
|
||||
line-height: 1.3;
|
||||
margin: 1.2em 0 .75em;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
line-height: 1.25;
|
||||
margin: 1.33em 0 .5em;
|
||||
letter-spacing: .015em;
|
||||
}
|
||||
|
||||
p,
|
||||
@@ -138,6 +169,11 @@ ol {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
font-size: 0.875em;
|
||||
display: flex;
|
||||
@@ -203,11 +239,18 @@ ol {
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: $light-bg;
|
||||
border-bottom: 1px solid $dark-border;
|
||||
position: relative;
|
||||
padding: .3em 0 .9em;
|
||||
flex-shrink: 0;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
@@ -278,6 +321,7 @@ ol {
|
||||
|
||||
&::placeholder {
|
||||
color: $subtle-color;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +394,8 @@ ol {
|
||||
&-title {
|
||||
font-size: inherit;
|
||||
font-weight: 500;
|
||||
color: hsl(206,8%,38%);
|
||||
margin: 0 0 .25em;
|
||||
}
|
||||
|
||||
&-category {
|
||||
@@ -364,6 +410,10 @@ ol {
|
||||
&-preview {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-preview {
|
||||
color: hsl(207,12%,50%);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -378,21 +428,23 @@ ol {
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
margin: 30px 0 40px;
|
||||
|
||||
.header + & {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.main--categories {
|
||||
padding-bottom: 42px;
|
||||
|
||||
h1 {
|
||||
color: $dark-color;
|
||||
}
|
||||
@@ -544,8 +596,10 @@ ol {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: 22px;
|
||||
& + hr {
|
||||
border: none;
|
||||
border-top: 1px solid hsl(219,13%,95%);
|
||||
margin: 28px 0 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,7 +627,6 @@ ol {
|
||||
margin: 15px 0;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
@@ -582,6 +635,7 @@ ol {
|
||||
|
||||
.not-published-note {
|
||||
margin-left: .5em;
|
||||
margin-top: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,6 +686,7 @@ ol {
|
||||
|
||||
span {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,8 +793,9 @@ ol {
|
||||
.section-inner & {
|
||||
color: $highlight-color;
|
||||
fill: currentColor;
|
||||
margin: 0 9px 0 3px;
|
||||
margin: 4px 9px 0 3px;
|
||||
line-height: inherit;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,6 +814,7 @@ ol {
|
||||
|
||||
@include phone {
|
||||
font-size: 35px;
|
||||
margin: 10px 0 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -769,30 +826,6 @@ ol {
|
||||
|
||||
.article-content {
|
||||
display: block;
|
||||
margin: 0 0 20px;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 1.8rem 0 1.2rem;
|
||||
line-height: 1.3;
|
||||
font-weight: bold;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
p,
|
||||
ol,
|
||||
@@ -888,10 +921,10 @@ ol {
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
border: 0;
|
||||
border-top: 1px solid #dfdfdf;
|
||||
margin-top: 13px;
|
||||
margin-bottom: 13px;
|
||||
border: none;
|
||||
border-top: 1px solid hsl(210,14%,91%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -958,6 +991,7 @@ ol {
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
border-top: 1px solid $dark-border;
|
||||
background: $light-bg;
|
||||
color: hsl(210,8%,50%);
|
||||
|
||||
@@ -2969,9 +2969,14 @@ ol.tabs li {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-bottom: -10px;
|
||||
|
||||
> form {
|
||||
width: calc(33.33% - 6px);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-provider {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 10px 0 7px;
|
||||
margin-bottom: 10px;
|
||||
@@ -2981,7 +2986,8 @@ ol.tabs li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
width: calc(33.33% - 6px);
|
||||
border: none;
|
||||
text-align: initial;
|
||||
|
||||
&.auth-provider--wide {
|
||||
padding-right: 25px;
|
||||
|
||||
@@ -61,6 +61,8 @@ class ReportsController < ApplicationController
|
||||
|
||||
# get data
|
||||
result = {}
|
||||
content = nil
|
||||
filename = nil
|
||||
get_params[:metric][:backend].each do |backend|
|
||||
next if params[:downloadBackendSelected] != backend[:name]
|
||||
|
||||
@@ -86,17 +88,21 @@ class ReportsController < ApplicationController
|
||||
result = { count: 0, ticket_ids: [] } if result.nil?
|
||||
|
||||
# generate sheet
|
||||
next if !params[:sheet]
|
||||
|
||||
content = sheet(get_params[:profile], backend[:display], result)
|
||||
if params[:sheet]
|
||||
content = sheet(get_params[:profile], backend[:display], result)
|
||||
filename = "tickets-#{get_params[:profile].name}-#{backend[:display]}.xls"
|
||||
end
|
||||
break
|
||||
end
|
||||
if content
|
||||
send_data(
|
||||
content,
|
||||
filename: "tickets-#{get_params[:profile].name}-#{backend[:display]}.xls",
|
||||
filename: filename,
|
||||
type: 'application/vnd.ms-excel',
|
||||
disposition: 'attachment'
|
||||
)
|
||||
return
|
||||
end
|
||||
return if params[:sheet]
|
||||
|
||||
render json: result
|
||||
end
|
||||
@@ -172,24 +178,29 @@ class ReportsController < ApplicationController
|
||||
|
||||
def sheet(profile, title, result)
|
||||
|
||||
params[:timezone] ||= Setting.get('timezone_default')
|
||||
|
||||
# Create a new Excel workbook
|
||||
temp_file = Tempfile.new('time_tracking.xls')
|
||||
workbook = WriteExcel.new(temp_file)
|
||||
|
||||
# Add a worksheet
|
||||
worksheet = workbook.add_worksheet
|
||||
worksheet.set_row(0, 18)
|
||||
worksheet.set_column(0, 0, 10)
|
||||
worksheet.set_column(1, 1, 34)
|
||||
worksheet.set_column(2, 2, 10)
|
||||
worksheet.set_column(3, 3, 10)
|
||||
worksheet.set_column(4, 8, 20)
|
||||
worksheet.set_column(11, 0, 20)
|
||||
worksheet.set_column(12, 0, 20)
|
||||
worksheet.set_column(13, 0, 20)
|
||||
|
||||
# Add and define a format
|
||||
format = workbook.add_format
|
||||
format.set_bold
|
||||
format.set_size(14)
|
||||
format.set_color('black')
|
||||
worksheet.set_row(0, 0, 6)
|
||||
|
||||
# Write a formatted and unformatted string, row and column notation.
|
||||
worksheet.write_string(0, 0, "Tickets: #{profile.name} (#{title})", format)
|
||||
@@ -199,6 +210,14 @@ class ReportsController < ApplicationController
|
||||
format_header.set_bg_color('gray')
|
||||
format_header.set_color('white')
|
||||
|
||||
format_time = workbook.add_format(num_format: 'yyyy-mm-dd hh:mm:ss')
|
||||
format_date = workbook.add_format(num_format: 'yyyy-mm-dd')
|
||||
|
||||
format_footer = workbook.add_format
|
||||
format_footer.set_italic
|
||||
format_footer.set_color('gray')
|
||||
format_footer.set_size(8)
|
||||
|
||||
worksheet.write_string(2, 0, '#', format_header)
|
||||
worksheet.write_string(2, 1, 'Title', format_header)
|
||||
worksheet.write_string(2, 2, 'State', format_header)
|
||||
@@ -213,6 +232,7 @@ class ReportsController < ApplicationController
|
||||
worksheet.write_string(2, 11, 'Created at', format_header)
|
||||
worksheet.write_string(2, 12, 'Updated at', format_header)
|
||||
worksheet.write_string(2, 13, 'Closed at', format_header)
|
||||
|
||||
# ObjectManager attributes
|
||||
header_column = 14
|
||||
# needs to be skipped
|
||||
@@ -223,6 +243,7 @@ class ReportsController < ApplicationController
|
||||
.pluck(:name, :display, :data_type, :data_option)
|
||||
.map { |name, display, data_type, data_option| { name: name, display: display, data_type: data_type, data_option: data_option } }
|
||||
objects.each do |object|
|
||||
worksheet.set_column(header_column, 0, 16)
|
||||
worksheet.write_string(2, header_column, object[:display].capitalize, format_header)
|
||||
header_column += 1
|
||||
end
|
||||
@@ -243,9 +264,11 @@ class ReportsController < ApplicationController
|
||||
worksheet.write_string(row, 8, ticket.create_article_type.name)
|
||||
worksheet.write_string(row, 9, ticket.create_article_sender.name)
|
||||
worksheet.write_string(row, 10, ticket.tag_list.join(','))
|
||||
worksheet.write_date_time(row, 11, ticket.created_at.to_time.iso8601)
|
||||
worksheet.write_date_time(row, 12, ticket.updated_at.to_time.iso8601)
|
||||
worksheet.write_date_time(row, 13, ticket.close_at.to_time.iso8601) if ticket.close_at.present?
|
||||
|
||||
worksheet.write_date_time(row, 11, time_in_localtime_for_excel(ticket.created_at, params[:timezone]), format_time)
|
||||
worksheet.write_date_time(row, 12, time_in_localtime_for_excel(ticket.updated_at, params[:timezone]), format_time)
|
||||
worksheet.write_date_time(row, 13, time_in_localtime_for_excel(ticket.close_at, params[:timezone]), format_time) if ticket.close_at.present?
|
||||
|
||||
# Object Manager attributes
|
||||
column = 14
|
||||
# We already queried ObjectManager::Attributes, so we just use objects
|
||||
@@ -253,13 +276,20 @@ class ReportsController < ApplicationController
|
||||
key = object[:name]
|
||||
case object[:data_type]
|
||||
when 'boolean', 'select'
|
||||
value = object[:data_option]['options'][ticket.send(key.to_sym)]
|
||||
value = ticket.send(key.to_sym)
|
||||
if object[:data_option] && object[:data_option]['options'] && object[:data_option]['options'][ticket.send(key.to_sym)]
|
||||
value = object[:data_option]['options'][ticket.send(key.to_sym)]
|
||||
end
|
||||
worksheet.write_string(row, column, value)
|
||||
when 'datetime', 'date'
|
||||
worksheet.write_date_time(row, column, ticket.send(key.to_sym).to_time.iso8601) if ticket.send(key.to_sym).present?
|
||||
when 'datetime'
|
||||
worksheet.write_date_time(row, column, time_in_localtime_for_excel(ticket.send(key.to_sym), params[:timezone]), format_time) if ticket.send(key.to_sym).present?
|
||||
when 'date'
|
||||
worksheet.write_date_time(row, column, ticket.send(key.to_sym).to_s, format_date) if ticket.send(key.to_sym).present?
|
||||
when 'integer'
|
||||
worksheet.write_number(row, column, ticket.send(key.to_sym))
|
||||
else
|
||||
# for text, integer and tree select
|
||||
worksheet.write_string(row, column, ticket.send(key.to_sym))
|
||||
worksheet.write_string(row, column, ticket.send(key.to_sym).to_s)
|
||||
end
|
||||
column += 1
|
||||
end
|
||||
@@ -268,6 +298,9 @@ class ReportsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
row += 2
|
||||
worksheet.write_string(row, 0, "#{Translation.translate(current_user.locale, 'Timezone')}: #{params[:timezone]}", format_footer)
|
||||
|
||||
workbook.close
|
||||
|
||||
# read file again
|
||||
@@ -277,4 +310,13 @@ class ReportsController < ApplicationController
|
||||
contents
|
||||
end
|
||||
|
||||
def time_in_localtime_for_excel(time, timezone)
|
||||
return if time.blank?
|
||||
|
||||
if timezone.present?
|
||||
offset = time.in_time_zone(timezone).utc_offset
|
||||
time -= offset
|
||||
end
|
||||
time.utc.iso8601.to_s.sub(/Z$/, '')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -96,15 +96,15 @@ class TimeAccountingsController < ApplicationController
|
||||
},
|
||||
{
|
||||
name: 'Created at',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'Closed at',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'Close Escalation At',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'Close In Min',
|
||||
@@ -116,11 +116,11 @@ class TimeAccountingsController < ApplicationController
|
||||
},
|
||||
{
|
||||
name: 'First Response At',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'First Response Escalation At',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'First Response In Min',
|
||||
@@ -132,7 +132,7 @@ class TimeAccountingsController < ApplicationController
|
||||
},
|
||||
{
|
||||
name: 'Update Escalation At',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'Update In Min',
|
||||
@@ -144,15 +144,15 @@ class TimeAccountingsController < ApplicationController
|
||||
},
|
||||
{
|
||||
name: 'Last Contact At',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'Last Contact Agent At',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'Last Contact Customer At',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
{
|
||||
name: 'Article Count',
|
||||
@@ -160,7 +160,7 @@ class TimeAccountingsController < ApplicationController
|
||||
},
|
||||
{
|
||||
name: 'Escalation At',
|
||||
width: 10,
|
||||
width: 18,
|
||||
},
|
||||
]
|
||||
objects = ObjectManager::Attribute.where(editable: true,
|
||||
@@ -170,18 +170,11 @@ class TimeAccountingsController < ApplicationController
|
||||
.pluck(:name, :display, :data_type, :data_option)
|
||||
.map { |name, display, data_type, data_option| { name: name, display: display, data_type: data_type, data_option: data_option } }
|
||||
objects.each do |object|
|
||||
header.push({ name: object[:display], width: 10 })
|
||||
header.push({ name: object[:display], width: 18 })
|
||||
end
|
||||
|
||||
result = []
|
||||
results.each do |row|
|
||||
row[:ticket].each_key do |field|
|
||||
next if row[:ticket][field].blank?
|
||||
next if !row[:ticket][field].is_a?(ActiveSupport::TimeWithZone)
|
||||
|
||||
row[:ticket][field] = row[:ticket][field].iso8601
|
||||
end
|
||||
|
||||
result_row = [
|
||||
row[:ticket]['number'],
|
||||
row[:ticket]['title'],
|
||||
@@ -209,19 +202,17 @@ class TimeAccountingsController < ApplicationController
|
||||
row[:ticket]['escalation_at'],
|
||||
]
|
||||
|
||||
# needed to get human values for boolean/select rather than true/false values
|
||||
ticket = Ticket.lookup(id: row[:ticket]['id'])
|
||||
|
||||
# Object Manager attributes
|
||||
# We already queried ObjectManager::Attributes, so we just use objects
|
||||
objects.each do |object|
|
||||
key = object[:name]
|
||||
case object[:data_type]
|
||||
when 'boolean', 'select'
|
||||
value = object[:data_option]['options'][ticket.send(key.to_sym)]
|
||||
value = row[:ticket][key]
|
||||
if object[:data_option] && object[:data_option]['options'] && object[:data_option]['options'][row[:ticket][key]]
|
||||
value = object[:data_option]['options'][row[:ticket][key]]
|
||||
end
|
||||
value.present? ? result_row.push(value) : result_row.push('')
|
||||
when 'datetime', 'date'
|
||||
row[:ticket][key].present? ? result_row.push(row[:ticket][key].to_time.iso8601) : result_row.push('')
|
||||
else
|
||||
# for text, integer and tree select
|
||||
row[:ticket][key].present? ? result_row.push(row[:ticket][key]) : result_row.push('')
|
||||
@@ -400,6 +391,8 @@ class TimeAccountingsController < ApplicationController
|
||||
|
||||
def sheet(title, header, result)
|
||||
|
||||
params[:timezone] ||= Setting.get('timezone_default')
|
||||
|
||||
# Create a new Excel workbook
|
||||
temp_file = Tempfile.new('time_tracking.xls')
|
||||
workbook = WriteExcel.new(temp_file)
|
||||
@@ -412,7 +405,16 @@ class TimeAccountingsController < ApplicationController
|
||||
format.set_bold
|
||||
format.set_size(14)
|
||||
format.set_color('black')
|
||||
worksheet.set_row(0, 0, header.count)
|
||||
|
||||
format_time = workbook.add_format(num_format: 'yyyy-mm-dd hh:mm:ss')
|
||||
format_date = workbook.add_format(num_format: 'yyyy-mm-dd')
|
||||
|
||||
format_footer = workbook.add_format
|
||||
format_footer.set_italic
|
||||
format_footer.set_color('gray')
|
||||
format_footer.set_size(8)
|
||||
|
||||
worksheet.set_row(0, 18, header.count)
|
||||
|
||||
# Write a formatted and unformatted string, row and column notation.
|
||||
worksheet.write_string(0, 0, title, format)
|
||||
@@ -435,15 +437,22 @@ class TimeAccountingsController < ApplicationController
|
||||
row_count += 1
|
||||
row_item_count = 0
|
||||
row.each do |item|
|
||||
if item.acts_like?(:date)
|
||||
worksheet.write_date_time(row_count, row_item_count, item.to_time.iso8601)
|
||||
if item.acts_like?(:time)
|
||||
worksheet.write_date_time(row_count, row_item_count, time_in_localtime_for_excel(item, params[:timezone]), format_time) if item.present?
|
||||
elsif item.acts_like?(:date)
|
||||
worksheet.write_date_time(row_count, row_item_count, item.to_s, format_date) if item.present?
|
||||
elsif item.is_a?(Integer) || item.is_a?(Float)
|
||||
worksheet.write_number(row_count, row_item_count, item)
|
||||
else
|
||||
worksheet.write_string(row_count, row_item_count, item)
|
||||
worksheet.write_string(row_count, row_item_count, item.to_s)
|
||||
end
|
||||
row_item_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
row_count += 2
|
||||
worksheet.write_string(row_count, 0, "#{Translation.translate(current_user.locale, 'Timezone')}: #{params[:timezone]}", format_footer)
|
||||
|
||||
workbook.close
|
||||
|
||||
# read file again
|
||||
@@ -453,4 +462,13 @@ class TimeAccountingsController < ApplicationController
|
||||
contents
|
||||
end
|
||||
|
||||
def time_in_localtime_for_excel(time, timezone)
|
||||
return if time.blank?
|
||||
|
||||
if timezone.present?
|
||||
offset = time.in_time_zone(timezone).utc_offset
|
||||
time -= offset
|
||||
end
|
||||
time.utc.iso8601.to_s.sub(/Z$/, '')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -155,13 +155,15 @@ module KnowledgeBaseHelper
|
||||
end
|
||||
|
||||
def build_kb_link(object)
|
||||
locale = params.fetch(:locale, object.translation.kb_locale)
|
||||
|
||||
path = case object
|
||||
when KnowledgeBase::Answer
|
||||
"knowledge_base/#{object.category.knowledge_base.id}/answer/#{object.id}"
|
||||
"knowledge_base/#{object.category.knowledge_base.id}/locale/#{locale}/answer/#{object.id}/edit"
|
||||
when KnowledgeBase::Category
|
||||
"knowledge_base/#{object.knowledge_base.id}/category/#{object.id}"
|
||||
"knowledge_base/#{object.knowledge_base.id}/locale/#{locale}/category/#{object.id}/edit"
|
||||
when KnowledgeBase
|
||||
"knowledge_base/#{object.id}"
|
||||
"knowledge_base/#{object.id}/locale/#{locale}/edit"
|
||||
end
|
||||
|
||||
build_zammad_link path
|
||||
|
||||
@@ -9,7 +9,7 @@ module ApplicationModel::CanLatestChange
|
||||
|
||||
get latest updated_at object timestamp
|
||||
|
||||
latest_change = Ticket.latest_change
|
||||
latest_change = object.latest_change
|
||||
|
||||
returns
|
||||
|
||||
@@ -18,13 +18,13 @@ returns
|
||||
=end
|
||||
|
||||
def latest_change
|
||||
key = "#{new.class.name}_latest_change"
|
||||
key = "#{name}_latest_change"
|
||||
updated_at = Cache.get(key)
|
||||
|
||||
return updated_at if updated_at
|
||||
|
||||
# if we do not have it cached, do lookup
|
||||
updated_at = Ticket.order(updated_at: :desc).limit(1).pluck(:updated_at).first
|
||||
updated_at = order(updated_at: :desc).limit(1).pluck(:updated_at).first
|
||||
|
||||
return if !updated_at
|
||||
|
||||
@@ -33,7 +33,7 @@ returns
|
||||
end
|
||||
|
||||
def latest_change_set(updated_at)
|
||||
key = "#{new.class.name}_latest_change"
|
||||
key = "#{name}_latest_change"
|
||||
expires_in = 86_400 # 1 day
|
||||
|
||||
if updated_at.nil?
|
||||
|
||||
@@ -14,7 +14,7 @@ data-available-locales='<%= @object_locales.map(&:locale).join(',') %>'>
|
||||
<% if (attachments = @object.attachments_sorted) && attachments.present? %>
|
||||
<div class="attachments">
|
||||
<%= icon 'paperclip' %>
|
||||
<div class="attachments-title"><%= zt('Attached files') %></div>
|
||||
<div class="attachments-title"><%= zt('Attached Files') %></div>
|
||||
<% attachments.each do |attachment| %>
|
||||
<%= link_to custom_path_if_needed(attachment_path(attachment)), class: 'attachment', download: true do %>
|
||||
<span class="attachment-name u-highlight"><%= attachment.filename %></span>
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<% if @categories&.present? && @answers&.present? %>
|
||||
<hr>
|
||||
<% end %>
|
||||
|
||||
<% if @answers&.present? %>
|
||||
<ul class="sections sections--list" data-less-than-four="<%= @answers.length < 4 %>">
|
||||
<% @answers.each do |translation| %>
|
||||
|
||||
@@ -8,64 +8,67 @@
|
||||
|
||||
<meta charset="utf-8">
|
||||
<title><%= kb_public_page_title %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
|
||||
<%= stylesheet_link_tag "knowledge_base.css", :media => 'all' %>
|
||||
<%= render 'knowledge_base/public/inline_stylesheet', knowledge_base: @knowledge_base %>
|
||||
|
||||
<%= render 'knowledge_base/public/top_banner', object: @object || @knowledge_base if editor? %>
|
||||
<div class="wrapper js-wrapper">
|
||||
<%= render 'knowledge_base/public/top_banner', object: @object || @knowledge_base if editor? %>
|
||||
|
||||
<header class="header js-header">
|
||||
<div class="container">
|
||||
<h1 class="logo">
|
||||
<%= link_to custom_path_if_needed(help_root_path(locale: params[:locale])) do %>
|
||||
<img src="/assets/images/<%= Setting.get('product_logo') %>">
|
||||
<% end %>
|
||||
</h1>
|
||||
<nav class="menu">
|
||||
<% menu_items.each do |menu_item| %>
|
||||
<%= link_to menu_item.title, menu_item.url, class: 'menu-item', target: menu_item.new_tab ? '_blank' : nil %>
|
||||
<% end %>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="search">
|
||||
<div class="search-field">
|
||||
<%= icon 'magnifier' %>
|
||||
<input class="js-search-input" type="search" placeholder="<%= zt('How can we help you? Search for an answer or a topic...') %>">
|
||||
</div>
|
||||
<ul class="search-results js-search-results" data-empty-placeholder="<%= zt('No results were found.') %>"></ul>
|
||||
<header class="header js-header">
|
||||
<div class="container">
|
||||
<h1 class="logo">
|
||||
<%= link_to custom_path_if_needed(help_root_path(locale: params[:locale])) do %>
|
||||
<img src="/assets/images/<%= Setting.get('product_logo') %>">
|
||||
<% end %>
|
||||
</h1>
|
||||
<nav class="menu">
|
||||
<% menu_items.each do |menu_item| %>
|
||||
<%= link_to menu_item.title, menu_item.url, class: 'menu-item', target: menu_item.new_tab ? '_blank' : nil %>
|
||||
<% end %>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<%= render_breadcrumb_if_needed %>
|
||||
|
||||
<%= yield %>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-menu">
|
||||
<div class="copyright">
|
||||
<%= @knowledge_base.translation.footer_note %>
|
||||
</div>
|
||||
<div class="language-picker">
|
||||
<a class="btn btn--action" href="#" data-toggle="dropdown" aria-expanded="false">
|
||||
<%= system_locale_via_uri.name %>
|
||||
<%= icon 'arrow-down' %>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-right dropdown-menu-up" role="menu">
|
||||
<% @object_locales&.each do |locale| %>
|
||||
<li class="<%= 'is-selected' if locale.name == system_locale_via_uri.name %>">
|
||||
<%= link_to custom_path_if_needed(url_for(locale: locale.locale)), hreflang: locale.locale do %>
|
||||
<%= icon 'checkmark' %>
|
||||
<%= locale.name %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<div class="container">
|
||||
<div class="search">
|
||||
<div class="search-field">
|
||||
<%= icon 'magnifier' %>
|
||||
<input class="js-search-input" type="search" placeholder="<%= zt('How can we help you? Search for an answer or a topic...') %>">
|
||||
</div>
|
||||
<ul class="search-results js-search-results" data-empty-placeholder="<%= zt('No results were found.') %>"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</header>
|
||||
|
||||
<%= render_breadcrumb_if_needed %>
|
||||
|
||||
<%= yield %>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-menu">
|
||||
<div class="copyright">
|
||||
<%= @knowledge_base.translation.footer_note %>
|
||||
</div>
|
||||
<div class="language-picker">
|
||||
<a class="btn btn--action" href="#" data-toggle="dropdown" aria-expanded="false">
|
||||
<%= system_locale_via_uri.name %>
|
||||
<%= icon 'arrow-down' %>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-right dropdown-menu-up" role="menu">
|
||||
<% @object_locales&.each do |locale| %>
|
||||
<li class="<%= 'is-selected' if locale.name == system_locale_via_uri.name %>">
|
||||
<%= link_to custom_path_if_needed(url_for(locale: locale.locale)), hreflang: locale.locale do %>
|
||||
<%= icon 'checkmark' %>
|
||||
<%= locale.name %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type='text/javascript'>
|
||||
if(window.fetch === undefined || window.Promise === undefined || Element.prototype.prepend === undefined){
|
||||
|
||||
22
app/views/tests/form_ticket_perform_action.html.erb
Normal file
22
app/views/tests/form_ticket_perform_action.html.erb
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
|
||||
<script src="/assets/tests/qunit-1.21.0.js"></script>
|
||||
<script src="/assets/tests/form_ticket_perform_action.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding-top: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
|
||||
<div id="qunit" class="u-dontfold"></div>
|
||||
|
||||
<div>
|
||||
<form class="form-stacked pull-left">
|
||||
<div id="forms"></div>
|
||||
<button type="submit" class="btn btn-primary submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,30 +1,31 @@
|
||||
Zammad::Application.routes.draw do
|
||||
|
||||
match '/tests_core', to: 'tests#core', via: :get
|
||||
match '/tests_session', to: 'tests#session', via: :get
|
||||
match '/tests_ui', to: 'tests#ui', via: :get
|
||||
match '/tests_model', to: 'tests#model', via: :get
|
||||
match '/tests_model_binding', to: 'tests#model_binding', via: :get
|
||||
match '/tests_model_ui', to: 'tests#model_ui', via: :get
|
||||
match '/tests_form', to: 'tests#form', via: :get
|
||||
match '/tests_form_tree_select', to: 'tests#form_tree_select', via: :get
|
||||
match '/tests_form_find', to: 'tests#form_find', via: :get
|
||||
match '/tests_form_trim', to: 'tests#form_trim', via: :get
|
||||
match '/tests_form_extended', to: 'tests#form_extended', via: :get
|
||||
match '/tests_form_timer', to: 'tests#form_timer', via: :get
|
||||
match '/tests_form_validation', to: 'tests#form_validation', via: :get
|
||||
match '/tests_form_column_select', to: 'tests#form_column_select', via: :get
|
||||
match '/tests_form_searchable_select', to: 'tests#form_searchable_select', via: :get
|
||||
match '/tests_table', to: 'tests#table', via: :get
|
||||
match '/tests_table_extended', to: 'tests#table_extended', via: :get
|
||||
match '/tests_html_utils', to: 'tests#html_utils', via: :get
|
||||
match '/tests_ticket_selector', to: 'tests#ticket_selector', via: :get
|
||||
match '/tests_taskbar', to: 'tests#taskbar', via: :get
|
||||
match '/tests/wait/:sec', to: 'tests#wait', via: :get
|
||||
match '/tests/unprocessable_entity', to: 'tests#error_unprocessable_entity', via: :get
|
||||
match '/tests/not_authorized', to: 'tests#error_not_authorized', via: :get
|
||||
match '/tests/ar_not_found', to: 'tests#error_ar_not_found', via: :get
|
||||
match '/tests/standard_error', to: 'tests#error_standard_error', via: :get
|
||||
match '/tests/argument_error', to: 'tests#error_argument_error', via: :get
|
||||
match '/tests_core', to: 'tests#core', via: :get
|
||||
match '/tests_session', to: 'tests#session', via: :get
|
||||
match '/tests_ui', to: 'tests#ui', via: :get
|
||||
match '/tests_model', to: 'tests#model', via: :get
|
||||
match '/tests_model_binding', to: 'tests#model_binding', via: :get
|
||||
match '/tests_model_ui', to: 'tests#model_ui', via: :get
|
||||
match '/tests_form', to: 'tests#form', via: :get
|
||||
match '/tests_form_tree_select', to: 'tests#form_tree_select', via: :get
|
||||
match '/tests_form_find', to: 'tests#form_find', via: :get
|
||||
match '/tests_form_trim', to: 'tests#form_trim', via: :get
|
||||
match '/tests_form_extended', to: 'tests#form_extended', via: :get
|
||||
match '/tests_form_timer', to: 'tests#form_timer', via: :get
|
||||
match '/tests_form_validation', to: 'tests#form_validation', via: :get
|
||||
match '/tests_form_column_select', to: 'tests#form_column_select', via: :get
|
||||
match '/tests_form_searchable_select', to: 'tests#form_searchable_select', via: :get
|
||||
match '/tests_form_ticket_perform_action', to: 'tests#form_ticket_perform_action', via: :get
|
||||
match '/tests_table', to: 'tests#table', via: :get
|
||||
match '/tests_table_extended', to: 'tests#table_extended', via: :get
|
||||
match '/tests_html_utils', to: 'tests#html_utils', via: :get
|
||||
match '/tests_ticket_selector', to: 'tests#ticket_selector', via: :get
|
||||
match '/tests_taskbar', to: 'tests#taskbar', via: :get
|
||||
match '/tests/wait/:sec', to: 'tests#wait', via: :get
|
||||
match '/tests/unprocessable_entity', to: 'tests#error_unprocessable_entity', via: :get
|
||||
match '/tests/not_authorized', to: 'tests#error_not_authorized', via: :get
|
||||
match '/tests/ar_not_found', to: 'tests#error_ar_not_found', via: :get
|
||||
match '/tests/standard_error', to: 'tests#error_standard_error', via: :get
|
||||
match '/tests/argument_error', to: 'tests#error_argument_error', via: :get
|
||||
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class Issue2541FixNotificationEmailWithoutBody < ActiveRecord::Migration[5.1]
|
||||
|
||||
# update jobs and triggers
|
||||
[::Job, ::Trigger].each do |model|
|
||||
model.all.each do |record|
|
||||
model.where(active: true).each do |record|
|
||||
next if record.perform.blank?
|
||||
|
||||
%w[notification.email notification.sms].each do |action|
|
||||
|
||||
@@ -5,12 +5,21 @@ test( "searchable_select check", function() {
|
||||
var el = $('#form1')
|
||||
var defaults = {
|
||||
searchable_select2: 'bbb',
|
||||
searchable_select4: 'ccc',
|
||||
}
|
||||
var options = {
|
||||
'aaa': 'aaa display',
|
||||
'bbb': 'bbb display',
|
||||
'ccc': 'ccc display',
|
||||
}
|
||||
var options_4_tree = [
|
||||
{ value: 'aaa', name: 'aaa display' },
|
||||
{ value: 'bbb', name: 'bbb display' },
|
||||
{ value: 'ccc', name: 'ccc display', children: [
|
||||
{ value: 'ccc::aaa', name: 'aaa display' },
|
||||
{ value: 'ccc::bbb', name: 'bbb display' },
|
||||
] },
|
||||
]
|
||||
new App.ControllerForm({
|
||||
el: el,
|
||||
model: {
|
||||
@@ -40,6 +49,15 @@ test( "searchable_select check", function() {
|
||||
null: true,
|
||||
unknown: true
|
||||
},
|
||||
{
|
||||
name: 'searchable_select4',
|
||||
display: 'SearchableSelect4',
|
||||
tag: 'searchable_select',
|
||||
options: options_4_tree,
|
||||
default: defaults['searchable_select4'],
|
||||
null: true,
|
||||
unknown: true
|
||||
},
|
||||
]
|
||||
},
|
||||
autofocus: true
|
||||
@@ -50,6 +68,7 @@ test( "searchable_select check", function() {
|
||||
searchable_select1: '',
|
||||
searchable_select2: 'bbb',
|
||||
searchable_select3: '',
|
||||
searchable_select4: 'ccc',
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
@@ -67,6 +86,7 @@ test( "searchable_select check", function() {
|
||||
searchable_select1: 'ccc',
|
||||
searchable_select2: 'bbb',
|
||||
searchable_select3: '',
|
||||
searchable_select4: 'ccc',
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
@@ -84,6 +104,7 @@ test( "searchable_select check", function() {
|
||||
searchable_select1: 'ccc',
|
||||
searchable_select2: 'ccc',
|
||||
searchable_select3: '',
|
||||
searchable_select4: 'ccc',
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
@@ -110,7 +131,57 @@ test( "searchable_select check", function() {
|
||||
searchable_select1: 'ccc',
|
||||
searchable_select2: 'ccc',
|
||||
searchable_select3: 'unknown value',
|
||||
searchable_select4: 'ccc',
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
$('#forms').append('<hr><h1>searchable_select check for .js-input field values</h1><form id="form2"></form>')
|
||||
var el = $('#form2')
|
||||
var defaults = {
|
||||
searchable_select1: 'ccc::aaa',
|
||||
searchable_select2: 'ccc::ccc',
|
||||
}
|
||||
var options = [
|
||||
{ value: 'aaa', name: 'aaa display' },
|
||||
{ value: 'bbb', name: 'bbb display' },
|
||||
{ value: 'ccc', name: 'ccc display', children: [
|
||||
{ value: 'ccc::aaa', name: 'aaa display L2' },
|
||||
{ value: 'ccc::bbb', name: 'bbb display L2' },
|
||||
{ value: 'ccc::ccc', name: 'ccc display L2' },
|
||||
] },
|
||||
]
|
||||
new App.ControllerForm({
|
||||
el: el,
|
||||
model: {
|
||||
configure_attributes: [
|
||||
{
|
||||
name: 'searchable_select1',
|
||||
display: 'SearchableSelect1',
|
||||
tag: 'searchable_select',
|
||||
options: options,
|
||||
default: defaults['searchable_select1'],
|
||||
null: true,
|
||||
},
|
||||
{
|
||||
name: 'searchable_select2',
|
||||
display: 'SearchableSelect2',
|
||||
tag: 'searchable_select',
|
||||
options: options,
|
||||
default: defaults['searchable_select2'],
|
||||
null: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
var params = App.ControllerForm.params(el)
|
||||
var test_params = {
|
||||
searchable_select1: 'ccc::aaa',
|
||||
searchable_select2: 'ccc::ccc',
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
equal(el.find('[name="searchable_select1"].js-shadow + .js-input').val(), 'aaa display L2', 'verify shown input')
|
||||
equal(el.find('[name="searchable_select2"].js-shadow + .js-input').val(), 'ccc display L2', 'verify shown input')
|
||||
|
||||
|
||||
});
|
||||
|
||||
197
public/assets/tests/form_ticket_perform_action.js
Normal file
197
public/assets/tests/form_ticket_perform_action.js
Normal file
@@ -0,0 +1,197 @@
|
||||
// ticket_perform_action
|
||||
test( "ticket_perform_action check", function() {
|
||||
|
||||
App.TicketPriority.refresh([
|
||||
{
|
||||
id: 2,
|
||||
name: '2 normal',
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: '1 low',
|
||||
active: true,
|
||||
},
|
||||
])
|
||||
|
||||
App.TicketState.refresh([
|
||||
{
|
||||
id: 1,
|
||||
name: 'new',
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'open',
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'closed',
|
||||
active: false,
|
||||
},
|
||||
])
|
||||
|
||||
$('#forms').append('<hr><h1>ticket_perform_action check</h1><form id="form1"></form>')
|
||||
var el = $('#form1')
|
||||
var defaults = {
|
||||
ticket_perform_action1: {
|
||||
'ticket.state_id': {
|
||||
value: '2'
|
||||
}
|
||||
},
|
||||
ticket_perform_action2: {
|
||||
'ticket.state_id': {
|
||||
value: '1'
|
||||
},
|
||||
'ticket.priority_id': {
|
||||
value: '2'
|
||||
},
|
||||
'notification.email': {
|
||||
body: 'some body',
|
||||
recipient: ['ticket_owner', 'ticket_customer'],
|
||||
subject: 'some subject'
|
||||
},
|
||||
},
|
||||
ticket_perform_action3: {
|
||||
'ticket.state_id': {
|
||||
value: '3'
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
new App.ControllerForm({
|
||||
el: el,
|
||||
model: {
|
||||
configure_attributes: [
|
||||
{
|
||||
name: 'ticket_perform_action1',
|
||||
display: 'TicketPerformAction1',
|
||||
tag: 'ticket_perform_action',
|
||||
null: true,
|
||||
},
|
||||
{
|
||||
name: 'ticket_perform_action2',
|
||||
display: 'TicketPerformAction2',
|
||||
tag: 'ticket_perform_action',
|
||||
null: false,
|
||||
notification: true,
|
||||
},
|
||||
{
|
||||
name: 'ticket_perform_action3',
|
||||
display: 'TicketPerformAction3',
|
||||
tag: 'ticket_perform_action',
|
||||
null: true,
|
||||
notification: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
params: defaults,
|
||||
autofocus: true
|
||||
})
|
||||
|
||||
var params = App.ControllerForm.params(el)
|
||||
var test_params = {
|
||||
ticket_perform_action1: {
|
||||
'ticket.state_id': {
|
||||
value: '2'
|
||||
}
|
||||
},
|
||||
ticket_perform_action2: {
|
||||
'notification.email': {
|
||||
body: 'some body',
|
||||
recipient: ['ticket_owner', 'ticket_customer'],
|
||||
subject: 'some subject'
|
||||
},
|
||||
'ticket.priority_id': {
|
||||
value: '2'
|
||||
},
|
||||
'ticket.state_id': {
|
||||
value: '1'
|
||||
},
|
||||
},
|
||||
ticket_perform_action3: {
|
||||
'ticket.state_id': {
|
||||
value: '3'
|
||||
}
|
||||
}
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
// add email notification
|
||||
$('[data-attribute-name="ticket_perform_action3"] .js-add').click()
|
||||
$('[data-attribute-name="ticket_perform_action3"] .js-attributeSelector .form-control').last().val('notification.email').trigger('change')
|
||||
$('[data-attribute-name="ticket_perform_action3"] .js-setNotification [name="ticket_perform_action3::notification.email::subject"]').val('some subject').trigger('change')
|
||||
$('[data-attribute-name="ticket_perform_action3"] .js-setNotification [data-name="ticket_perform_action3::notification.email::body"]').html('some body').trigger('change')
|
||||
$('[data-attribute-name="ticket_perform_action3"] .js-setNotification .js-recipient .js-option[data-value="ticket_owner"]').click()
|
||||
|
||||
params = App.ControllerForm.params(el)
|
||||
test_params = {
|
||||
ticket_perform_action1: {
|
||||
'ticket.state_id': {
|
||||
value: '2'
|
||||
}
|
||||
},
|
||||
ticket_perform_action2: {
|
||||
'notification.email': {
|
||||
body: 'some body',
|
||||
recipient: ['ticket_owner', 'ticket_customer'],
|
||||
subject: 'some subject'
|
||||
},
|
||||
'ticket.priority_id': {
|
||||
value: '2'
|
||||
},
|
||||
'ticket.state_id': {
|
||||
value: '1'
|
||||
},
|
||||
},
|
||||
ticket_perform_action3: {
|
||||
'notification.email': {
|
||||
body: 'some body',
|
||||
recipient: 'ticket_owner',
|
||||
subject: 'some subject'
|
||||
},
|
||||
'ticket.state_id': {
|
||||
value: '3'
|
||||
}
|
||||
}
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
// remove recipient
|
||||
$('[data-attribute-name="ticket_perform_action2"] .js-setNotification .js-recipient .js-remove.js-option[data-value="ticket_owner"]').click()
|
||||
|
||||
params = App.ControllerForm.params(el)
|
||||
test_params = {
|
||||
ticket_perform_action1: {
|
||||
'ticket.state_id': {
|
||||
value: '2'
|
||||
}
|
||||
},
|
||||
ticket_perform_action2: {
|
||||
'notification.email': {
|
||||
body: 'some body',
|
||||
recipient: 'ticket_customer',
|
||||
subject: 'some subject'
|
||||
},
|
||||
'ticket.priority_id': {
|
||||
value: '2'
|
||||
},
|
||||
'ticket.state_id': {
|
||||
value: '1'
|
||||
},
|
||||
},
|
||||
ticket_perform_action3: {
|
||||
'notification.email': {
|
||||
body: 'some body',
|
||||
recipient: 'ticket_owner',
|
||||
subject: 'some subject'
|
||||
},
|
||||
'ticket.state_id': {
|
||||
value: '3'
|
||||
}
|
||||
}
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
});
|
||||
32
spec/models/application_model/can_latest_change_examples.rb
Normal file
32
spec/models/application_model/can_latest_change_examples.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
RSpec.shared_examples 'ApplicationModel::CanLatestChange' do
|
||||
subject { create(described_class.name.underscore) }
|
||||
|
||||
describe '#latest_change' do
|
||||
describe 'caching updated_at' do
|
||||
context 'with empty cache' do
|
||||
it 'stores updated_at in the cache and returns it' do
|
||||
expect(subject.updated_at).to eq(described_class.latest_change)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid cache' do
|
||||
before { described_class.latest_change_set(subject.updated_at) }
|
||||
|
||||
it 'return updated_at from cache' do
|
||||
expect(subject.updated_at).to eq(described_class.latest_change)
|
||||
end
|
||||
end
|
||||
|
||||
context 'delete valid cache' do
|
||||
before do
|
||||
subject.touch
|
||||
described_class.latest_change_set(nil)
|
||||
end
|
||||
|
||||
it 'stores new updated_at in the cache and returns it' do
|
||||
expect(subject.updated_at).to eq(described_class.latest_change)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +1,11 @@
|
||||
require 'models/application_model/can_assets_examples'
|
||||
require 'models/application_model/can_associations_examples'
|
||||
require 'models/application_model/can_latest_change_examples'
|
||||
require 'models/application_model/checks_import_examples'
|
||||
|
||||
RSpec.shared_examples 'ApplicationModel' do |options = {}|
|
||||
include_examples 'ApplicationModel::CanAssets', options[:can_assets]
|
||||
include_examples 'ApplicationModel::CanAssociations'
|
||||
include_examples 'ApplicationModel::CanLatestChange'
|
||||
include_examples 'ApplicationModel::ChecksImport'
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ RSpec.describe 'OAuth', type: :request do
|
||||
describe 'request handling' do
|
||||
|
||||
it 'does o365 - start' do
|
||||
get '/auth/microsoft_office365'
|
||||
post '/auth/microsoft_office365'
|
||||
expect(response).to have_http_status(:found)
|
||||
expect(response.body).to include('https://login.microsoftonline.com/common/oauth2/v2.0/authorize')
|
||||
expect(response.body).to include('redirect_uri=http%3A%2F%2Fzammad.example.com%2Fauth%2Fmicrosoft_office365%2Fcallback')
|
||||
|
||||
@@ -86,6 +86,10 @@ RSpec.describe 'QUnit', type: :system, authenticated: false, set_up: true, webso
|
||||
q_unit_tests('form_column_select')
|
||||
end
|
||||
|
||||
it 'Ticket perform action' do
|
||||
q_unit_tests('form_ticket_perform_action')
|
||||
end
|
||||
|
||||
it 'Validation' do
|
||||
q_unit_tests('form_validation')
|
||||
end
|
||||
|
||||
@@ -157,6 +157,17 @@ class AAbUnitTest < TestCase
|
||||
value: '0',
|
||||
)
|
||||
|
||||
location(url: browser_url + '/tests_form_ticket_perform_action')
|
||||
watch_for(
|
||||
css: '.result',
|
||||
value: 'Tests completed',
|
||||
timeout: 8,
|
||||
)
|
||||
match(
|
||||
css: '.result .failed',
|
||||
value: '0',
|
||||
)
|
||||
|
||||
location(url: browser_url + '/tests_form_tree_select')
|
||||
watch_for(
|
||||
css: '.result',
|
||||
|
||||
Reference in New Issue
Block a user