Compare commits

...

18 Commits

Author SHA1 Message Date
Thorsten Eckel
5e3cf37194 Maintenance: Applied required changes to resolve 'omniauth' CVE-2015-9284
While Zammad was not affected it was using GET requests. These are now prohibited because they enable the attack vector (in other scenarios).
Since it's not a general 'rails' or 'omnniauth' issue but an issue affecting scenarios where both are combined. It is required to use the 'omniauth-rails_csrf_protection' gem instead of 'omniauth' for those scenarios.

More information can be found here:
https://github.com/omniauth/omniauth/pull/809
https://github.com/omniauth/omniauth/pull/809#issuecomment-497376674
https://nvd.nist.gov/vuln/detail/CVE-2015-9284
https://github.com/cookpad/omniauth-rails_csrf_protection
https://github.com/rubysec/ruby-advisory-db/pull/390#issuecomment-509108186
2019-07-10 01:00:33 +02:00
Martin Edenhofer
234aaa9707 Updated changes to 3.0.1. 2019-07-10 00:50:18 +02:00
Denny Bresch
c101f73094 Fixed #2624 - stable - Concern 'CanLatestChange' returns wrong updated_at 2019-06-27 12:01:29 +02:00
Billy Zhou
7e72474c07 Contributed by Lorenzo Milesi <maxxer@ufficyo.com>:
Fixed typo in timezone.coffee

(cherry picked from commit 4d0c618baa)
2019-06-27 11:09:09 +02:00
Martin Edenhofer
16ad30bea6 Fixes #2614 - Tree Select shows empty value in ticket zoom after submit when value contains trailing spaces. 2019-06-25 13:02:10 +02:00
Felix Niklas
dcfe5b011c KB public: fix IE11 layout bug
It adds a wrapper element around the contents inside the body  to workaround the wrong height calculation bug in IE11 : https://github.com/philipwalton/flexbugs#flexbug-3
Additionally it fixes a text overflow bug in grid elements.
Fixes #2600
2019-06-24 16:16:52 +02:00
Billy Zhou
577f813666 Contributed by Silsha Fux <hallo@silsha.me>
Update "Attached Files" string in knowledge base to use existing translation.
2019-06-24 11:33:43 +02:00
Martin Edenhofer
d1775ab0e6 Fixes #2594 - Wrong date format in Excel Exports of reporting download. 2019-06-24 09:32:42 +02:00
Thorsten Eckel
617324d65d Fixed issue #2604 - Broken Edit-button in public KB interface for KB editors. 2019-06-13 13:57:56 +02:00
Denny Bresch
872bee0d55 Fixed issue #2599 - Activation of Knowledgebase is impossible on IE11 - agent interface 2019-06-13 11:39:06 +02:00
Martin Edenhofer
e3ff2250e7 Followup for issue #2541 - only update active jobs and triggers. 2019-06-13 02:10:52 +02:00
Thorsten Eckel
7dfad5976a Maintenance: Use corresponding ES docker image tag. 2019-06-12 17:03:21 +02:00
Martin Edenhofer
ff3a289979 Fixes issue #2516 - trigger email notification UI defective on removal of recipient. 2019-06-07 14:33:03 +02:00
Felix Niklas
9898a875e4 KB public UI: fix list layout - prevent icon from shrinking 2019-06-06 13:08:59 +02:00
Felix Niklas
8398bcf21c KB public UI: add overflow-ellipsis to placeholder 2019-06-06 13:08:59 +02:00
Felix Niklas
26a6632a18 KB public UI: fix viewport size
- add viewport header
- adjust headline sizes for mobile
2019-06-06 13:08:59 +02:00
Felix Niklas
a880c7f01a Improved layout op public Knowledge Base:
- fix bug where search result title has to much spacing around (its a h3)
- remove border from h2
- size & spacing finetuning
- bigger spacing around hr
- add hr between categories and answers
2019-06-06 08:28:21 +02:00
Martin Edenhofer
a96f3a7104 Updated changes to 3.0.0. 2019-06-06 03:51:27 +02:00
32 changed files with 725 additions and 227 deletions

View File

@@ -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

View File

@@ -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)]

View File

@@ -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'

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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}
, {}

View File

@@ -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

View File

@@ -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]

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()

View File

@@ -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%);

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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?

View File

@@ -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>

View File

@@ -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| %>

View File

@@ -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){

View 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>

View File

@@ -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

View File

@@ -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|

View File

@@ -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')
});

View 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')
});

View 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

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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',