Initial knowledge base support.
This commit is contained in:
parent
5d2398a3aa
commit
97d14a93b3
308 changed files with 37506 additions and 505 deletions
|
@ -57,7 +57,7 @@ rules:
|
||||||
no-case-declarations: 2
|
no-case-declarations: 2
|
||||||
no-div-regex: 2
|
no-div-regex: 2
|
||||||
no-else-return: 0
|
no-else-return: 0
|
||||||
no-empty-label: 2
|
no-labels: 2
|
||||||
no-empty-pattern: 2
|
no-empty-pattern: 2
|
||||||
no-eq-null: 2
|
no-eq-null: 2
|
||||||
no-eval: 2
|
no-eval: 2
|
||||||
|
@ -69,7 +69,6 @@ rules:
|
||||||
no-implied-eval: 2
|
no-implied-eval: 2
|
||||||
no-invalid-this: 0
|
no-invalid-this: 0
|
||||||
no-iterator: 2
|
no-iterator: 2
|
||||||
no-labels: 0
|
|
||||||
no-lone-blocks: 2
|
no-lone-blocks: 2
|
||||||
no-loop-func: 2
|
no-loop-func: 2
|
||||||
no-magic-number: 0
|
no-magic-number: 0
|
||||||
|
|
8
Gemfile
8
Gemfile
|
@ -31,6 +31,9 @@ gem 'eventmachine'
|
||||||
# core - password security
|
# core - password security
|
||||||
gem 'argon2', '1.1.5'
|
gem 'argon2', '1.1.5'
|
||||||
|
|
||||||
|
# core - state machine
|
||||||
|
gem 'aasm'
|
||||||
|
|
||||||
# performance - Memcached
|
# performance - Memcached
|
||||||
gem 'dalli'
|
gem 'dalli'
|
||||||
|
|
||||||
|
@ -105,6 +108,9 @@ gem 'telephone_number'
|
||||||
# feature - SMS
|
# feature - SMS
|
||||||
gem 'twilio-ruby'
|
gem 'twilio-ruby'
|
||||||
|
|
||||||
|
# feature - ordering
|
||||||
|
gem 'acts_as_list'
|
||||||
|
|
||||||
# integrations
|
# integrations
|
||||||
gem 'clearbit'
|
gem 'clearbit'
|
||||||
gem 'net-ldap'
|
gem 'net-ldap'
|
||||||
|
@ -136,7 +142,9 @@ group :development, :test do
|
||||||
gem 'pry-stack_explorer'
|
gem 'pry-stack_explorer'
|
||||||
|
|
||||||
# test frameworks
|
# test frameworks
|
||||||
|
gem 'rails-controller-testing'
|
||||||
gem 'rspec-rails'
|
gem 'rspec-rails'
|
||||||
|
gem 'shoulda-matchers'
|
||||||
gem 'test-unit'
|
gem 'test-unit'
|
||||||
|
|
||||||
# test DB
|
# test DB
|
||||||
|
|
14
Gemfile.lock
14
Gemfile.lock
|
@ -47,6 +47,8 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
|
aasm (5.0.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
actioncable (5.1.7)
|
actioncable (5.1.7)
|
||||||
actionpack (= 5.1.7)
|
actionpack (= 5.1.7)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
|
@ -94,6 +96,8 @@ GEM
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
|
acts_as_list (0.9.16)
|
||||||
|
activerecord (>= 3.0)
|
||||||
addressable (2.5.2)
|
addressable (2.5.2)
|
||||||
public_suffix (>= 2.0.2, < 4.0)
|
public_suffix (>= 2.0.2, < 4.0)
|
||||||
arel (8.0.0)
|
arel (8.0.0)
|
||||||
|
@ -374,6 +378,10 @@ GEM
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.3.0)
|
||||||
railties (= 5.1.7)
|
railties (= 5.1.7)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
|
rails-controller-testing (1.0.4)
|
||||||
|
actionpack (>= 5.0.1.x)
|
||||||
|
actionview (>= 5.0.1.x)
|
||||||
|
activesupport (>= 5.0.1.x)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
|
@ -450,6 +458,8 @@ GEM
|
||||||
childprocess (>= 0.5, < 2.0)
|
childprocess (>= 0.5, < 2.0)
|
||||||
rubyzip (~> 1.2, >= 1.2.2)
|
rubyzip (~> 1.2, >= 1.2.2)
|
||||||
shellany (0.0.1)
|
shellany (0.0.1)
|
||||||
|
shoulda-matchers (4.0.1)
|
||||||
|
activesupport (>= 4.2.0)
|
||||||
simple_oauth (0.3.1)
|
simple_oauth (0.3.1)
|
||||||
simplecov (0.16.1)
|
simplecov (0.16.1)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
|
@ -530,9 +540,11 @@ PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
aasm
|
||||||
activerecord-import
|
activerecord-import
|
||||||
activerecord-nulldb-adapter
|
activerecord-nulldb-adapter
|
||||||
activerecord-session_store
|
activerecord-session_store
|
||||||
|
acts_as_list
|
||||||
argon2 (= 1.1.5)
|
argon2 (= 1.1.5)
|
||||||
autodiscover!
|
autodiscover!
|
||||||
autoprefixer-rails
|
autoprefixer-rails
|
||||||
|
@ -592,6 +604,7 @@ DEPENDENCIES
|
||||||
puma
|
puma
|
||||||
rack-livereload
|
rack-livereload
|
||||||
rails (= 5.1.7)
|
rails (= 5.1.7)
|
||||||
|
rails-controller-testing
|
||||||
rails-observers
|
rails-observers
|
||||||
rb-fsevent
|
rb-fsevent
|
||||||
rchardet (>= 1.8.0)
|
rchardet (>= 1.8.0)
|
||||||
|
@ -603,6 +616,7 @@ DEPENDENCIES
|
||||||
rubyntlm!
|
rubyntlm!
|
||||||
sassc-rails
|
sassc-rails
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
|
shoulda-matchers
|
||||||
simplecov
|
simplecov
|
||||||
simplecov-rcov
|
simplecov-rcov
|
||||||
slack-notifier
|
slack-notifier
|
||||||
|
|
|
@ -163,3 +163,28 @@ Source: https://gist.github.com/sbrin/6801034
|
||||||
Copyright: 2015, sbrin - https://github.com/sbrin
|
Copyright: 2015, sbrin - https://github.com/sbrin
|
||||||
License: MIT license
|
License: MIT license
|
||||||
-----------------------------------------------------------------------------
|
-----------------------------------------------------------------------------
|
||||||
|
ant-design icon font
|
||||||
|
Source: https://github.com/ant-design/ant-design
|
||||||
|
Copyright: 2015-present Alipay.com, https://www.alipay.com/
|
||||||
|
License: MIT license
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
Font Awesome icon font
|
||||||
|
Source: http://fontawesome.io/
|
||||||
|
Copyright: Font Awesome by Dave Gandy - http://fontawesome.io
|
||||||
|
License: SIL OFL 1.1
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
Simple line icons font
|
||||||
|
Source: https://github.com/thesabbir/simple-line-icons
|
||||||
|
Copyright: 2016 Sabbir Ahmed & All Contributors
|
||||||
|
License: MIT license
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
Ionicons icon font
|
||||||
|
Source: https://github.com/ionic-team/ionicons
|
||||||
|
Copyright: 2016 Drifty (http://drifty.com/)
|
||||||
|
License: MIT license
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
Material icon font
|
||||||
|
Source: https://github.com/google/material-design-icons
|
||||||
|
Copyright: Google
|
||||||
|
License: Apache License, Version 2.0
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
|
@ -29,8 +29,13 @@
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"reply.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"weibo-button.svg": {
|
"weibo-button.svg": {
|
||||||
"author": "",
|
"author": "Weibo",
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": ""
|
"license": ""
|
||||||
},
|
},
|
||||||
|
@ -159,11 +164,46 @@
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"rearange.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"external.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"mood-sad.svg": {
|
"mood-sad.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"radio.svg": {
|
||||||
"author": "Zammad",
|
"author": "Zammad",
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"radio-checked.svg": {
|
||||||
|
"author": "Zammad",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"knowledge-base.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"eye.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"document.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"low-priority.svg": {
|
"low-priority.svg": {
|
||||||
"author": "Felix Niklas",
|
"author": "Felix Niklas",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -180,9 +220,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"inactive-user.svg": {
|
"inactive-user.svg": {
|
||||||
"author": "R\u00e9my M\u00e9dard",
|
"author": "Felix Niklas",
|
||||||
"url": "https:\/\/thenounproject.com\/search\/?q=user&i=10314",
|
"url": "",
|
||||||
"license": "CC 3.0 Attribution"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"inactive-organization.svg": {
|
"inactive-organization.svg": {
|
||||||
"author": "Felix Niklas",
|
"author": "Felix Niklas",
|
||||||
|
@ -194,6 +234,16 @@
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"important.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"reply-all.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"paperclip.svg": {
|
"paperclip.svg": {
|
||||||
"author": "Cheesefork",
|
"author": "Cheesefork",
|
||||||
"url": "https:\/\/thenounproject.com\/search\/?q=attachment&i=197956",
|
"url": "https:\/\/thenounproject.com\/search\/?q=attachment&i=197956",
|
||||||
|
@ -204,6 +254,21 @@
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"lock.svg": {
|
||||||
|
"author": "Zammad",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"lock-open.svg": {
|
||||||
|
"author": "Zammad",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"forward.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"file-word.svg": {
|
"file-word.svg": {
|
||||||
"author": "Felix Niklas",
|
"author": "Felix Niklas",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -220,9 +285,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"file-pdf.svg": {
|
"file-pdf.svg": {
|
||||||
"author": "Felix Niklas",
|
"author": "Adobe",
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": ""
|
||||||
},
|
},
|
||||||
"file-excel.svg": {
|
"file-excel.svg": {
|
||||||
"author": "Felix Niklas",
|
"author": "Felix Niklas",
|
||||||
|
@ -244,28 +309,8 @@
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"reply.svg": {
|
|
||||||
"author": "Felix Niklas",
|
|
||||||
"url": "",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"reply-all.svg": {
|
|
||||||
"author": "Felix Niklas",
|
|
||||||
"url": "",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"lock.svg": {
|
|
||||||
"author": "Zammad",
|
|
||||||
"url": "",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"forward.svg": {
|
|
||||||
"author": "Felix Niklas",
|
|
||||||
"url": "",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"office365-button.svg": {
|
"office365-button.svg": {
|
||||||
"author": "",
|
"author": "Office 365",
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": ""
|
"license": ""
|
||||||
},
|
},
|
||||||
|
@ -589,6 +634,31 @@
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"checkmark.svg": {
|
||||||
|
"author": "Zammad",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"chain.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"bold.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"checkbox.svg": {
|
||||||
|
"author": "Zammad",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"checkbox-indeterminate.svg": {
|
||||||
|
"author": "Felix Niklas",
|
||||||
|
"url": "",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"checkbox-checked.svg": {
|
"checkbox-checked.svg": {
|
||||||
"author": "Zammad",
|
"author": "Zammad",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -614,11 +684,6 @@
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"checkmark.svg": {
|
|
||||||
"author": "Zammad",
|
|
||||||
"url": "",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"chat.svg": {
|
"chat.svg": {
|
||||||
"author": "Felix Niklas",
|
"author": "Felix Niklas",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
|
|
@ -88,8 +88,10 @@ class App.Controller extends Spine.Controller
|
||||||
for callId in idsToCancel
|
for callId in idsToCancel
|
||||||
App.Ajax.abort(callId)
|
App.Ajax.abort(callId)
|
||||||
|
|
||||||
|
# release Spine's event handling
|
||||||
release: ->
|
release: ->
|
||||||
# release custom bindings after it got removed from dom
|
@off()
|
||||||
|
@stopListening()
|
||||||
|
|
||||||
# add @title methode to set title
|
# add @title methode to set title
|
||||||
title: (name, translate = false) ->
|
title: (name, translate = false) ->
|
||||||
|
@ -452,6 +454,7 @@ class App.ControllerModal extends App.Controller
|
||||||
buttonCancel: false
|
buttonCancel: false
|
||||||
buttonCancelClass: 'btn--text btn--subtle'
|
buttonCancelClass: 'btn--text btn--subtle'
|
||||||
buttonSubmit: true
|
buttonSubmit: true
|
||||||
|
includeForm: true
|
||||||
headPrefix: ''
|
headPrefix: ''
|
||||||
shown: true
|
shown: true
|
||||||
closeOnAnyClick: false
|
closeOnAnyClick: false
|
||||||
|
@ -516,6 +519,7 @@ class App.ControllerModal extends App.Controller
|
||||||
buttonClass: @buttonClass
|
buttonClass: @buttonClass
|
||||||
centerButtons: @centerButtons
|
centerButtons: @centerButtons
|
||||||
leftButtons: @leftButtons
|
leftButtons: @leftButtons
|
||||||
|
includeForm: @includeForm
|
||||||
))
|
))
|
||||||
modal.find('.modal-body').html(content)
|
modal.find('.modal-body').html(content)
|
||||||
if !@initRenderingDone
|
if !@initRenderingDone
|
||||||
|
@ -554,18 +558,19 @@ class App.ControllerModal extends App.Controller
|
||||||
if @small
|
if @small
|
||||||
@el.addClass('modal--small')
|
@el.addClass('modal--small')
|
||||||
|
|
||||||
@el.modal(
|
@el
|
||||||
keyboard: @keyboard
|
.on(
|
||||||
show: true
|
'show.bs.modal': @localOnShow
|
||||||
backdrop: @backdrop
|
'shown.bs.modal': @localOnShown
|
||||||
container: @container
|
'hide.bs.modal': @localOnClose
|
||||||
).on(
|
'hidden.bs.modal': @localOnClosed
|
||||||
'show.bs.modal': @localOnShow
|
'dismiss.bs.modal': @localOnCancel
|
||||||
'shown.bs.modal': @localOnShown
|
).modal(
|
||||||
'hide.bs.modal': @localOnClose
|
keyboard: @keyboard
|
||||||
'hidden.bs.modal': @localOnClosed
|
show: true
|
||||||
'dismiss.bs.modal': @localOnCancel
|
backdrop: @backdrop
|
||||||
)
|
container: @container
|
||||||
|
)
|
||||||
|
|
||||||
if @closeOnAnyClick
|
if @closeOnAnyClick
|
||||||
@el.on('click', =>
|
@el.on('click', =>
|
||||||
|
@ -604,7 +609,7 @@ class App.ControllerModal extends App.Controller
|
||||||
|
|
||||||
onShown: (e) =>
|
onShown: (e) =>
|
||||||
if @autoFocusOnFirstInput
|
if @autoFocusOnFirstInput
|
||||||
@$('input:not([disabled]):not([type="hidden"]):not(".btn"), textarea').first().focus()
|
@$('input:not([disabled]):not([type="hidden"]):not(".btn"):not([type="radio"]:not(:checked)), textarea').first().focus()
|
||||||
@initalFormParams = @formParams()
|
@initalFormParams = @formParams()
|
||||||
|
|
||||||
localOnClose: (e) =>
|
localOnClose: (e) =>
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
class App.ControllerForm extends App.Controller
|
class App.ControllerForm extends App.Controller
|
||||||
|
fullFormSubmitLabel: 'Submit'
|
||||||
|
fullFormSubmitAdditionalClasses: ''
|
||||||
|
fullFormButtonsContainerClass: ''
|
||||||
|
fullFormAdditionalButtons: [] # [{className: 'js-class', text: 'Label'}]
|
||||||
|
|
||||||
constructor: (params) ->
|
constructor: (params) ->
|
||||||
super
|
super
|
||||||
for key, value of params
|
for key, value of params
|
||||||
|
@ -71,7 +76,9 @@ class App.ControllerForm extends App.Controller
|
||||||
App.Log.debug 'ControllerForm', 'formGen', @model.configure_attributes
|
App.Log.debug 'ControllerForm', 'formGen', @model.configure_attributes
|
||||||
|
|
||||||
# check if own fieldset should be generated
|
# check if own fieldset should be generated
|
||||||
if @noFieldset
|
# forced when the form is a grid form because flex-wrap doesn't work on fieldsets
|
||||||
|
# source: https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers
|
||||||
|
if @noFieldset || @grid
|
||||||
fieldset = @el
|
fieldset = @el
|
||||||
else
|
else
|
||||||
fieldset = $('<fieldset></fieldset>')
|
fieldset = $('<fieldset></fieldset>')
|
||||||
|
@ -127,7 +134,24 @@ class App.ControllerForm extends App.Controller
|
||||||
if @fullForm
|
if @fullForm
|
||||||
if !@formClass
|
if !@formClass
|
||||||
@formClass = ''
|
@formClass = ''
|
||||||
fieldset = $('<form class="' + @formClass + '" autocomplete="off"><button class="btn">' + App.i18n.translateContent('Submit') + '</button></form>').prepend(fieldset)
|
|
||||||
|
fieldset = $("<form class='form #{@formClass}' autocomplete='off'>").prepend(fieldset)
|
||||||
|
container = $("<div class='form-buttons #{@fullFormButtonsContainerClass}'>")
|
||||||
|
|
||||||
|
for buttonConfig in @fullFormAdditionalButtons
|
||||||
|
btn = $("<button class='btn #{buttonConfig.className}'>").text(buttonConfig.text)
|
||||||
|
if buttonConfig.disabled
|
||||||
|
btn.prop('disabled', true)
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
$("<button type=submit class='btn #{@fullFormSubmitAdditionalClasses}\' value=\"#{@fullFormSubmitLabel}\"></button>")
|
||||||
|
.text(App.i18n.translateContent(@fullFormSubmitLabel))
|
||||||
|
.appendTo(container)
|
||||||
|
|
||||||
|
container.appendTo(fieldset)
|
||||||
|
|
||||||
|
#fieldset = $("<form class=\"#{@formClass}\" autocomplete=\"off\"><div class='horizontal #{@fullFormButtonsContainerClass}'><input type=submit class=\"btn #{@fullFormSubmitAdditionalClasses}\" value=\"#{label}\"></div></form>").prepend(fieldset)
|
||||||
|
#fieldset = $("<form class=\"#{@formClass}\" autocomplete=\"off\"><input type=submit class=\"btn #{@fullFormSubmitAdditionalClasses}\" value=\"#{label}\"></form>").prepend(fieldset)
|
||||||
|
|
||||||
# bind form events
|
# bind form events
|
||||||
if @events
|
if @events
|
||||||
|
@ -258,11 +282,15 @@ class App.ControllerForm extends App.Controller
|
||||||
# set params value
|
# set params value
|
||||||
if @params
|
if @params
|
||||||
|
|
||||||
# check if we have a references
|
|
||||||
parts = attribute.name.split '::'
|
parts = attribute.name.split '::'
|
||||||
if parts[0] && parts[1]
|
|
||||||
if @params[ parts[0] ] && parts[1] of @params[ parts[0] ]
|
if parts.length > 1
|
||||||
attribute.value = @params[ parts[0] ][ parts[1] ]
|
deepValue = parts.reduce((memo, elem) ->
|
||||||
|
memo?[elem]
|
||||||
|
, @params)
|
||||||
|
|
||||||
|
if deepValue isnt undefined
|
||||||
|
attribute.value = deepValue
|
||||||
|
|
||||||
# set params value to default
|
# set params value to default
|
||||||
if attribute.name of @params
|
if attribute.name of @params
|
||||||
|
@ -426,11 +454,16 @@ class App.ControllerForm extends App.Controller
|
||||||
)
|
)
|
||||||
|
|
||||||
# get all params of the form
|
# get all params of the form
|
||||||
@params: (form) ->
|
# set clearAccessories to true to remove inline image resizing handles
|
||||||
|
@params: (form, clearAccessories = false) ->
|
||||||
param = {}
|
param = {}
|
||||||
|
|
||||||
lookupForm = @findForm(form)
|
lookupForm = @findForm(form)
|
||||||
|
|
||||||
|
if clearAccessories
|
||||||
|
# remove inline image resizing handles
|
||||||
|
lookupForm.find('.richtext.form-control').trigger('click')
|
||||||
|
|
||||||
# get contenteditable
|
# get contenteditable
|
||||||
for element in lookupForm.find('[contenteditable]')
|
for element in lookupForm.find('[contenteditable]')
|
||||||
name = $(element).data('name')
|
name = $(element).data('name')
|
||||||
|
@ -656,6 +689,9 @@ class App.ControllerForm extends App.Controller
|
||||||
# set forms to read only during communication with backend
|
# set forms to read only during communication with backend
|
||||||
lookupForm.find('button, input, select, textarea').prop('readonly', true)
|
lookupForm.find('button, input, select, textarea').prop('readonly', true)
|
||||||
|
|
||||||
|
# disable radio and checbkox buttons
|
||||||
|
lookupForm.find('input[type=checkbox], input[type=radio]').prop('disabled', true)
|
||||||
|
|
||||||
# disable additionals submits
|
# disable additionals submits
|
||||||
lookupForm.find('button').prop('disabled', true)
|
lookupForm.find('button').prop('disabled', true)
|
||||||
else
|
else
|
||||||
|
@ -678,6 +714,9 @@ class App.ControllerForm extends App.Controller
|
||||||
# enable fields again
|
# enable fields again
|
||||||
lookupForm.find('button, input, select, textarea').prop('readonly', false)
|
lookupForm.find('button, input, select, textarea').prop('readonly', false)
|
||||||
|
|
||||||
|
# enable radio and checbkox buttons
|
||||||
|
lookupForm.find('input[type=checkbox], input[type=radio]').prop('disabled', false)
|
||||||
|
|
||||||
# enable submits again
|
# enable submits again
|
||||||
lookupForm.find('button').prop('disabled', false)
|
lookupForm.find('button').prop('disabled', false)
|
||||||
else
|
else
|
||||||
|
|
|
@ -403,6 +403,8 @@ class App.ControllerTabs extends App.Controller
|
||||||
subHeader: @subHeader
|
subHeader: @subHeader
|
||||||
tabs: @tabs
|
tabs: @tabs
|
||||||
addTab: @addTab
|
addTab: @addTab
|
||||||
|
headerSwitchName: @headerSwitchName
|
||||||
|
headerSwitchChecked: @headerSwitchChecked
|
||||||
)
|
)
|
||||||
|
|
||||||
# insert content
|
# insert content
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
class App.ControllerReorderModal extends App.ControllerModal
|
||||||
|
head: 'Drag to reorder'
|
||||||
|
content: ->
|
||||||
|
view = $(App.view('reorder_modal')())
|
||||||
|
|
||||||
|
table = new App.ControllerTable(
|
||||||
|
baseColWidth: null
|
||||||
|
dndCallback: ->
|
||||||
|
true
|
||||||
|
overview: ['title']
|
||||||
|
attribute_list: [
|
||||||
|
{ name: 'title', display: 'Name' }
|
||||||
|
]
|
||||||
|
objects: @items
|
||||||
|
)
|
||||||
|
|
||||||
|
view.find('.js-table-container').html(table.el)
|
||||||
|
|
||||||
|
view
|
||||||
|
|
||||||
|
onShown: ->
|
||||||
|
super
|
||||||
|
@$('.js-submit').focus()
|
||||||
|
|
||||||
|
save: ->
|
||||||
|
ids = @$('tr.item').toArray().map (el) -> parseInt(el.dataset.id)
|
||||||
|
|
||||||
|
@$('.alert').addClass('hidden')
|
||||||
|
|
||||||
|
@formDisable(@el)
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'reorder_save'
|
||||||
|
type: 'PATCH'
|
||||||
|
data: JSON.stringify({ordered_ids: ids})
|
||||||
|
url: @url
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data)
|
||||||
|
App.Event.trigger 'knowledge_base::sidebar::rerender'
|
||||||
|
@close()
|
||||||
|
error: (xhr) =>
|
||||||
|
data = JSON.parse(xhr.responseText)
|
||||||
|
@$('.alert--danger').removeClass('hidden').text(data.error)
|
||||||
|
@formEnable(@el)
|
||||||
|
)
|
||||||
|
|
||||||
|
onSubmit: ->
|
||||||
|
super
|
||||||
|
@save()
|
||||||
|
|
|
@ -116,6 +116,7 @@ class App.ControllerTable extends App.Controller
|
||||||
shownPage: 0
|
shownPage: 0
|
||||||
|
|
||||||
destroy: false
|
destroy: false
|
||||||
|
customActions: []
|
||||||
|
|
||||||
columnsLength: undefined
|
columnsLength: undefined
|
||||||
headers: undefined
|
headers: undefined
|
||||||
|
@ -544,7 +545,7 @@ class App.ControllerTable extends App.Controller
|
||||||
|
|
||||||
# get header data
|
# get header data
|
||||||
@headers = []
|
@headers = []
|
||||||
@actions = []
|
@actions = [].concat @customActions
|
||||||
availableWidth = @availableWidth
|
availableWidth = @availableWidth
|
||||||
for item in @overviewAttributes
|
for item in @overviewAttributes
|
||||||
headerFound = false
|
headerFound = false
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
class App.ManageKnowledgeBase extends App.ControllerTabs
|
||||||
|
header: 'Knowledge Base'
|
||||||
|
headerSwitchName: 'kb-activate'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'hidden.bs.tab li': 'didHideTab'
|
||||||
|
'show.bs.tab li': 'willShowTab'
|
||||||
|
'change .js-header-switch input': 'didChangeHeaderSwitch'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-header-switch input': 'headerSwitchInput'
|
||||||
|
|
||||||
|
didHideTab: (e) ->
|
||||||
|
selector = $(e.relatedTarget).attr('href')
|
||||||
|
@$(selector).trigger('hidden.bs.tab')
|
||||||
|
|
||||||
|
willShowTab: (e) ->
|
||||||
|
selector = $(e.target).attr('href')
|
||||||
|
@$(selector).trigger('show.bs.tab')
|
||||||
|
|
||||||
|
tabs: []
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@render()
|
||||||
|
@fetchAndRender()
|
||||||
|
|
||||||
|
fetchAndRender: =>
|
||||||
|
@startLoading()
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'knowledge_bases_init_admin'
|
||||||
|
type: 'GET'
|
||||||
|
url: @apiPath + '/knowledge_bases/manage/init'
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data)
|
||||||
|
|
||||||
|
@knowledge_base_id = App.KnowledgeBase.first()?.id
|
||||||
|
@stopLoading()
|
||||||
|
@processLoaded()
|
||||||
|
error: (xhr) =>
|
||||||
|
@knowledge_base_id = undefined
|
||||||
|
@stopLoading()
|
||||||
|
)
|
||||||
|
|
||||||
|
clear: ->
|
||||||
|
App.KnowledgeBase.find(@knowledge_base_id).remove(clear: true)
|
||||||
|
@fetchAndRender()
|
||||||
|
|
||||||
|
processLoaded: ->
|
||||||
|
if @knowledge_base_id
|
||||||
|
@renderLoaded()
|
||||||
|
else
|
||||||
|
@renderNonExistant()
|
||||||
|
|
||||||
|
renderNonExistant: ->
|
||||||
|
@renderScreenError(detail: 'No Knowledge Base. Please create first Knowledge Base', el: @$('.page-content'))
|
||||||
|
@headerSwitchInput.prop('checked', false)
|
||||||
|
|
||||||
|
new App.KnowledgeBaseNewModal(
|
||||||
|
parentVC: @
|
||||||
|
container: @el.closest('.main')
|
||||||
|
)
|
||||||
|
|
||||||
|
didChangeHeaderSwitch: ->
|
||||||
|
@headerSwitchInput.prop('disabled', true)
|
||||||
|
|
||||||
|
upcomingState = @headerSwitchInput.prop('checked')
|
||||||
|
action = if upcomingState then 'activate' else 'deactivate'
|
||||||
|
kb = App.KnowledgeBase.find(@knowledge_base_id)
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'knowledge_bases_init_admin'
|
||||||
|
type: 'PATCH'
|
||||||
|
url: kb.manageUrl(action)
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data)
|
||||||
|
@processLoaded()
|
||||||
|
@headerSwitchInput.prop('disabled', false)
|
||||||
|
error: (xhr) =>
|
||||||
|
@headerSwitchInput.prop('checked', !upcomingState)
|
||||||
|
@headerSwitchInput.prop('disabled', false)
|
||||||
|
)
|
||||||
|
|
||||||
|
renderLoaded: ->
|
||||||
|
params = {
|
||||||
|
knowledge_base_id: @knowledge_base_id
|
||||||
|
parentVC: @
|
||||||
|
}
|
||||||
|
|
||||||
|
@tabs = [
|
||||||
|
{
|
||||||
|
name: 'Style'
|
||||||
|
target: 'style'
|
||||||
|
controller: App.KnowledgeBaseForm
|
||||||
|
params: _.extend({}, params, { screen: 'style', split: true })
|
||||||
|
},{
|
||||||
|
name: 'Languages'
|
||||||
|
target: 'languages'
|
||||||
|
controller: App.KnowledgeBaseForm
|
||||||
|
params: _.extend({}, params, { screen: 'languages' })
|
||||||
|
},{
|
||||||
|
name: 'Public Menu'
|
||||||
|
target: 'public_menu'
|
||||||
|
controller: App.KnowledgeBasePublicMenuForm
|
||||||
|
params: _.extend({}, params, { screen: 'public_menu' })
|
||||||
|
},{
|
||||||
|
name: 'Delete'
|
||||||
|
target: 'delete'
|
||||||
|
controller: App.KnowledgeBaseDelete
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if !App.Config.get('system_online_service')
|
||||||
|
@tabs.splice(-1, 0, {
|
||||||
|
name: 'Custom Address'
|
||||||
|
target: 'custom_address'
|
||||||
|
controller: App.KnowledgeBaseCustomAddressForm,
|
||||||
|
params: _.extend({}, params, { screen: 'custom_address' })
|
||||||
|
})
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
|
@headerSwitchInput.prop('checked', App.KnowledgeBase.find(@knowledge_base_id).active)
|
||||||
|
|
||||||
|
App.Config.set('KnowledgeBase', { prio: 10000, name: 'Knowledge Base', parent: '#manage', target: '#manage/knowledge_base', controller: App.ManageKnowledgeBase, permission: ['admin.knowledge_base'] }, 'NavBarAdmin')
|
|
@ -49,8 +49,9 @@ class Index extends App.ControllerSubContent
|
||||||
delete: (e) =>
|
delete: (e) =>
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
id = $(e.currentTarget).data('token-id')
|
||||||
|
|
||||||
callback = =>
|
callback = =>
|
||||||
id = $(e.target).closest('a').data('token-id')
|
|
||||||
@ajax(
|
@ajax(
|
||||||
id: 'user_access_token_delete'
|
id: 'user_access_token_delete'
|
||||||
type: 'DELETE'
|
type: 'DELETE'
|
||||||
|
|
|
@ -163,12 +163,21 @@ class App.UiElement.ApplicationUiElement
|
||||||
if attribute.translate
|
if attribute.translate
|
||||||
nameNew = App.i18n.translateInline(nameNew)
|
nameNew = App.i18n.translateInline(nameNew)
|
||||||
|
|
||||||
attribute.options.push
|
row =
|
||||||
value: item.id,
|
value: item.id,
|
||||||
note: item.note,
|
note: item.note,
|
||||||
name: nameNew,
|
name: nameNew,
|
||||||
title: if item.email then item.email else nameNew
|
title: if item.email then item.email else nameNew
|
||||||
|
|
||||||
|
if item.graphic
|
||||||
|
row.graphic = item.graphic
|
||||||
|
|
||||||
|
# only used for graphics
|
||||||
|
if item.aspect_ratio
|
||||||
|
row.aspect_ratio = item.aspect_ratio
|
||||||
|
|
||||||
|
attribute.options.push row
|
||||||
|
|
||||||
attribute.sortBy = null
|
attribute.sortBy = null
|
||||||
|
|
||||||
# execute filter
|
# execute filter
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
# coffeelint: disable=camel_case_classes
|
# coffeelint: disable=camel_case_classes
|
||||||
class App.UiElement.autocompletion_ajax
|
class App.UiElement.autocompletion_ajax
|
||||||
@render: (attribute, params = {}) ->
|
@render: (attribute, params = {}, form) ->
|
||||||
|
|
||||||
if params[attribute.name] || attribute.value
|
if params[attribute.name] || attribute.value
|
||||||
object = App[attribute.relation].find(params[attribute.name] || attribute.value)
|
object = App[attribute.relation].find(params[attribute.name] || attribute.value)
|
||||||
valueName = object.displayName()
|
valueName = object.displayName()
|
||||||
|
|
||||||
# selectable search
|
# selectable search
|
||||||
searchableAjaxSelectObject = new App.SearchableAjaxSelect(
|
searchableAjaxSelectObject = new App.SearchableAjaxSelect(
|
||||||
|
delegate: form
|
||||||
attribute:
|
attribute:
|
||||||
value: params[attribute.name] || attribute.value
|
value: params[attribute.name] || attribute.value
|
||||||
valueName: valueName
|
valueName: valueName
|
||||||
name: attribute.name
|
name: attribute.name
|
||||||
id: params.organization_id || attribute.value
|
id: params.organization_id || attribute.id
|
||||||
placeholder: App.i18n.translateInline('Search...')
|
placeholder: App.i18n.translateInline('Search...')
|
||||||
limit: 40
|
limit: 40
|
||||||
object: attribute.relation
|
object: attribute.relation
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.UiElement.color extends App.UiElement.ApplicationUiElement
|
||||||
|
@render: (attribute, params) ->
|
||||||
|
new App.Color(attribute: attribute).element()
|
|
@ -0,0 +1,4 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.UiElement.icon_picker extends App.UiElement.ApplicationUiElement
|
||||||
|
@render: (attribute, params) ->
|
||||||
|
new App.IconPicker(attribute: attribute).element()
|
|
@ -0,0 +1,4 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.UiElement.iconset_picker extends App.UiElement.ApplicationUiElement
|
||||||
|
@render: (attribute, params) ->
|
||||||
|
new App.IconsetPicker(attribute: attribute).element()
|
|
@ -0,0 +1,34 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.UiElement.multi_locales extends App.UiElement.ApplicationUiElement
|
||||||
|
@render: (attribute, params, form) ->
|
||||||
|
new App.MultiLocales(attribute: attribute, object: form?.parentController?.object()).el
|
||||||
|
|
||||||
|
@prepareParams: (attribute, dom, params) ->
|
||||||
|
if typeof params[attribute.name] == 'string'
|
||||||
|
params[attribute.name] = [params[attribute.name]]
|
||||||
|
|
||||||
|
if !Array.isArray params[attribute.name]
|
||||||
|
return
|
||||||
|
|
||||||
|
primary_system_locale_id = dom.find("[name=#{attribute.name}_primary_locale_id]:checked").val()
|
||||||
|
|
||||||
|
params["#{attribute.name}_attributes"] = params[attribute.name]
|
||||||
|
.filter (elem) -> elem
|
||||||
|
.map (system_locale_id) ->
|
||||||
|
data = {
|
||||||
|
system_locale_id: system_locale_id
|
||||||
|
primary: system_locale_id == primary_system_locale_id
|
||||||
|
}
|
||||||
|
|
||||||
|
domRow = dom.find(".js-primary input[value=#{system_locale_id}]").closest('tr')
|
||||||
|
|
||||||
|
if domRow.hasClass('settings-list--deleted')
|
||||||
|
data['_destroy'] = '1'
|
||||||
|
|
||||||
|
if (kb_locale_id = domRow.data('kbLocaleId'))
|
||||||
|
data['id'] = parseInt(kb_locale_id)
|
||||||
|
|
||||||
|
data
|
||||||
|
|
||||||
|
delete params["#{attribute.name}"]
|
||||||
|
delete params["#{attribute.name}_primary_locale_id"]
|
|
@ -1,5 +1,7 @@
|
||||||
# coffeelint: disable=camel_case_classes
|
# coffeelint: disable=camel_case_classes
|
||||||
class App.UiElement.radio extends App.UiElement.ApplicationUiElement
|
class App.UiElement.radio extends App.UiElement.ApplicationUiElement
|
||||||
|
@template_name: 'radio'
|
||||||
|
|
||||||
@render: (attribute, params) ->
|
@render: (attribute, params) ->
|
||||||
|
|
||||||
# build options list based on config
|
# build options list based on config
|
||||||
|
@ -23,4 +25,4 @@ class App.UiElement.radio extends App.UiElement.ApplicationUiElement
|
||||||
# filter attributes
|
# filter attributes
|
||||||
@filterOption(attribute, params)
|
@filterOption(attribute, params)
|
||||||
|
|
||||||
$( App.view('generic/radio')( attribute: attribute ) )
|
$( App.view("generic/#{@template_name}")( attribute: attribute ) )
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.UiElement.radio_graphic extends App.UiElement.radio
|
||||||
|
@template_name: 'radio_graphic'
|
|
@ -1,12 +1,19 @@
|
||||||
# coffeelint: disable=camel_case_classes
|
# coffeelint: disable=camel_case_classes
|
||||||
class App.UiElement.richtext
|
class App.UiElement.richtext
|
||||||
@render: (attribute, params) ->
|
@render: (attribute, params, form) ->
|
||||||
item = $( App.view('generic/richtext')(attribute: attribute) )
|
if _.isObject(attribute.value)
|
||||||
item.find('[contenteditable]').ce(
|
attribute.attachments = attribute.value.attachments
|
||||||
|
attribute.value = attribute.value.text
|
||||||
|
|
||||||
|
item = $( App.view('generic/richtext')(attribute: attribute, toolButtons: @toolButtons) )
|
||||||
|
@contenteditable = item.find('[contenteditable]').ce(
|
||||||
mode: attribute.type
|
mode: attribute.type
|
||||||
maxlength: attribute.maxlength
|
maxlength: attribute.maxlength
|
||||||
|
buttons: attribute.buttons
|
||||||
)
|
)
|
||||||
|
|
||||||
|
item.find('a.btn--action[data-action]').click (event) => @toolButtonClicked(event, form)
|
||||||
|
|
||||||
if attribute.plugins
|
if attribute.plugins
|
||||||
for plugin in attribute.plugins
|
for plugin in attribute.plugins
|
||||||
params = plugin.params || {}
|
params = plugin.params || {}
|
||||||
|
@ -25,6 +32,10 @@ class App.UiElement.richtext
|
||||||
for file in params.attachments
|
for file in params.attachments
|
||||||
renderFile(file)
|
renderFile(file)
|
||||||
|
|
||||||
|
if attribute.attachments
|
||||||
|
for file in attribute.attachments
|
||||||
|
renderFile(file)
|
||||||
|
|
||||||
# remove items
|
# remove items
|
||||||
item.find('.attachments').on('click', '.js-delete', (e) =>
|
item.find('.attachments').on('click', '.js-delete', (e) =>
|
||||||
id = $(e.currentTarget).data('id')
|
id = $(e.currentTarget).data('id')
|
||||||
|
@ -35,10 +46,12 @@ class App.UiElement.richtext
|
||||||
item
|
item
|
||||||
)
|
)
|
||||||
|
|
||||||
|
form_id = item.closest('form').find('[name=form_id]').val()
|
||||||
|
|
||||||
# delete attachment from storage
|
# delete attachment from storage
|
||||||
App.Ajax.request(
|
App.Ajax.request(
|
||||||
type: 'DELETE'
|
type: 'DELETE'
|
||||||
url: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}/items/#{id}"
|
url: "#{App.Config.get('api_path')}/upload_caches/#{form_id}/items/#{id}"
|
||||||
processData: false
|
processData: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,58 +69,101 @@ class App.UiElement.richtext
|
||||||
@attachmentsHolder = item.find('.attachments')
|
@attachmentsHolder = item.find('.attachments')
|
||||||
@cancelContainer = item.find('.js-cancel')
|
@cancelContainer = item.find('.js-cancel')
|
||||||
|
|
||||||
upload_initialize_callback = =>
|
u = => html5Upload.initialize(
|
||||||
form_id = item.closest('form').find('[name=form_id]').val()
|
uploadUrl: "#{App.Config.get('api_path')}/attachments"
|
||||||
html5Upload.initialize(
|
dropContainer: item.closest('form').get(0)
|
||||||
uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{form_id}"
|
cancelContainer: @cancelContainer
|
||||||
dropContainer: item.closest('form').get(0)
|
inputField: item.find('input').get(0)
|
||||||
cancelContainer: @cancelContainer
|
maxSimultaneousUploads: 1,
|
||||||
inputField: item.find('input').get(0)
|
key: 'File'
|
||||||
maxSimultaneousUploads: 1,
|
data:
|
||||||
key: 'File'
|
form_id: item.closest('form').find('[name=form_id]').val()
|
||||||
onFileAdded: (file) =>
|
onFileAdded: (file) =>
|
||||||
|
|
||||||
file.on(
|
file.on(
|
||||||
onStart: =>
|
onStart: =>
|
||||||
@attachmentPlaceholder.addClass('hide')
|
@attachmentPlaceholder.addClass('hide')
|
||||||
@attachmentUpload.removeClass('hide')
|
@attachmentUpload.removeClass('hide')
|
||||||
@cancelContainer.removeClass('hide')
|
@cancelContainer.removeClass('hide')
|
||||||
item.find('[contenteditable]').trigger('fileUploadStart')
|
item.find('[contenteditable]').trigger('fileUploadStart')
|
||||||
App.Log.debug 'UiElement.richtext', 'upload start'
|
App.Log.debug 'UiElement.richtext', 'upload start'
|
||||||
|
|
||||||
onAborted: =>
|
onAborted: =>
|
||||||
@attachmentPlaceholder.removeClass('hide')
|
@attachmentPlaceholder.removeClass('hide')
|
||||||
@attachmentUpload.addClass('hide')
|
@attachmentUpload.addClass('hide')
|
||||||
item.find('input').val('')
|
item.find('input').val('')
|
||||||
item.find('[contenteditable]').trigger('fileUploadStop', ['aborted'])
|
item.find('[contenteditable]').trigger('fileUploadStop', ['aborted'])
|
||||||
|
|
||||||
# Called after received response from the server
|
# Called after received response from the server
|
||||||
onCompleted: (response) =>
|
onCompleted: (response) =>
|
||||||
response = JSON.parse(response)
|
response = JSON.parse(response)
|
||||||
|
|
||||||
@attachmentPlaceholder.removeClass('hide')
|
@attachmentPlaceholder.removeClass('hide')
|
||||||
@attachmentUpload.addClass('hide')
|
@attachmentUpload.addClass('hide')
|
||||||
|
|
||||||
# reset progress bar
|
# reset progress bar
|
||||||
@progressBar.width(parseInt(0) + '%')
|
@progressBar.width(parseInt(0) + '%')
|
||||||
@progressText.text('')
|
@progressText.text('')
|
||||||
|
|
||||||
renderFile(response.data)
|
renderFile(response.data)
|
||||||
item.find('input').val('')
|
item.find('input').val('')
|
||||||
item.find('[contenteditable]').trigger('fileUploadStop', ['completed'])
|
item.find('[contenteditable]').trigger('fileUploadStop', ['completed'])
|
||||||
App.Log.debug 'UiElement.richtext', 'upload complete', response.data
|
App.Log.debug 'UiElement.richtext', 'upload complete', response.data
|
||||||
|
|
||||||
# Called during upload progress, first parameter
|
# Called during upload progress, first parameter
|
||||||
# is decimal value from 0 to 100.
|
# is decimal value from 0 to 100.
|
||||||
onProgress: (progress, fileSize, uploadedBytes) =>
|
onProgress: (progress, fileSize, uploadedBytes) =>
|
||||||
@progressBar.width(parseInt(progress) + '%')
|
@progressBar.width(parseInt(progress) + '%')
|
||||||
@progressText.text(parseInt(progress))
|
@progressText.text(parseInt(progress))
|
||||||
# hide cancel on 90%
|
# hide cancel on 90%
|
||||||
if parseInt(progress) >= 90
|
if parseInt(progress) >= 90
|
||||||
@cancelContainer.addClass('hide')
|
@cancelContainer.addClass('hide')
|
||||||
App.Log.debug 'UiElement.richtext', 'uploadProgress ', parseInt(progress)
|
App.Log.debug 'UiElement.richtext', 'uploadProgress ', parseInt(progress)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
App.Delay.set(upload_initialize_callback, 100, undefined, 'form_upload')
|
)
|
||||||
|
App.Delay.set(u, 100, undefined, 'form_upload')
|
||||||
|
|
||||||
item
|
item
|
||||||
|
|
||||||
|
@toolButtonClicked: (event, form) ->
|
||||||
|
action = $(event.currentTarget).data('action')
|
||||||
|
@toolButtons[action]?.onClick(event, form)
|
||||||
|
|
||||||
|
@toolButtons = {}
|
||||||
|
@additions = {}
|
||||||
|
|
||||||
|
# 1 next, -1 previous
|
||||||
|
# jQuery's helper doesn't work because it doesn't include non-element nodes
|
||||||
|
@allDirectionalSiblings: (elem, direction, to = null) ->
|
||||||
|
if !elem?
|
||||||
|
return []
|
||||||
|
|
||||||
|
output = []
|
||||||
|
next = elem
|
||||||
|
|
||||||
|
while sibling = App.UiElement.richtext.directionalSibling(next, direction)
|
||||||
|
next = sibling
|
||||||
|
if to? and sibling is to
|
||||||
|
break
|
||||||
|
|
||||||
|
output.push sibling
|
||||||
|
|
||||||
|
output
|
||||||
|
|
||||||
|
# 1 next, -1 previous
|
||||||
|
@directionalSibling: (elem, direction) ->
|
||||||
|
if direction > 0
|
||||||
|
elem.nextSibling
|
||||||
|
else
|
||||||
|
elem.previousSibling
|
||||||
|
|
||||||
|
@buildParentsList: (elem, container) ->
|
||||||
|
$(elem)
|
||||||
|
.parentsUntil(container)
|
||||||
|
.toArray()
|
||||||
|
|
||||||
|
@buildParentsListWithSelf: (elem, container) ->
|
||||||
|
output = App.UiElement.richtext.buildParentsList(elem, container)
|
||||||
|
output.unshift(elem)
|
||||||
|
output
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
class App.UiElement.richtext.additions.RichTextToolButton
|
||||||
|
@icon: undefined # 'chain'
|
||||||
|
@text: undefined # 'Weblink'
|
||||||
|
|
||||||
|
@klass: ->
|
||||||
|
# Needs implementation. Return constructor of RichTextToolPopup subclass.
|
||||||
|
|
||||||
|
@initializeAttributes: {}
|
||||||
|
|
||||||
|
@instantiateContent: (event, selection, delegate) ->
|
||||||
|
attrs = @initializeAttributes
|
||||||
|
|
||||||
|
attrs['event'] = event
|
||||||
|
attrs['selection'] = selection
|
||||||
|
attrs['container'] = $(event.currentTarget).closest('.content')
|
||||||
|
attrs['delegate'] = delegate
|
||||||
|
|
||||||
|
klassConstructor = @klass()
|
||||||
|
instance = new klassConstructor(attrs)
|
||||||
|
instance.el
|
||||||
|
|
||||||
|
@popoverAttributes: (event, selection, delegate) ->
|
||||||
|
content = @instantiateContent(event, selection, delegate)
|
||||||
|
hash =
|
||||||
|
trigger: 'manual'
|
||||||
|
backdrop: true
|
||||||
|
html: true
|
||||||
|
animation: false
|
||||||
|
delay: 0
|
||||||
|
placement: 'auto right'
|
||||||
|
theme: 'dark'
|
||||||
|
content: content
|
||||||
|
container: 'body'
|
||||||
|
template: '<div class="popover popover--has-horizontal-form" role="tooltip"><div class="arrow"></div><h2 class="popover-title"></h2><div class="popover-content"></div></div>'
|
||||||
|
|
||||||
|
hash
|
||||||
|
|
||||||
|
@pickLinkInSingleContainer: (elem, containerToLookUpTo) ->
|
||||||
|
if elem.nodeName == 'A'
|
||||||
|
elem
|
||||||
|
else if innerLink = $(elem).find('a')[0]
|
||||||
|
innerLink
|
||||||
|
else if containerToLookUpTo and closestLink = $(elem).closest('a', containerToLookUpTo)[0]
|
||||||
|
closestLink
|
||||||
|
else
|
||||||
|
null
|
||||||
|
|
||||||
|
@pickLinkAt: (elem, container, direction, boundary = null) ->
|
||||||
|
for parent in App.UiElement.richtext.buildParentsListWithSelf(elem, container)
|
||||||
|
if parent.nodeName is 'A'
|
||||||
|
return parent
|
||||||
|
|
||||||
|
for elem in App.UiElement.richtext.allDirectionalSiblings(parent, direction, boundary)
|
||||||
|
if link = @pickLinkInSingleContainer(elem)
|
||||||
|
return link
|
||||||
|
|
||||||
|
null
|
||||||
|
|
||||||
|
@pickLink: (sel, textEditor) ->
|
||||||
|
range = sel.getRangeAt(0)
|
||||||
|
|
||||||
|
if range.startContainer == range.endContainer
|
||||||
|
return @pickLinkInSingleContainer(range.startContainer, textEditor)
|
||||||
|
|
||||||
|
if link = @pickLinkAt(range.startContainer, range.commonAncestorContainer, 1, range.endContainer)
|
||||||
|
return link
|
||||||
|
|
||||||
|
if startParent = App.UiElement.richtext.buildParentsList(range.startContainer, range.commonAncestorContainer).pop()
|
||||||
|
for elem in App.UiElement.richtext.allDirectionalSiblings(startParent, 1, range.endContainer)
|
||||||
|
if link = @pickLinkInSingleContainer(elem)
|
||||||
|
return link
|
||||||
|
|
||||||
|
if link = @pickLinkAt(range.endContainer, range.commonAncestorContainer, -1)
|
||||||
|
return link
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
# close other buttons' popovers
|
||||||
|
@closeOtherPopovers: (event) ->
|
||||||
|
$(event.currentTarget)
|
||||||
|
.closest('.richtext-controls')
|
||||||
|
.find('.btn')
|
||||||
|
.toArray()
|
||||||
|
.filter (elem) -> $(elem).attr('aria-describedby')
|
||||||
|
.forEach (elem) -> $(elem).popover('hide')
|
||||||
|
|
||||||
|
# normalize selection to parse later
|
||||||
|
@selectionSnapshot: (sel) ->
|
||||||
|
textEditor = $(event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||||
|
|
||||||
|
if sel.isCollapsed and selectedLink = $(sel.anchorNode).closest('a')[0]
|
||||||
|
{
|
||||||
|
type: 'existing'
|
||||||
|
dom: $(selectedLink)
|
||||||
|
}
|
||||||
|
else if !sel.isCollapsed and selectedLink = @pickLink(sel, textEditor)
|
||||||
|
{
|
||||||
|
type: 'existing'
|
||||||
|
dom: $(selectedLink)
|
||||||
|
}
|
||||||
|
else if sel.type is 'Range' and $(sel.anchorNode).closest('[contenteditable]', textEditor)[0]
|
||||||
|
range = sel.getRangeAt(0)
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'range'
|
||||||
|
range: sel.getRangeAt(0)
|
||||||
|
}
|
||||||
|
else if $(sel.anchorNode).closest('[contenteditable]', textEditor)[0] and !$(sel.anchorNode).is('[contenteditable]')
|
||||||
|
{
|
||||||
|
type: 'caret'
|
||||||
|
dom: $(sel.anchorNode)
|
||||||
|
offset: sel.anchorOffset
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type: 'append'
|
||||||
|
dom: textEditor
|
||||||
|
}
|
||||||
|
|
||||||
|
@onClick: (event, delegate) ->
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
# close popover if already open and stop
|
||||||
|
if $(event.currentTarget).attr('aria-describedby')
|
||||||
|
$(event.currentTarget).popover('hide')
|
||||||
|
return
|
||||||
|
|
||||||
|
@closeOtherPopovers(event)
|
||||||
|
|
||||||
|
textEditor = $(event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||||
|
|
||||||
|
sel = document.getSelection()
|
||||||
|
selectionSnapshot = @selectionSnapshot(sel)
|
||||||
|
sel.removeAllRanges()
|
||||||
|
|
||||||
|
$(event.currentTarget)
|
||||||
|
.popover(@popoverAttributes(event, selectionSnapshot, delegate))
|
||||||
|
.popover('show')
|
|
@ -0,0 +1,151 @@
|
||||||
|
class App.UiElement.richtext.additions.RichTextToolPopup extends App.ControllerForm
|
||||||
|
events:
|
||||||
|
'submit form': 'onSubmit'
|
||||||
|
'click .js-unlink': 'onUnlink'
|
||||||
|
|
||||||
|
formParams: (params) ->
|
||||||
|
# needs implementation
|
||||||
|
|
||||||
|
constructor: (params) ->
|
||||||
|
if params.selection.type is 'existing'
|
||||||
|
url = params.selection.dom.attr('href')
|
||||||
|
label = 'Update'
|
||||||
|
additional = [{
|
||||||
|
className: 'btn btn--danger js-unlink'
|
||||||
|
text: 'Remove'
|
||||||
|
}]
|
||||||
|
else
|
||||||
|
label = 'Link'
|
||||||
|
|
||||||
|
defaultParams =
|
||||||
|
params: @formParams(params)
|
||||||
|
fullForm: true
|
||||||
|
formClass: 'form--horizontal'
|
||||||
|
fullFormSubmitLabel: label
|
||||||
|
fullFormSubmitAdditionalClasses: 'btn--create'
|
||||||
|
fullFormAdditionalButtons: additional
|
||||||
|
autofocus: true
|
||||||
|
model:
|
||||||
|
configure_attributes: []
|
||||||
|
|
||||||
|
fullParams = $.extend(true, {}, defaultParams, params)
|
||||||
|
|
||||||
|
super(fullParams)
|
||||||
|
|
||||||
|
@didInitialize()
|
||||||
|
|
||||||
|
$(@event.currentTarget).on('hidden.bs.popover', (e) => @willClose(e))
|
||||||
|
|
||||||
|
getAjaxAttributes: (field, attributes) ->
|
||||||
|
@delegate?.getAjaxAttributes?(field, attributes)
|
||||||
|
|
||||||
|
onUnlink: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
switch @selection.type
|
||||||
|
when 'existing'
|
||||||
|
$(@selection.dom).contents().unwrap()
|
||||||
|
|
||||||
|
$(@event.currentTarget).popover('hide')
|
||||||
|
|
||||||
|
@wrapElement: (wrapper, selection) ->
|
||||||
|
topLevelOriginals = App.UiElement.richtext.buildParentsList(selection.range.startContainer, selection.range.commonAncestorContainer).reverse()
|
||||||
|
|
||||||
|
if topLevelOriginalStart = topLevelOriginals.shift()
|
||||||
|
clonedStart = topLevelOriginalStart.cloneNode(false)
|
||||||
|
nextParent = clonedStart
|
||||||
|
|
||||||
|
for orig in topLevelOriginals
|
||||||
|
clone = orig.cloneNode(false)
|
||||||
|
nextParent.append(clone)
|
||||||
|
|
||||||
|
for elem in App.UiElement.richtext.allDirectionalSiblings(orig, 1)
|
||||||
|
nextParent.append(elem.cloneNode(true))
|
||||||
|
|
||||||
|
nextParent = clone
|
||||||
|
|
||||||
|
startClone = selection.range.startContainer.cloneNode(true)
|
||||||
|
remaining = startClone.splitText(selection.range.startOffset)
|
||||||
|
nextParent.append(remaining)
|
||||||
|
|
||||||
|
wrapper.append(clonedStart)
|
||||||
|
|
||||||
|
for elem in App.UiElement.richtext.allDirectionalSiblings(selection.range.startContainer, 1)
|
||||||
|
nextParent.append(elem.cloneNode(true))
|
||||||
|
else
|
||||||
|
topLevelOriginalStart = selection.range.startContainer
|
||||||
|
startClone = selection.range.startContainer.cloneNode(true)
|
||||||
|
remaining = startClone.splitText(selection.range.startOffset)
|
||||||
|
wrapper.append(remaining)
|
||||||
|
|
||||||
|
for elem in App.UiElement.richtext.allDirectionalSiblings(topLevelOriginalStart, 1, selection.range.endContainer)
|
||||||
|
wrapper.append(elem.cloneNode(true))
|
||||||
|
|
||||||
|
topLevelOriginals = App.UiElement.richtext.buildParentsList(selection.range.endContainer, selection.range.commonAncestorContainer).reverse()
|
||||||
|
|
||||||
|
if topLevelOriginalEnd = topLevelOriginals.shift()
|
||||||
|
clonedEnd = topLevelOriginalEnd.cloneNode(false)
|
||||||
|
nextParent = clonedEnd
|
||||||
|
|
||||||
|
for orig in topLevelOriginals
|
||||||
|
clone = orig.cloneNode(false)
|
||||||
|
nextParent.append(clone)
|
||||||
|
|
||||||
|
for elem in App.UiElement.richtext.allDirectionalSiblings(orig, -1)
|
||||||
|
nextParent.prepend(elem.cloneNode(true))
|
||||||
|
|
||||||
|
nextParent = clone
|
||||||
|
|
||||||
|
endClone = selection.range.endContainer.cloneNode(true)
|
||||||
|
endClone.splitText(selection.range.endOffset)
|
||||||
|
nextParent.append(endClone)
|
||||||
|
|
||||||
|
wrapper.append(clonedEnd)
|
||||||
|
else
|
||||||
|
endClone = selection.range.endContainer.cloneNode(true)
|
||||||
|
endClone.splitText(selection.range.endOffset)
|
||||||
|
wrapper.append(endClone)
|
||||||
|
|
||||||
|
document.getSelection().removeAllRanges()
|
||||||
|
document.getSelection().addRange(selection.range)
|
||||||
|
document.getSelection().deleteFromDocument()
|
||||||
|
document.getSelection().removeAllRanges()
|
||||||
|
|
||||||
|
wrapper.insertAfter(topLevelOriginalStart)
|
||||||
|
|
||||||
|
wrapLink: ->
|
||||||
|
# needs implementation
|
||||||
|
|
||||||
|
onSubmit: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
@wrapLink()
|
||||||
|
|
||||||
|
$(@event.currentTarget).popover('destroy')
|
||||||
|
|
||||||
|
didInitialize: ->
|
||||||
|
switch @selection.type
|
||||||
|
when 'existing'
|
||||||
|
@selection.dom.addClass('highlight-emulator')
|
||||||
|
when 'range'
|
||||||
|
span = $('<span>').addClass('highlight-emulator')
|
||||||
|
|
||||||
|
if @selection.range.startContainer == @selection.range.endContainer
|
||||||
|
@selection.range.startContainer.splitText(@selection.range.endOffset)
|
||||||
|
visibleText = @selection.range.startContainer.splitText(@selection.range.startOffset)
|
||||||
|
|
||||||
|
$(visibleText).wrap(span)
|
||||||
|
else
|
||||||
|
@constructor.wrapElement(span, @selection)
|
||||||
|
|
||||||
|
willClose: (e) ->
|
||||||
|
switch @selection.type
|
||||||
|
when 'existing'
|
||||||
|
@selection.dom.removeClass('highlight-emulator')
|
||||||
|
when 'range'
|
||||||
|
textEditor = $(@event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||||
|
textEditor.find('span.highlight-emulator').contents().unwrap()
|
||||||
|
|
||||||
|
$(@event.currentTarget).off('hidden.bs.popover')
|
||||||
|
$(e.currentTarget).popover('destroy')
|
|
@ -0,0 +1,15 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.UiElement.richtext.toolButtons.link_answer extends App.UiElement.richtext.additions.RichTextToolButton
|
||||||
|
@icon: 'knowledge-base-answer'
|
||||||
|
@text: 'Link Answer'
|
||||||
|
@klass: -> App.UiElement.richtext.additions.RichTextToolPopupAnswer
|
||||||
|
@initializeAttributes:
|
||||||
|
model:
|
||||||
|
configure_attributes: [
|
||||||
|
{
|
||||||
|
name: 'link'
|
||||||
|
display: 'Answer'
|
||||||
|
relation: 'KnowledgeBaseAnswerTranslation'
|
||||||
|
tag: 'autocompletion_ajax'
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,15 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.UiElement.richtext.toolButtons.link extends App.UiElement.richtext.additions.RichTextToolButton
|
||||||
|
@icon: 'chain'
|
||||||
|
@text: 'Weblink'
|
||||||
|
@klass: -> App.UiElement.richtext.additions.RichTextToolPopupLink
|
||||||
|
@initializeAttributes:
|
||||||
|
model:
|
||||||
|
configure_attributes: [
|
||||||
|
{
|
||||||
|
name: 'link'
|
||||||
|
display: 'Link'
|
||||||
|
tag: 'input'
|
||||||
|
placeholder: 'http://'
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,43 @@
|
||||||
|
class App.UiElement.richtext.additions.RichTextToolPopupAnswer extends App.UiElement.richtext.additions.RichTextToolPopup
|
||||||
|
formParams: (params) ->
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
url = if params.selection.type is 'existing' && params.selection.dom.attr('data-target-type') is 'knowledge-base-answer'
|
||||||
|
params.selection.dom.attr('data-target-id')
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
link: url
|
||||||
|
|
||||||
|
applyOnto: (dom, object, text = null) ->
|
||||||
|
dom
|
||||||
|
.attr('href', object.uiUrl('edit'))
|
||||||
|
.attr('data-target-id', object.id)
|
||||||
|
.attr('data-target-type', 'knowledge-base-answer')
|
||||||
|
|
||||||
|
if text?
|
||||||
|
dom.text(text)
|
||||||
|
|
||||||
|
dom
|
||||||
|
|
||||||
|
wrapLink: ->
|
||||||
|
id = @el.find('input').val()
|
||||||
|
object = App.KnowledgeBaseAnswerTranslation.find(id)
|
||||||
|
textEditor = $(@event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||||
|
|
||||||
|
switch @selection.type
|
||||||
|
when 'existing'
|
||||||
|
@applyOnto(@selection.dom, object)
|
||||||
|
when 'append'
|
||||||
|
newElem = $('<a>')
|
||||||
|
@applyOnto(newElem, object, object.title)
|
||||||
|
@selection.dom.append(newElem)
|
||||||
|
when 'caret'
|
||||||
|
newElem = $('<a>')
|
||||||
|
@applyOnto(newElem, object, object.title)
|
||||||
|
@selection.dom[0].splitText(@selection.offset)
|
||||||
|
newElem.insertAfter(@selection.dom)
|
||||||
|
when 'range'
|
||||||
|
placeholder = textEditor.find('span.highlight-emulator')
|
||||||
|
newElem = $('<a>')
|
||||||
|
@applyOnto(newElem, object)
|
||||||
|
placeholder.wrap(newElem)
|
||||||
|
placeholder.contents()
|
|
@ -0,0 +1,52 @@
|
||||||
|
class App.UiElement.richtext.additions.RichTextToolPopupLink extends App.UiElement.richtext.additions.RichTextToolPopup
|
||||||
|
formParams: (params) ->
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
url = if params.selection.type is 'existing' && !params.selection.dom.attr('data-target-type')?
|
||||||
|
params.selection.dom.attr('href')
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
link: url
|
||||||
|
|
||||||
|
applyOnto: (dom, url, text = null) ->
|
||||||
|
dom
|
||||||
|
.attr('href', url)
|
||||||
|
.removeAttr('data-target-id')
|
||||||
|
.removeAttr('data-target-type')
|
||||||
|
|
||||||
|
if text?
|
||||||
|
dom.text(text)
|
||||||
|
|
||||||
|
dom
|
||||||
|
|
||||||
|
ensureProtocol: (input) ->
|
||||||
|
input = input.trim()
|
||||||
|
|
||||||
|
if !input.match(/^\S+\:\/\//) and input[0] isnt '/'
|
||||||
|
'http://' + input
|
||||||
|
else
|
||||||
|
input
|
||||||
|
|
||||||
|
wrapLink: ->
|
||||||
|
input = @el.find('input').val()
|
||||||
|
url = @ensureProtocol(input)
|
||||||
|
|
||||||
|
textEditor = $(@event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
|
||||||
|
|
||||||
|
switch @selection.type
|
||||||
|
when 'existing'
|
||||||
|
@applyOnto(@selection.dom, url)
|
||||||
|
when 'append'
|
||||||
|
newElem = $('<a>')
|
||||||
|
@applyOnto(newElem, url, input)
|
||||||
|
@selection.dom.append(newElem)
|
||||||
|
when 'caret'
|
||||||
|
newElem = $('<a>')
|
||||||
|
@applyOnto(newElem, url, input)
|
||||||
|
@selection.dom[0].splitText?(@selection.offset)
|
||||||
|
newElem.insertAfter(@selection.dom)
|
||||||
|
when 'range'
|
||||||
|
placeholder = textEditor.find('span.highlight-emulator')
|
||||||
|
newElem = $('<a>')
|
||||||
|
@applyOnto(newElem, url)
|
||||||
|
placeholder.wrap(newElem)
|
||||||
|
placeholder.contents()
|
|
@ -51,6 +51,16 @@ class App.UiElement.select extends App.UiElement.ApplicationUiElement
|
||||||
return if value of attribute.options
|
return if value of attribute.options
|
||||||
return if value in (temp for own prop, temp of attribute.options)
|
return if value in (temp for own prop, temp of attribute.options)
|
||||||
|
|
||||||
|
if _.isArray(attribute.options)
|
||||||
|
# Array of Strings (value)
|
||||||
|
return if value of attribute.options
|
||||||
|
|
||||||
|
# Array of Objects (for ordering purposes)
|
||||||
|
return if attribute.options.filter((elem) -> elem.value == value) isnt null
|
||||||
|
else
|
||||||
|
# regular Object
|
||||||
|
return if value in (temp for own prop, temp of attribute.options)
|
||||||
|
|
||||||
if attribute.historical_options && value of attribute.historical_options
|
if attribute.historical_options && value of attribute.historical_options
|
||||||
attribute.options[value] = attribute.historical_options[value]
|
attribute.options[value] = attribute.historical_options[value]
|
||||||
else
|
else
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
class App.KnowledgeBaseAddForm extends App.ControllerModal
|
||||||
|
constructor: (params) ->
|
||||||
|
for key, value of params
|
||||||
|
@[key] = value
|
||||||
|
|
||||||
|
@head = @object.objectActionName()
|
||||||
|
super(params)
|
||||||
|
|
||||||
|
buttonSubmit: 'Create'
|
||||||
|
|
||||||
|
content: ->
|
||||||
|
kb_locale = @parentController.kb_locale()
|
||||||
|
@formController = new App.KnowledgeBaseFormController(@object, kb_locale, 'agent_create', $('<div>'))
|
||||||
|
@form = @formController.form # used for disabling inputs during saving
|
||||||
|
@formController.el
|
||||||
|
|
||||||
|
submit: (e) ->
|
||||||
|
@preventDefaultAndStopPropagation(e)
|
||||||
|
|
||||||
|
if !@formController.validateAndShowErrors()
|
||||||
|
return
|
||||||
|
|
||||||
|
params = @formController.paramsForSaving()
|
||||||
|
params.translations_attributes[0].content_attributes = { body: '' }
|
||||||
|
|
||||||
|
@parentController.coordinator.saveChanges(@object, params, @)
|
||||||
|
|
||||||
|
showAlert: (text) ->
|
||||||
|
@formController?.showAlert(text)
|
||||||
|
|
||||||
|
didSaveCallback: (data) ->
|
||||||
|
url = @object.constructor.find(data.id).uiUrl(@parentController.kb_locale(), 'edit')
|
||||||
|
@parentController.navigate(url)
|
|
@ -0,0 +1,385 @@
|
||||||
|
class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
|
className: 'knowledge-base vertical'
|
||||||
|
name: 'Knowledge Base'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-body': 'body'
|
||||||
|
'.js-navigation': 'navigation'
|
||||||
|
'.js-sidebar': 'sidebar'
|
||||||
|
|
||||||
|
constructor: (params) ->
|
||||||
|
super
|
||||||
|
@bind 'config_update_local', (data) => @configUpdated(data)
|
||||||
|
|
||||||
|
if @permissionCheck('knowledge_base.*') and App.Config.get('kb_active')
|
||||||
|
@updateNavMenu()
|
||||||
|
else if App.Config.get('kb_active_publicly')
|
||||||
|
@loadInitial(
|
||||||
|
{},
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@updateNavMenu()
|
||||||
|
)
|
||||||
|
|
||||||
|
configUpdated: (data) ->
|
||||||
|
if data.name isnt 'kb_active' and data.name isnt 'kb_active_publicly'
|
||||||
|
return
|
||||||
|
|
||||||
|
@updateNavMenu()
|
||||||
|
|
||||||
|
firstRunIfNeeded: ->
|
||||||
|
if @firstRunDone
|
||||||
|
return
|
||||||
|
|
||||||
|
@firstRunDone = true
|
||||||
|
|
||||||
|
@coordinator = new App.KnowledgeBaseEditorCoordinator(parentController: @)
|
||||||
|
|
||||||
|
@fetchAndRender()
|
||||||
|
|
||||||
|
@bind('ui:rerender',
|
||||||
|
=>
|
||||||
|
@render(true)
|
||||||
|
@contentController?.url = null
|
||||||
|
@lastParams.selectedSystemLocale = App.KnowledgeBaseLocale.detect(@getKnowledgeBase()).systemLocale()
|
||||||
|
@show(@lastParams)
|
||||||
|
)
|
||||||
|
|
||||||
|
@bind 'kb_data_changed', (pushed_data) =>
|
||||||
|
key = "kb_pull_#{pushed_data.class}_#{pushed_data.id}"
|
||||||
|
|
||||||
|
App.Delay.set( =>
|
||||||
|
@loadChange(pushed_data)
|
||||||
|
, 1000, key, 'kb_data_changed_loading')
|
||||||
|
|
||||||
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
|
return if !@displayingError
|
||||||
|
|
||||||
|
object = @constructor.pickObjectUsing(@lastParams, @)
|
||||||
|
|
||||||
|
if !@objectVisibleInternally(object)
|
||||||
|
return
|
||||||
|
|
||||||
|
@renderControllers(@lastParams)
|
||||||
|
|
||||||
|
@checkForUpdates()
|
||||||
|
|
||||||
|
loadChange: (pushed_data) =>
|
||||||
|
url = pushed_data.url + '?full=true'
|
||||||
|
|
||||||
|
if pushed_data.class is 'KnowledgeBase::Answer'
|
||||||
|
object = App.KnowledgeBaseAnswer.find pushed_data.id
|
||||||
|
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
loaded_ids = object
|
||||||
|
?.translations()
|
||||||
|
.map (elem) -> elem.content()?.id
|
||||||
|
.filter (elem) -> elem isnt undefined
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
if loaded_ids and loaded_ids.length isnt 0
|
||||||
|
url += '&include_contents=' + loaded_ids.join(',')
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: "kb_pull_#{pushed_data.class}_#{pushed_data.id}"
|
||||||
|
type: 'GET'
|
||||||
|
url: url
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
|
||||||
|
@notifyChangeLoaded()
|
||||||
|
error: (xhr) =>
|
||||||
|
if xhr.status != 404
|
||||||
|
return
|
||||||
|
|
||||||
|
klassName = pushed_data.class.replace(/::/g,'')
|
||||||
|
|
||||||
|
if object = App[klassName]?.find(pushed_data.id)
|
||||||
|
object.remove(clear: true)
|
||||||
|
@notifyChangeLoaded()
|
||||||
|
)
|
||||||
|
|
||||||
|
objectVisibleInternally: (object) ->
|
||||||
|
if !object
|
||||||
|
return false
|
||||||
|
else if object instanceof App.KnowledgeBaseAnswer and !object.exists()
|
||||||
|
return false
|
||||||
|
else if object instanceof App.KnowledgeBaseCategory and !object.visibleInternally(@kb_locale())
|
||||||
|
return false
|
||||||
|
|
||||||
|
true
|
||||||
|
|
||||||
|
notifyChangeLoaded: ->
|
||||||
|
App.KnowledgeBase.trigger('kb_data_change_loaded')
|
||||||
|
|
||||||
|
active: (state) ->
|
||||||
|
return @shown if state is undefined
|
||||||
|
@shown = state
|
||||||
|
|
||||||
|
featureActive: ->
|
||||||
|
(@permissionCheck('knowledge_base.*') and App.Config.get('kb_active')) or (App.Config.get('kb_active_publicly') and App.KnowledgeBase.first()?)
|
||||||
|
|
||||||
|
activeLocaleSuffix: ->
|
||||||
|
@kb_locale().urlSuffix()
|
||||||
|
|
||||||
|
requiredPermissionSuffix: (params) ->
|
||||||
|
if params.action is 'edit'
|
||||||
|
'editor'
|
||||||
|
else
|
||||||
|
'*'
|
||||||
|
|
||||||
|
show: (params) =>
|
||||||
|
@firstRunIfNeeded()
|
||||||
|
@navupdate '#knowledge_base'
|
||||||
|
|
||||||
|
@bodyModal?.close()
|
||||||
|
@bodyModal = null
|
||||||
|
|
||||||
|
if !@permissionCheckRedirect("knowledge_base.#{@requiredPermissionSuffix(params)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if @loaded && @rendered && @lastParams && !params.knowledge_base_id && @contentController && @kb_locale()?
|
||||||
|
@navigate @lastParams.match[0] , true
|
||||||
|
return
|
||||||
|
|
||||||
|
if @contentController && @contentController.url is params.match[0]
|
||||||
|
@title @lastTitle
|
||||||
|
@contentController.restoreVisibility?()
|
||||||
|
return
|
||||||
|
|
||||||
|
@rendered = true
|
||||||
|
|
||||||
|
@lastParams = params
|
||||||
|
|
||||||
|
if @loaded and params.selectedSystemLocale is null and params.selectedSystemLocalePresent
|
||||||
|
@renderError()
|
||||||
|
return
|
||||||
|
|
||||||
|
@displayingError = false
|
||||||
|
|
||||||
|
if @loaded
|
||||||
|
if params.knowledge_base_id
|
||||||
|
@renderControllers(params)
|
||||||
|
else
|
||||||
|
if (kb = App.KnowledgeBase.all()[0])
|
||||||
|
@navigate kb.uiUrl(App.KnowledgeBaseLocale.detect(kb)), true
|
||||||
|
else
|
||||||
|
@renderScreenErrorInContent('No Knowledge Base created')
|
||||||
|
else
|
||||||
|
@pendingParams = params
|
||||||
|
|
||||||
|
renderScreenErrorInContent: (text) ->
|
||||||
|
@contentController = undefined
|
||||||
|
@renderScreenError(detail: text, el: @$('.page-content'))
|
||||||
|
@displayingError = true
|
||||||
|
|
||||||
|
renderControllers: (params) ->
|
||||||
|
object = @constructor.pickObjectUsing(params, @)
|
||||||
|
|
||||||
|
if !object || (!@isEditor() && !object.visibleInternally(@kb_locale()))
|
||||||
|
@renderNotFound()
|
||||||
|
return
|
||||||
|
|
||||||
|
titleSuffix = if !(object instanceof App.KnowledgeBase)
|
||||||
|
object.guaranteedTitle(@kb_locale().id)
|
||||||
|
else if params.action is 'search'
|
||||||
|
App.i18n.translateInline('Search')
|
||||||
|
else
|
||||||
|
''
|
||||||
|
|
||||||
|
@updateTitle(titleSuffix)
|
||||||
|
|
||||||
|
klass = @contentControllerClass(params)
|
||||||
|
@contentController = @buildUsing(klass, params, object)
|
||||||
|
@navigationController?.show(object, params.action)
|
||||||
|
@sidebarController?.show(object, params.action)
|
||||||
|
|
||||||
|
updateTitle: (titleSuffix) ->
|
||||||
|
newTitle = @getKnowledgeBase()?.guaranteedTitle(@kb_locale()?.id) || ''
|
||||||
|
|
||||||
|
if titleSuffix != ''
|
||||||
|
if newTitle
|
||||||
|
newTitle += ' - '
|
||||||
|
|
||||||
|
newTitle += titleSuffix
|
||||||
|
|
||||||
|
@title newTitle
|
||||||
|
@lastTitle = newTitle
|
||||||
|
|
||||||
|
contentControllerClass: (params) ->
|
||||||
|
if params.action is 'search'
|
||||||
|
return App.KnowledgeBaseSearchController
|
||||||
|
|
||||||
|
if params.action is 'edit'
|
||||||
|
return App.KnowledgeBaseContentController
|
||||||
|
|
||||||
|
if params.answer_id
|
||||||
|
App.KnowledgeBaseReaderController
|
||||||
|
else
|
||||||
|
App.KnowledgeBaseReaderListController
|
||||||
|
|
||||||
|
edit: false
|
||||||
|
|
||||||
|
renderNotFound: ->
|
||||||
|
title = App.i18n.translateInline('Not Found')
|
||||||
|
@updateTitle(title)
|
||||||
|
@navigationController?.show(undefined, title)
|
||||||
|
@renderScreenErrorInContent('The page was not found')
|
||||||
|
@sidebarController?.hide()
|
||||||
|
|
||||||
|
renderNotAvailableAnymore: ->
|
||||||
|
@updateTitle(App.i18n.translateInline('Not Available'))
|
||||||
|
@renderScreenErrorInContent('The page is not available anymore')
|
||||||
|
|
||||||
|
renderError: ->
|
||||||
|
@bodyModal?.close()
|
||||||
|
|
||||||
|
url = App.Utils.joinUrlComponents @lastParams.effectivePath, @getKnowledgeBase().primaryKbLocale().urlSuffix()
|
||||||
|
|
||||||
|
@bodyModal = new App.ControllerModal(
|
||||||
|
head: 'Locale not found'
|
||||||
|
contentInline: "<a href='#{url}'>Open in primary locale</a>"
|
||||||
|
buttonClose: false
|
||||||
|
buttonSubmit: false
|
||||||
|
backdrop: 'static'
|
||||||
|
keyboard: false
|
||||||
|
container: @el
|
||||||
|
)
|
||||||
|
|
||||||
|
kb_locale: ->
|
||||||
|
kb = @getKnowledgeBase()
|
||||||
|
return if !kb
|
||||||
|
|
||||||
|
if @lastParams.selectedSystemLocale
|
||||||
|
kb.kb_locales().filter((elem) => elem.system_locale_id == @lastParams.selectedSystemLocale.id)[0]
|
||||||
|
|
||||||
|
getKnowledgeBase: ->
|
||||||
|
App.KnowledgeBase.find(@lastParams.knowledge_base_id)
|
||||||
|
|
||||||
|
fetchAndRender: =>
|
||||||
|
@fetch(true, true)
|
||||||
|
|
||||||
|
fetch: (showLoader, processLoaded) ->
|
||||||
|
if showLoader
|
||||||
|
@startLoading()
|
||||||
|
|
||||||
|
loaded_content_ids = App.KnowledgeBaseAnswerTranslationContent.all().map (elem) -> elem.id
|
||||||
|
|
||||||
|
params = {
|
||||||
|
answer_translation_content_ids: loaded_content_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
@loadInitial(
|
||||||
|
params,
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
if showLoader
|
||||||
|
@stopLoading()
|
||||||
|
|
||||||
|
if processLoaded
|
||||||
|
@processLoaded()
|
||||||
|
,
|
||||||
|
error: (xhr) =>
|
||||||
|
if showLoader
|
||||||
|
@stopLoading()
|
||||||
|
)
|
||||||
|
|
||||||
|
loadInitial: (params, options = {}) =>
|
||||||
|
@ajax(
|
||||||
|
id: 'knowledge_bases_init'
|
||||||
|
type: 'POST'
|
||||||
|
url: @apiPath + '/knowledge_bases/init'
|
||||||
|
data: JSON.stringify(params)
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@loaded = true
|
||||||
|
@loadKbData(data)
|
||||||
|
|
||||||
|
options.success?(data, status, xhr)
|
||||||
|
error: (xhr) ->
|
||||||
|
options.error?(xhr)
|
||||||
|
)
|
||||||
|
|
||||||
|
loadKbData: (data) ->
|
||||||
|
App.Collection.loadAssets(data)
|
||||||
|
|
||||||
|
for elem in @calculateIdsToDelete(data)
|
||||||
|
for id in elem.ids
|
||||||
|
App[elem.modelName].find(id)?.remove(clear: true)
|
||||||
|
|
||||||
|
calculateIdsToDelete: (data) ->
|
||||||
|
Object
|
||||||
|
.keys(data)
|
||||||
|
.filter (elem) -> elem.match(/^KnowledgeBase/)
|
||||||
|
.map (model) ->
|
||||||
|
newIds = Object.keys data[model]
|
||||||
|
oldIds = App[model].all().map (elem) -> elem.id
|
||||||
|
diff = oldIds.filter (elem) -> !newIds.includes(String(elem))
|
||||||
|
|
||||||
|
{modelName: model, ids: diff}
|
||||||
|
, {}
|
||||||
|
|
||||||
|
processLoaded: ->
|
||||||
|
@render(true)
|
||||||
|
|
||||||
|
if @pendingParams
|
||||||
|
@show(@pendingParams)
|
||||||
|
@pendingParams = undefined
|
||||||
|
|
||||||
|
render: (force = false) =>
|
||||||
|
@html App.view('knowledge_base/agent')()
|
||||||
|
|
||||||
|
@navigationController = new App.KnowledgeBaseNavigation(
|
||||||
|
el: @$('.js-navigation')
|
||||||
|
parentController: @
|
||||||
|
)
|
||||||
|
|
||||||
|
@sidebarController = new App.KnowledgeBaseSidebar(
|
||||||
|
el: @$('.js-sidebar')
|
||||||
|
parentController: @
|
||||||
|
)
|
||||||
|
|
||||||
|
isEditor: ->
|
||||||
|
App.User.current().permission('knowledge_base.editor')
|
||||||
|
|
||||||
|
checkForUpdates: ->
|
||||||
|
@interval(@checkUpdatesAction, 10 * 60 * 1000, 'kb_interval_check')
|
||||||
|
|
||||||
|
checkUpdatesAction: =>
|
||||||
|
if !@loaded
|
||||||
|
return
|
||||||
|
|
||||||
|
@fetch(false, false)
|
||||||
|
|
||||||
|
buildUsing: (klass, params, object) ->
|
||||||
|
new klass(
|
||||||
|
el: @$('.page-content')
|
||||||
|
object: object
|
||||||
|
parentController: @
|
||||||
|
selectedSystemLocale: params.selectedSystemLocale
|
||||||
|
url: params.match[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
onclick: ->
|
||||||
|
!(@permissionCheck('knowledge_base.*') and App.Config.get('kb_active')) and (App.Config.get('kb_active_publicly') and App.KnowledgeBase.first()?)
|
||||||
|
|
||||||
|
accessoryIcon: ->
|
||||||
|
return if !@onclick()
|
||||||
|
|
||||||
|
'external'
|
||||||
|
|
||||||
|
clicked: ->
|
||||||
|
window.open(App.KnowledgeBase.first().publicBaseUrl(), '_blank')
|
||||||
|
|
||||||
|
@pickObjectUsing: (params, parentController) ->
|
||||||
|
kb = parentController.getKnowledgeBase()
|
||||||
|
return if !kb
|
||||||
|
|
||||||
|
if answer_id = params['answer_id']
|
||||||
|
App.KnowledgeBaseAnswer.find(answer_id)
|
||||||
|
else if category_id = params['category_id']
|
||||||
|
App.KnowledgeBaseCategory.find(category_id)
|
||||||
|
else if knowledge_base_id = params['knowledge_base_id']
|
||||||
|
kb
|
||||||
|
|
||||||
|
App.Config.set('KnowledgeBase', { controller: 'KnowledgeBaseAgentController' }, 'permanentTask')
|
||||||
|
App.Config.set('KnowledgeBase', { prio: 1150, parent: '', name: 'Knowledge Base', target: '#knowledge_base', key: 'KnowledgeBase', class: 'knowledge-base', shown: false}, 'NavBar')
|
|
@ -0,0 +1,68 @@
|
||||||
|
class App.KnowledgeBaseContentCanBePublishedDialog extends App.ControllerModal
|
||||||
|
events:
|
||||||
|
'click .scheduled-widget-delete': 'clickedCancelTimer'
|
||||||
|
'submit form': 'submitTiming'
|
||||||
|
|
||||||
|
head: 'Visibility'
|
||||||
|
includeForm: false
|
||||||
|
buttonSubmit: false
|
||||||
|
|
||||||
|
constructor: (params) ->
|
||||||
|
super
|
||||||
|
|
||||||
|
content: =>
|
||||||
|
@formController = new App.KnowledgeBaseContentCanBePublishedForm(
|
||||||
|
object: @object
|
||||||
|
)
|
||||||
|
|
||||||
|
@formController.form
|
||||||
|
|
||||||
|
saveUpdate: (params, successCallback = null) =>
|
||||||
|
@clearAlerts()
|
||||||
|
@formController.toggleDisabled(true)
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'knowledge_base_can_be_published'
|
||||||
|
type: 'POST'
|
||||||
|
data: JSON.stringify(params)
|
||||||
|
url: @object.generateURL('has_publishing_update')
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.load(type: 'KnowledgeBaseAnswer', data: [data])
|
||||||
|
successCallback?()
|
||||||
|
@formController.toggleDisabled(false)
|
||||||
|
error: (xhr) =>
|
||||||
|
@formController.toggleDisabled(false)
|
||||||
|
@showAlert(xhr.responseJSON?.error || 'Unable to save changes')
|
||||||
|
)
|
||||||
|
|
||||||
|
clickedCancelTimer: (e) ->
|
||||||
|
widget = $(e.currentTarget).closest('.scheduled-widget')
|
||||||
|
state = widget.data('state')
|
||||||
|
params = { "#{state}_at": null }
|
||||||
|
|
||||||
|
@saveUpdate params, ->
|
||||||
|
widget.remove()
|
||||||
|
|
||||||
|
submitTiming: (e) =>
|
||||||
|
@preventDefaultAndStopPropagation(e)
|
||||||
|
|
||||||
|
data = @formParams()
|
||||||
|
|
||||||
|
params =
|
||||||
|
"#{data.visibility}_at": if data.timing is 'scheduled' then data.scheduled else '--now--'
|
||||||
|
|
||||||
|
newVisibilityIndex = @formController.states.indexOf(data.visibility)
|
||||||
|
oldVisibilityIndex = @formController.states.indexOf(@formController.params.visibility)
|
||||||
|
|
||||||
|
if newVisibilityIndex < oldVisibilityIndex
|
||||||
|
for index in [(newVisibilityIndex+1)..oldVisibilityIndex]
|
||||||
|
params["#{@formController.states[index]}_at"] = null
|
||||||
|
|
||||||
|
@saveUpdate params, =>
|
||||||
|
if data.timing is 'now'
|
||||||
|
@close()
|
||||||
|
return
|
||||||
|
|
||||||
|
@update()
|
||||||
|
@initalFormParams = @formParams()
|
|
@ -0,0 +1,133 @@
|
||||||
|
class App.KnowledgeBaseContentCanBePublishedForm extends App.ControllerForm
|
||||||
|
elements:
|
||||||
|
'.js-datepicker': 'datePicker'
|
||||||
|
'[name=visibility]': 'visibilityRadios'
|
||||||
|
'[value=now]': 'timingNow'
|
||||||
|
'[value=scheduled]': 'timingScheduled'
|
||||||
|
|
||||||
|
constructor: (params) ->
|
||||||
|
@prepare(params)
|
||||||
|
super
|
||||||
|
@postRendering()
|
||||||
|
@visibilityRadios.trigger('change')
|
||||||
|
|
||||||
|
prepare: (params) ->
|
||||||
|
@handlers = [@timingHandler, @visibilityHandler, @scheduledHandler]
|
||||||
|
|
||||||
|
@params =
|
||||||
|
visibility: params.object.can_be_published_state()
|
||||||
|
|
||||||
|
scheduledHandler: (params, attribute, attributes, classname, form, ui) =>
|
||||||
|
if attribute.name isnt 'scheduled'
|
||||||
|
return
|
||||||
|
|
||||||
|
if !params.scheduled
|
||||||
|
return
|
||||||
|
|
||||||
|
@timingScheduled.prop('checked', true)
|
||||||
|
|
||||||
|
visibilityHandler: (params, attribute, attributes, classname, form, ui) =>
|
||||||
|
if attribute.name isnt 'visibility'
|
||||||
|
return
|
||||||
|
|
||||||
|
@toggleDisabled(false)
|
||||||
|
|
||||||
|
scheduledWidget = @form.find(".scheduled-widget[data-state=#{params.visibility}]")
|
||||||
|
|
||||||
|
if scheduledWidget.length > 0 and !@form.find('.controls--datetime input[data-item=date]').val()
|
||||||
|
date = scheduledWidget.data('date')
|
||||||
|
@datePicker.datepicker('setDate', date)
|
||||||
|
else
|
||||||
|
@datePicker.datepicker('clearDates')
|
||||||
|
@timingNow.prop('checked', true)
|
||||||
|
|
||||||
|
timingHandler: (params, attribute, attributes, classname, form, ui) =>
|
||||||
|
if attribute.name isnt 'timing'
|
||||||
|
return
|
||||||
|
|
||||||
|
if params.timing isnt 'now'
|
||||||
|
return
|
||||||
|
|
||||||
|
if !params.scheduled
|
||||||
|
return
|
||||||
|
|
||||||
|
@datePicker.datepicker('clearDates')
|
||||||
|
|
||||||
|
postRendering: =>
|
||||||
|
# simulate elements
|
||||||
|
for key, value of @elements
|
||||||
|
@[value] = @form.find(key)
|
||||||
|
|
||||||
|
# move date picker to inside of timing radio
|
||||||
|
@timingScheduled.parent().addClass('additional-radio-controls').append(@form.find('[data-name="scheduled"]'))
|
||||||
|
@form.find('[data-attribute-name="scheduled"]').remove()
|
||||||
|
@datePicker.datepicker('setStartDate', new Date())
|
||||||
|
|
||||||
|
# add scheduled tiemr widgets
|
||||||
|
now = new Date()
|
||||||
|
|
||||||
|
for state in @states
|
||||||
|
if @object["#{state}_at"] && new Date(@object["#{state}_at"]) > now
|
||||||
|
label = @form.find("input[value=#{state}]").closest('label')
|
||||||
|
timer = new App.KnowledgeBaseScheduledWidget(object: @object, state: state)
|
||||||
|
label.after timer.el
|
||||||
|
|
||||||
|
toggleDisabled: (state) ->
|
||||||
|
selectedState = @visibilityRadios.filter(':checked').val()
|
||||||
|
timingDisabled = @params.visibility is selectedState
|
||||||
|
isRollback = @states.indexOf(@params.visibility) > @states.indexOf(selectedState)
|
||||||
|
|
||||||
|
@form.find('[value=now], [type=submit]')
|
||||||
|
.attr('disabled', state or timingDisabled)
|
||||||
|
|
||||||
|
@form.find('[value=scheduled], .controls--datetime input')
|
||||||
|
.attr('disabled', state or timingDisabled or isRollback)
|
||||||
|
|
||||||
|
@visibilityRadios.attr('disabled', state)
|
||||||
|
|
||||||
|
fullForm: true
|
||||||
|
fullFormSubmitLabel: 'Update'
|
||||||
|
fullFormSubmitAdditionalClasses: 'btn--primary'
|
||||||
|
states: ['draft', 'internal', 'published', 'archived']
|
||||||
|
|
||||||
|
model:
|
||||||
|
configure_attributes: [
|
||||||
|
name: 'visibility'
|
||||||
|
display: 'Visibility'
|
||||||
|
tag: 'radio'
|
||||||
|
default: false
|
||||||
|
options: [
|
||||||
|
value: 'draft'
|
||||||
|
name: 'Draft'
|
||||||
|
note: 'Only visible to editors'
|
||||||
|
,
|
||||||
|
value: 'internal'
|
||||||
|
name: 'Internal'
|
||||||
|
note: 'Only visible to agents & editors'
|
||||||
|
,
|
||||||
|
value: 'published'
|
||||||
|
name: 'Public'
|
||||||
|
note: 'Visible to everyone'
|
||||||
|
,
|
||||||
|
value: 'archived'
|
||||||
|
name: 'Archived'
|
||||||
|
]
|
||||||
|
,
|
||||||
|
name: 'timing'
|
||||||
|
display: 'Timing'
|
||||||
|
tag: 'radio'
|
||||||
|
default: 'now'
|
||||||
|
options: [
|
||||||
|
value: 'now'
|
||||||
|
name: 'Now'
|
||||||
|
,
|
||||||
|
value: 'scheduled'
|
||||||
|
name: 'Schedule for'
|
||||||
|
]
|
||||||
|
,
|
||||||
|
name: 'scheduled'
|
||||||
|
display: 'Date'
|
||||||
|
tag: 'datetime'
|
||||||
|
class: 'form-control--small'
|
||||||
|
null: true
|
||||||
|
]
|
|
@ -0,0 +1,161 @@
|
||||||
|
class App.KnowledgeBaseContentController extends App.Controller
|
||||||
|
elements:
|
||||||
|
'.js-form': 'form'
|
||||||
|
'.js-discard': 'discardButton'
|
||||||
|
'.js-submitContainer': 'submitContainer'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .js-submit': 'submit'
|
||||||
|
'click .js-discard': 'discardChanges'
|
||||||
|
'submit .js-form': 'submit'
|
||||||
|
'input .js-form': 'showDiscardButton'
|
||||||
|
'click .js-submit-action': 'submit'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
translation = @object.translation(@parentController.kb_locale().id)
|
||||||
|
|
||||||
|
if translation and !translation.fullyLoaded()
|
||||||
|
@html App.view('knowledge_base/content')(@)
|
||||||
|
@startLoading()
|
||||||
|
|
||||||
|
translation.loadFull (isSuccess) =>
|
||||||
|
@stopLoading()
|
||||||
|
|
||||||
|
if !isSuccess
|
||||||
|
return
|
||||||
|
|
||||||
|
@initialize()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
@initialize()
|
||||||
|
|
||||||
|
initialize: ->
|
||||||
|
@render()
|
||||||
|
|
||||||
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
|
@objectRefreshed()
|
||||||
|
true
|
||||||
|
|
||||||
|
# update availability display whenever object is touched
|
||||||
|
@listenTo @object, 'refresh', =>
|
||||||
|
@renderAvailabilityWidgets()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('knowledge_base/content')(@)
|
||||||
|
@renderAvailabilityWidgets()
|
||||||
|
|
||||||
|
@formController = @buildFormController(@form)
|
||||||
|
@startingParams = App.ControllerForm.params(@formController.el)
|
||||||
|
|
||||||
|
buildFormController: (dom = undefined) ->
|
||||||
|
new App.KnowledgeBaseFormController(@object, @parentController.kb_locale(), 'agent_edit', dom)
|
||||||
|
|
||||||
|
remoteDidntChangeSinceStart: ->
|
||||||
|
remoteParams = @buildFormController().rawParams()
|
||||||
|
App.KnowledgeBaseFormController.compareParams(remoteParams, @startingParams)
|
||||||
|
|
||||||
|
objectRefreshed: ->
|
||||||
|
@renderAvailabilityWidgets()
|
||||||
|
|
||||||
|
if @remoteDidntChangeSinceStart()
|
||||||
|
@pendingRerender = false
|
||||||
|
return
|
||||||
|
|
||||||
|
if !@parentController.shown
|
||||||
|
@pendingRerender = true
|
||||||
|
return
|
||||||
|
|
||||||
|
@rerenderIfConfirmed()
|
||||||
|
|
||||||
|
rerenderIfConfirmed: ->
|
||||||
|
text = App.i18n.translatePlain('Changes were made. Do you want to reload? You\'ll loose your changes')
|
||||||
|
if confirm(text)
|
||||||
|
@render()
|
||||||
|
|
||||||
|
renderAvailabilityWidgets: ->
|
||||||
|
if !@object.constructor.canBePublished?()
|
||||||
|
return
|
||||||
|
|
||||||
|
new App.WidgetButtonWithDropdown(
|
||||||
|
el: @submitContainer
|
||||||
|
mainActionLabel: 'Update'
|
||||||
|
actions: @quickActions()
|
||||||
|
)
|
||||||
|
|
||||||
|
html = App.view('knowledge_base/content_can_be_published_header_suffix')(object: @object)
|
||||||
|
@el.find('.js-published-header-suffix').replaceWith(html)
|
||||||
|
|
||||||
|
submit: (e) ->
|
||||||
|
@preventDefaultAndStopPropagation(e)
|
||||||
|
|
||||||
|
if !@formController.validateAndShowErrors()
|
||||||
|
return
|
||||||
|
|
||||||
|
paramsForSaving = @formController.paramsForSaving()
|
||||||
|
|
||||||
|
additional_action = $(e.currentTarget).data('id')
|
||||||
|
|
||||||
|
if @remoteDidntChangeSinceStart()
|
||||||
|
@parentController.coordinator.saveChanges(@object, paramsForSaving, @, additional_action)
|
||||||
|
return
|
||||||
|
|
||||||
|
new App.ControllerConfirm(
|
||||||
|
head: 'Content was changed since loading'
|
||||||
|
message: 'Your changes may override someone else\'s changes. Are you sure to save?'
|
||||||
|
callback: =>
|
||||||
|
@parentController.coordinator.saveChanges(@object, paramsForSaving, @)
|
||||||
|
)
|
||||||
|
|
||||||
|
missingTranslation: ->
|
||||||
|
@object.translation(@parentController.kb_locale().id) is undefined && !@object.isNew()
|
||||||
|
|
||||||
|
showDiscardButton: ->
|
||||||
|
@delay =>
|
||||||
|
noChanges = App.KnowledgeBaseFormController.compareParams(@formController.rawParams(), @startingParams)
|
||||||
|
@discardButton.toggleClass('hide', noChanges)
|
||||||
|
, 500, 'check_unsaved_changes'
|
||||||
|
|
||||||
|
quickActions: ->
|
||||||
|
prefix = App.i18n.translatePlain('Update') + ' & '
|
||||||
|
actions = @object.can_be_published_quick_actions()
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'internal'
|
||||||
|
name: prefix + App.i18n.translatePlain('Internal')
|
||||||
|
disabled: !_.includes(actions, 'internal')
|
||||||
|
},{
|
||||||
|
id: 'publish'
|
||||||
|
name: prefix + App.i18n.translatePlain('Publish')
|
||||||
|
disabled: !_.includes(actions, 'publish')
|
||||||
|
},{
|
||||||
|
id: 'archive'
|
||||||
|
name: prefix + App.i18n.translatePlain('Archive')
|
||||||
|
disabled: !_.includes(actions, 'archive')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
discardChanges: ->
|
||||||
|
@render()
|
||||||
|
|
||||||
|
showAlert: (text) ->
|
||||||
|
@formController?.showAlert(text)
|
||||||
|
|
||||||
|
didSaveCallback: (data) ->
|
||||||
|
@render()
|
||||||
|
|
||||||
|
App.Event.trigger 'knowledge_base::sidebar::rerender'
|
||||||
|
App.Event.trigger 'knowledge_base::navigation::rerender'
|
||||||
|
|
||||||
|
# this method is called when user comes back to already instantiated view
|
||||||
|
restoreVisibility: ->
|
||||||
|
if !@pendingRerender
|
||||||
|
return
|
||||||
|
|
||||||
|
@pendingRerender = false
|
||||||
|
|
||||||
|
# add delay to give it time to rerender before showing prompt
|
||||||
|
App.Delay.set => @rerenderIfConfirmed()
|
|
@ -0,0 +1,71 @@
|
||||||
|
class App.KnowledgeBaseDeleteAction
|
||||||
|
constructor: (params) ->
|
||||||
|
for key, value of params
|
||||||
|
@[key] = value
|
||||||
|
|
||||||
|
if @object instanceof App.KnowledgeBaseCategory and !@object.isEmpty()
|
||||||
|
@showCannotDelete(
|
||||||
|
'Cannot delete category',
|
||||||
|
'Please delete all children categories and answers first.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
@showConfirm()
|
||||||
|
|
||||||
|
showConfirm: ->
|
||||||
|
kb_locale = @parentController.kb_locale()
|
||||||
|
translation = @object.guaranteedTranslation(kb_locale.id)
|
||||||
|
|
||||||
|
@dialog = new App.ControllerConfirm(
|
||||||
|
head: 'Delete'
|
||||||
|
message: "Are you sure to delete \"#{translation?.title}\"?"
|
||||||
|
callback: @doDelete
|
||||||
|
container: @parentController.el
|
||||||
|
onSubmit: ->
|
||||||
|
@formDisable(@el)
|
||||||
|
@callback(@)
|
||||||
|
@dialog = null
|
||||||
|
)
|
||||||
|
|
||||||
|
showCannotDelete: (title, message) ->
|
||||||
|
modal = new App.ControllerModal(
|
||||||
|
head: title
|
||||||
|
contentInline: message
|
||||||
|
container: @parentController.el
|
||||||
|
buttonClose: true
|
||||||
|
buttonSubmit: 'Ok'
|
||||||
|
onSubmit: (e) =>
|
||||||
|
modal.close()
|
||||||
|
@dialog = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@dialog = modal
|
||||||
|
|
||||||
|
doDelete: (modal) =>
|
||||||
|
App.Ajax.request(
|
||||||
|
type: 'DELETE'
|
||||||
|
url: @object.generateURL() + '?full=true'
|
||||||
|
success: =>
|
||||||
|
@deleteOk(modal)
|
||||||
|
error: (xhr) =>
|
||||||
|
@deleteFailure(modal, xhr)
|
||||||
|
)
|
||||||
|
|
||||||
|
deleteOk: (modal) =>
|
||||||
|
futureObject = @object.parent?() || @object.category?() || @object.knowledge_base()
|
||||||
|
|
||||||
|
@parentController.contentController.stopListening()
|
||||||
|
@object.removeIncludingTranslations(clear: true)
|
||||||
|
|
||||||
|
modal.close()
|
||||||
|
|
||||||
|
@parentController.navigate futureObject.uiUrl(@parentController.kb_locale(), 'edit')
|
||||||
|
|
||||||
|
deleteFailure: (modal, xhr) ->
|
||||||
|
modal.formEnable(modal.el)
|
||||||
|
modal.showAlert xhr.responseJSON?.error || 'Unable to delete.'
|
||||||
|
|
||||||
|
# simulate modal's close function
|
||||||
|
close: ->
|
||||||
|
@dialog?.close()
|
|
@ -0,0 +1,44 @@
|
||||||
|
class App.KnowledgeBaseEditorCoordinator
|
||||||
|
constructor: (params) ->
|
||||||
|
for key, value of params
|
||||||
|
@[key] = value
|
||||||
|
|
||||||
|
clickedCanBePublished: (object) ->
|
||||||
|
new App.KnowledgeBaseContentCanBePublishedDialog(
|
||||||
|
object: object
|
||||||
|
container: @parentController.el
|
||||||
|
)
|
||||||
|
|
||||||
|
clickedDelete: (object) ->
|
||||||
|
new App.KnowledgeBaseDeleteAction(
|
||||||
|
object: object
|
||||||
|
parentController: @parentController
|
||||||
|
)
|
||||||
|
|
||||||
|
# built-in Spine's function doesn't work when object has no ID set and includes "undefined" in URL
|
||||||
|
urlFor: (object) ->
|
||||||
|
if object.id
|
||||||
|
object.generateURL()
|
||||||
|
else
|
||||||
|
object.url()
|
||||||
|
|
||||||
|
saveChanges: (object, data, formController, action) ->
|
||||||
|
App.ControllerForm.disable(formController.form)
|
||||||
|
|
||||||
|
url = @urlFor(object) + '?full=true'
|
||||||
|
|
||||||
|
if action
|
||||||
|
url += "&additional_action=#{action}"
|
||||||
|
|
||||||
|
App.Ajax.request(
|
||||||
|
type: object.writeMethod()
|
||||||
|
data: JSON.stringify(data)
|
||||||
|
url: url
|
||||||
|
success: (data) ->
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
formController.didSaveCallback(data)
|
||||||
|
error: (xhr) ->
|
||||||
|
data = JSON.parse(xhr.responseText)
|
||||||
|
App.ControllerForm.enable(formController.form)
|
||||||
|
formController.showAlert(data.error || 'Unable to save changes.')
|
||||||
|
)
|
|
@ -0,0 +1,69 @@
|
||||||
|
class App.KnowledgeBaseFormController extends App.ControllerForm
|
||||||
|
# set screen to agent_edit or agent_create
|
||||||
|
constructor: (object, kb_locale, screen, dom) ->
|
||||||
|
@object = object
|
||||||
|
@kb_locale = kb_locale
|
||||||
|
|
||||||
|
objectParams = @currentParams()
|
||||||
|
objectParams['form_id'] = App.ControllerForm.formId()
|
||||||
|
|
||||||
|
super(
|
||||||
|
params: objectParams
|
||||||
|
autofocus: dom isnt null
|
||||||
|
grid: true
|
||||||
|
el: dom || $('<form>')
|
||||||
|
screen: screen
|
||||||
|
model: { configure_attributes: @getAttrs() }
|
||||||
|
)
|
||||||
|
|
||||||
|
getAjaxAttributes: (field, attributes) ->
|
||||||
|
@apiPath = App.Config.get('api_path')
|
||||||
|
|
||||||
|
attributes.type = 'POST'
|
||||||
|
attributes.url = "#{@apiPath}/knowledge_bases/search"
|
||||||
|
|
||||||
|
attributes.data.flavor = 'agent'
|
||||||
|
attributes.data.knowledge_base_id = @object.knowledge_base().id
|
||||||
|
attributes.data.exclude_ids = [@object.translation(@kb_locale.id)?.id]
|
||||||
|
attributes.data.index = 'KnowledgeBase::Answer::Translation'
|
||||||
|
attributes.data.locale = @kb_locale.systemLocale().locale
|
||||||
|
attributes.data.highlight_enabled = false
|
||||||
|
|
||||||
|
attributes.data = JSON.stringify(attributes.data)
|
||||||
|
|
||||||
|
attributes
|
||||||
|
|
||||||
|
currentParams: ->
|
||||||
|
@object.attributesIncludingTranslation(@kb_locale.id)
|
||||||
|
|
||||||
|
rawParams: ->
|
||||||
|
App.ControllerForm.params(@el)
|
||||||
|
|
||||||
|
paramsForSaving: ->
|
||||||
|
@object.prepareNestedParams(@rawParams(), @kb_locale.id)
|
||||||
|
|
||||||
|
validateAndShowErrors: ->
|
||||||
|
errors = @validate(@rawParams())
|
||||||
|
|
||||||
|
@constructor.validate(
|
||||||
|
errors: errors
|
||||||
|
form: @.el
|
||||||
|
)
|
||||||
|
|
||||||
|
!errors
|
||||||
|
|
||||||
|
getAttrs: ->
|
||||||
|
attrs = @object.configure_attributes?(@kb_locale) || @object.constructor.configure_attributes
|
||||||
|
|
||||||
|
attrs.push {
|
||||||
|
name: 'form_id'
|
||||||
|
tag: 'input'
|
||||||
|
type: 'hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs
|
||||||
|
|
||||||
|
@compareParams: (a, b) ->
|
||||||
|
for params in [a, b]
|
||||||
|
delete params.form_id
|
||||||
|
_.isEqual(a, b)
|
|
@ -0,0 +1,97 @@
|
||||||
|
class App.KnowledgeBaseReaderPagination extends App.Controller
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
className: 'knowledge-base-article-nav'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@stopListening()
|
||||||
|
|
||||||
|
previousAnswer = @calculatePreviousAnswer()
|
||||||
|
nextAnswer = @calculateNextAnswer()
|
||||||
|
|
||||||
|
@html App.view('knowledge_base/_reader_pagination')(
|
||||||
|
previousAnswer: previousAnswer?.attributesForRendering(@kb_locale)
|
||||||
|
nextAnswer: nextAnswer?.attributesForRendering(@kb_locale)
|
||||||
|
)
|
||||||
|
|
||||||
|
for object in [@object, previousAnswer, nextAnswer, @object.category()]
|
||||||
|
if object
|
||||||
|
@listenTo object, 'refresh', (e) =>
|
||||||
|
@render()
|
||||||
|
|
||||||
|
calculatePreviousAnswer: ->
|
||||||
|
@calculateSiblingAnswer(-1)
|
||||||
|
|
||||||
|
calculateNextAnswer: ->
|
||||||
|
@calculateSiblingAnswer(+1)
|
||||||
|
|
||||||
|
calculateSiblingAnswer: (direction) ->
|
||||||
|
if sibling = @calculateSibling(@object.category().answers(), @object, direction)
|
||||||
|
return sibling
|
||||||
|
|
||||||
|
if direction < 0 and cat_answer = @findlastAnswer(@object.category())
|
||||||
|
return cat_answer
|
||||||
|
|
||||||
|
scope = @object
|
||||||
|
|
||||||
|
while scope
|
||||||
|
parent = scope.category?() || scope.parent?()
|
||||||
|
|
||||||
|
list = if parent
|
||||||
|
parent.children()
|
||||||
|
else
|
||||||
|
scope.knowledge_base().rootCategories()
|
||||||
|
|
||||||
|
if siblingAtScope = @findAnswerInSiblingCategory(scope, list, direction)
|
||||||
|
return siblingAtScope
|
||||||
|
|
||||||
|
scope = parent
|
||||||
|
|
||||||
|
null
|
||||||
|
|
||||||
|
calculateSibling: (list, current, direction) ->
|
||||||
|
list[@getIndexOf(list, current) + direction]
|
||||||
|
|
||||||
|
getIndexOf: (list, current) ->
|
||||||
|
matching = list.filter((elem) -> elem.id == current.id)[0]
|
||||||
|
list.indexOf(matching)
|
||||||
|
|
||||||
|
findlastAnswer: (category, include_direct_answers = false) ->
|
||||||
|
if include_direct_answers and last_direct = category.answers().slice(-1)[0]
|
||||||
|
return last_direct
|
||||||
|
|
||||||
|
for category in category.children().reverse()
|
||||||
|
if answer = @findlastAnswer(category, true)
|
||||||
|
return answer
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
findFirstAnswer: (category) ->
|
||||||
|
for category in category.children()
|
||||||
|
if answer = @findFirstAnswer(category)
|
||||||
|
return answer
|
||||||
|
|
||||||
|
category.answers()[0]
|
||||||
|
|
||||||
|
findAnswerInSiblingCategory: (category, list, direction) ->
|
||||||
|
currentCategoryIndex = @getIndexOf(list, category)
|
||||||
|
|
||||||
|
categories = if direction < 0
|
||||||
|
list.slice(0, currentCategoryIndex).reverse()
|
||||||
|
else
|
||||||
|
list.slice(currentCategoryIndex + 1)
|
||||||
|
|
||||||
|
for category in categories
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
found = if direction < 0
|
||||||
|
@findlastAnswer(category, true)
|
||||||
|
else
|
||||||
|
@findFirstAnswer(category)
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
if found
|
||||||
|
return found
|
||||||
|
|
||||||
|
null
|
|
@ -0,0 +1,173 @@
|
||||||
|
class App.KnowledgeBaseNavigation extends App.Controller
|
||||||
|
@extend(Spine.Events)
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .js-search': 'clickedToggleSearch'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-edit': 'editButton'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
@bind 'knowledge_base::navigation::rerender', => @needsUpdate()
|
||||||
|
|
||||||
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
|
@needsUpdate()
|
||||||
|
|
||||||
|
buildCrumbsForRendering: (array, kb_locale, action) ->
|
||||||
|
if action is 'search'
|
||||||
|
action = null
|
||||||
|
|
||||||
|
if !kb_locale
|
||||||
|
return []
|
||||||
|
|
||||||
|
array
|
||||||
|
.filter (elem) ->
|
||||||
|
elem != undefined and elem != null
|
||||||
|
.map (elem) =>
|
||||||
|
if typeof elem is 'string'
|
||||||
|
return { title: elem }
|
||||||
|
|
||||||
|
@listenToChangesOn(elem)
|
||||||
|
elem.attributesForRendering(kb_locale, action: action)
|
||||||
|
|
||||||
|
listenToChangesOn: (object) ->
|
||||||
|
locale = @parentController.kb_locale()
|
||||||
|
|
||||||
|
if !locale
|
||||||
|
return
|
||||||
|
|
||||||
|
@stopListening object, 'refresh'
|
||||||
|
@listenToOnce object.translationBindlableObject(locale.id), 'refresh', (obj) =>
|
||||||
|
@needsUpdate()
|
||||||
|
|
||||||
|
show: (object, action) ->
|
||||||
|
@savedAction = action
|
||||||
|
|
||||||
|
if @dontRenderFor(object)
|
||||||
|
return
|
||||||
|
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
crumbs = if title = @calculateTitle(object, action)
|
||||||
|
[@parentController.getKnowledgeBase(), title]
|
||||||
|
else
|
||||||
|
@breadcrumbTo(object).reverse()
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
crumbsForRendering = @buildCrumbsForRendering(crumbs, @parentController.kb_locale(), action)
|
||||||
|
|
||||||
|
@render(crumbsForRendering, object, action)
|
||||||
|
@savedParams = object
|
||||||
|
|
||||||
|
calculateTitle: (object, action) ->
|
||||||
|
if action is 'search'
|
||||||
|
App.i18n.translateInline 'Search'
|
||||||
|
else if !object
|
||||||
|
App.i18n.translateInline 'Not found'
|
||||||
|
|
||||||
|
dontRenderFor: (object) ->
|
||||||
|
if object instanceof App.Model
|
||||||
|
object.isNew() && !object.isFresh
|
||||||
|
else
|
||||||
|
false
|
||||||
|
|
||||||
|
needsUpdate: ->
|
||||||
|
@show(@savedParams, @savedAction)
|
||||||
|
|
||||||
|
selectedLocaleDisplay: ->
|
||||||
|
@parentController.kb_locale()?.systemLocale().alias || '-'
|
||||||
|
|
||||||
|
render: (crumbs = [], object = null, action = null) ->
|
||||||
|
kb_locale = @parentController.kb_locale()
|
||||||
|
return if !kb_locale
|
||||||
|
|
||||||
|
@html App.view('knowledge_base/navigation')(
|
||||||
|
crumbs: crumbs
|
||||||
|
kbLocales: @kbLocaleOptions(object, kb_locale, action)
|
||||||
|
search: @searchOptions(object, kb_locale, action)
|
||||||
|
edit: @editOptions(object, kb_locale, action)
|
||||||
|
externalUrl: @externalUrl(object, kb_locale, action)
|
||||||
|
iconset: @parentController.getKnowledgeBase().iconset
|
||||||
|
)
|
||||||
|
|
||||||
|
kbLocaleOptions: (object, kb_locale, action) ->
|
||||||
|
{
|
||||||
|
selected: kb_locale
|
||||||
|
collection: @kb_locales()
|
||||||
|
}
|
||||||
|
|
||||||
|
searchOptions: (object, kb_locale, action) ->
|
||||||
|
enabled = action is 'search'
|
||||||
|
|
||||||
|
url = if enabled == true
|
||||||
|
@toggleSearchSource || @parentController.getKnowledgeBase()?.uiUrl(kb_locale)
|
||||||
|
else
|
||||||
|
@parentController.getKnowledgeBase()?.uiUrl(kb_locale, 'search')
|
||||||
|
|
||||||
|
{
|
||||||
|
enabled: enabled
|
||||||
|
url: url
|
||||||
|
}
|
||||||
|
|
||||||
|
editOptions: (object, kb_locale, action) ->
|
||||||
|
enabled = action is 'edit'
|
||||||
|
|
||||||
|
{
|
||||||
|
url: object?.uiUrl(kb_locale, if !enabled then 'edit')
|
||||||
|
enabled: enabled
|
||||||
|
available: @parentController.isEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
externalUrl: (object, kb_locale, action) ->
|
||||||
|
if action and action != 'edit'
|
||||||
|
return
|
||||||
|
|
||||||
|
if !(object?.visiblePublicly?(kb_locale) or (object?.translation?(kb_locale?.id)? and @parentController.isEditor()))
|
||||||
|
return
|
||||||
|
|
||||||
|
object.publicBaseUrl(kb_locale)
|
||||||
|
|
||||||
|
kb_locales: ->
|
||||||
|
path = '#' + @parentController.lastParams.match.input
|
||||||
|
|
||||||
|
@parentController
|
||||||
|
.getKnowledgeBase()
|
||||||
|
.kb_locales()
|
||||||
|
.map (elem) -> elem.attributesForRendering(path)
|
||||||
|
|
||||||
|
toggleSearchSource: undefined
|
||||||
|
|
||||||
|
clickedToggleSearch: ->
|
||||||
|
if @savedAction is 'search'
|
||||||
|
return
|
||||||
|
|
||||||
|
@toggleSearchSource = location.hash
|
||||||
|
|
||||||
|
breadcrumbTo: (object) ->
|
||||||
|
if !object
|
||||||
|
return []
|
||||||
|
|
||||||
|
output = switch object.constructor
|
||||||
|
when App.KnowledgeBaseAnswer
|
||||||
|
@breadcrumbToAnswer(object)
|
||||||
|
when App.KnowledgeBaseCategory
|
||||||
|
@breadcrumbToCategory(object)
|
||||||
|
when App.KnowledgeBase
|
||||||
|
@breadcrumbToKb(object)
|
||||||
|
|
||||||
|
breadcrumbToAnswer: (answer) ->
|
||||||
|
[answer].concat @breadcrumbToCategory(answer.category())
|
||||||
|
|
||||||
|
breadcrumbToCategory: (category) ->
|
||||||
|
array = [category]
|
||||||
|
|
||||||
|
while parent = (parent || category).parent()
|
||||||
|
array = array.concat parent
|
||||||
|
|
||||||
|
array.concat @breadcrumbToKb(category.knowledge_base())
|
||||||
|
|
||||||
|
breadcrumbToKb: (kb) ->
|
||||||
|
[kb]
|
|
@ -0,0 +1,17 @@
|
||||||
|
class App.KnowledgeBasePublicMenuForm extends App.Controller
|
||||||
|
events:
|
||||||
|
'show.bs.tab': 'willShow'
|
||||||
|
|
||||||
|
willShow: ->
|
||||||
|
@el.empty()
|
||||||
|
|
||||||
|
for kb_locale in App.KnowledgeBase.find(@knowledge_base_id).kb_locales()
|
||||||
|
menu_items = App.KnowledgeBaseMenuItem.using_kb_locale(kb_locale)
|
||||||
|
|
||||||
|
form_item = new App.KnowledgeBasePublicMenuFormItem(
|
||||||
|
knowledge_base_id: @knowledge_base_id,
|
||||||
|
kb_locale: kb_locale,
|
||||||
|
menu_items: menu_items
|
||||||
|
)
|
||||||
|
|
||||||
|
@el.append form_item.el
|
|
@ -0,0 +1,144 @@
|
||||||
|
class App.KnowledgeBasePublicMenuFormItem extends App.Controller
|
||||||
|
events:
|
||||||
|
'click .js-add': 'add'
|
||||||
|
'click .js-remove': 'remove'
|
||||||
|
'input input': 'input'
|
||||||
|
'submit form': 'submit'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-alert': 'alert'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('knowledge_base/public_menu_form_item')(
|
||||||
|
kb_locale_id: @kb_locale.id
|
||||||
|
rows: @menu_items
|
||||||
|
title: @kb_locale.systemLocale().name
|
||||||
|
)
|
||||||
|
|
||||||
|
@applySortable()
|
||||||
|
|
||||||
|
applySortable: ->
|
||||||
|
dndOptions =
|
||||||
|
tolerance: 'pointer'
|
||||||
|
distance: 15
|
||||||
|
opacity: 0.6
|
||||||
|
items: 'tr.sortable'
|
||||||
|
start: (e, ui) ->
|
||||||
|
ui.placeholder.height( ui.item.height() )
|
||||||
|
helper: (e, tr) ->
|
||||||
|
originals = tr.children()
|
||||||
|
helper = tr
|
||||||
|
helper.children().each (index, el) ->
|
||||||
|
# Set helper cell sizes to match the original sizes
|
||||||
|
$(@).width( originals.eq(index).width() )
|
||||||
|
return helper
|
||||||
|
update: @dndCallback
|
||||||
|
stop: (e, ui) ->
|
||||||
|
ui.item.children().each (index, element) ->
|
||||||
|
element.style.width = ''
|
||||||
|
|
||||||
|
@el.find('tbody').sortable(dndOptions)
|
||||||
|
|
||||||
|
toggleUserInteraction: (enabled) ->
|
||||||
|
if enabled
|
||||||
|
App.ControllerForm.enable(@el)
|
||||||
|
else
|
||||||
|
App.ControllerForm.disable(@el)
|
||||||
|
|
||||||
|
@$('.js-remove, .js-add').attr('disabled', !enabled)
|
||||||
|
@el.find('tbody').sortable(disabled: !enabled)
|
||||||
|
|
||||||
|
buildData: ->
|
||||||
|
items = @$('tr.sortable')
|
||||||
|
.toArray()
|
||||||
|
.map (elem) -> $(elem)
|
||||||
|
.map (elem) ->
|
||||||
|
{
|
||||||
|
id: elem.data('id')
|
||||||
|
title: elem.find('input[data-name=title]').val()
|
||||||
|
url: elem.find('input[data-name=url]').val()
|
||||||
|
new_tab: elem.find('input[data-name=new_tab]').prop('checked')
|
||||||
|
_destroy: elem.hasClass('js-deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
kb_locale_id: @$('form').data('kb-locale-id'),
|
||||||
|
menu_items: items
|
||||||
|
}
|
||||||
|
|
||||||
|
input: ->
|
||||||
|
if @validateForm(false)
|
||||||
|
@hideAlert()
|
||||||
|
|
||||||
|
add: ->
|
||||||
|
el = App.view('knowledge_base/public_menu_form_item_row')()
|
||||||
|
$(el).insertBefore(@$('tr:has(.js-add)'))
|
||||||
|
|
||||||
|
remove: (e) ->
|
||||||
|
row = $(e.currentTarget).closest('tr')
|
||||||
|
|
||||||
|
if row.data('id')
|
||||||
|
row.toggleClass('settings-list--deleted js-deleted')
|
||||||
|
row.find('.js-remove input').prop('checked', row.hasClass('settings-list--deleted'))
|
||||||
|
row.find('.js-new-tab input').attr('disabled', row.hasClass('js-deleted'))
|
||||||
|
else
|
||||||
|
row.remove()
|
||||||
|
|
||||||
|
showAlert: (message) ->
|
||||||
|
translated = App.i18n.translatePlain(message)
|
||||||
|
|
||||||
|
@alert
|
||||||
|
.text(translated)
|
||||||
|
.removeClass('hidden')
|
||||||
|
|
||||||
|
hideAlert: ->
|
||||||
|
@alert.addClass('hidden')
|
||||||
|
|
||||||
|
emptyFields: ->
|
||||||
|
@$('tr.sortable:not(.js-deleted)')
|
||||||
|
.find('input[data-name]')
|
||||||
|
.toArray()
|
||||||
|
.filter (elem) -> $(elem).val().length == 0
|
||||||
|
|
||||||
|
validateForm: (showAlert = true) ->
|
||||||
|
if @emptyFields().length == 0
|
||||||
|
return true
|
||||||
|
|
||||||
|
if showAlert
|
||||||
|
@showAlert('Please fill in all fields')
|
||||||
|
|
||||||
|
false
|
||||||
|
|
||||||
|
submit: (e) ->
|
||||||
|
@preventDefaultAndStopPropagation(e)
|
||||||
|
|
||||||
|
if !@validateForm()
|
||||||
|
return
|
||||||
|
|
||||||
|
@hideAlert()
|
||||||
|
@toggleUserInteraction(false)
|
||||||
|
|
||||||
|
kb = App.KnowledgeBase.find(@knowledge_base_id)
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'update_menu_items'
|
||||||
|
type: 'PATCH'
|
||||||
|
url: kb.manageUrl('update_menu_items')
|
||||||
|
data: JSON.stringify(@buildData())
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
for menu_item in App.KnowledgeBaseMenuItem.using_kb_locale(@kb_locale)
|
||||||
|
menu_item.remove(clear: true)
|
||||||
|
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
|
||||||
|
@menu_items = App.KnowledgeBaseMenuItem.using_kb_locale(@kb_locale)
|
||||||
|
@render()
|
||||||
|
error: (xhr) =>
|
||||||
|
@showAlert(xhr.responseJSON?.error_human || 'Couldn\'t save changes')
|
||||||
|
@toggleUserInteraction(true)
|
||||||
|
)
|
|
@ -0,0 +1,126 @@
|
||||||
|
class App.KnowledgeBaseReaderController extends App.Controller
|
||||||
|
@extend App.PopoverProvidable
|
||||||
|
@registerPopovers 'Ticket'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-answer-title': 'answerTitle'
|
||||||
|
'.js-answer-body': 'answerBody'
|
||||||
|
'.js-answer-pagination': 'answerPagination'
|
||||||
|
'.js-answer-attachments': 'answerAttachments'
|
||||||
|
'.js-answer-linked-tickets': 'answerLinkedTickets'
|
||||||
|
'.js-answer-meta': 'answerMeta'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
translation = @object.translation(@parentController.kb_locale().id)
|
||||||
|
|
||||||
|
@html App.view('knowledge_base/reader')(
|
||||||
|
search_return_url: @buildSearchReturnUrl()
|
||||||
|
)
|
||||||
|
|
||||||
|
if translation and !translation.fullyLoaded()
|
||||||
|
@startLoading(@answerBody)
|
||||||
|
|
||||||
|
translation.loadFull (isSuccess) =>
|
||||||
|
@stopLoading()
|
||||||
|
|
||||||
|
if !isSuccess
|
||||||
|
return
|
||||||
|
|
||||||
|
@initialize()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
@initialize()
|
||||||
|
|
||||||
|
initialize: ->
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@stopListening()
|
||||||
|
|
||||||
|
kb_locale = @parentController.kb_locale()
|
||||||
|
|
||||||
|
@renderAnswer(@object, kb_locale)
|
||||||
|
|
||||||
|
if !@object
|
||||||
|
return
|
||||||
|
|
||||||
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
|
@renderAnswer(@object, kb_locale)
|
||||||
|
|
||||||
|
renderAnswer: (answer, kb_locale) ->
|
||||||
|
if !answer
|
||||||
|
@parentController.renderNotFound()
|
||||||
|
return
|
||||||
|
|
||||||
|
if !answer.exists()
|
||||||
|
@parentController.renderNotAvailableAnymore()
|
||||||
|
return
|
||||||
|
|
||||||
|
@renderAttachments(answer.attachments)
|
||||||
|
@renderLinkedTickets(answer.translation(kb_locale.id)?.linked_tickets())
|
||||||
|
|
||||||
|
paginator = new App.KnowledgeBaseReaderPagination(object: @object, kb_locale: kb_locale)
|
||||||
|
@answerPagination.html paginator.el
|
||||||
|
|
||||||
|
answer_translation = answer.translation(kb_locale.id)
|
||||||
|
|
||||||
|
if !answer_translation
|
||||||
|
@renderTranslationMissing(answer)
|
||||||
|
return
|
||||||
|
|
||||||
|
@answerTitle.text(answer_translation.title)
|
||||||
|
|
||||||
|
@renderBody(answer_translation)
|
||||||
|
|
||||||
|
@answerMeta.html App.view('knowledge_base/_reader_answer_meta')(
|
||||||
|
answer: answer
|
||||||
|
)
|
||||||
|
|
||||||
|
@renderPopovers()
|
||||||
|
|
||||||
|
renderBody: (translation) ->
|
||||||
|
body = $($.parseHTML(translation.content().body))
|
||||||
|
|
||||||
|
for linkDom in body.find('a').andSelf('a').toArray()
|
||||||
|
switch $(linkDom).attr('data-target-type')
|
||||||
|
when 'knowledge-base-answer'
|
||||||
|
if object = App.KnowledgeBaseAnswerTranslation.find $(linkDom).attr('data-target-id')
|
||||||
|
$(linkDom).attr 'href', object.uiUrl()
|
||||||
|
else
|
||||||
|
$(linkDom).attr 'href', '#'
|
||||||
|
|
||||||
|
@answerBody.html(body)
|
||||||
|
|
||||||
|
renderAttachments: (attachments) ->
|
||||||
|
@answerAttachments.html App.view('generic/attachments')(
|
||||||
|
attachments: attachments
|
||||||
|
)
|
||||||
|
|
||||||
|
renderLinkedTickets: (linked_tickets) ->
|
||||||
|
@answerLinkedTickets.html App.view('knowledge_base/_reader_linked_tickets')(
|
||||||
|
tickets: linked_tickets
|
||||||
|
)
|
||||||
|
|
||||||
|
renderTranslationMissing: (answer) ->
|
||||||
|
if !@parentController.isEditor()
|
||||||
|
@parentController.renderNotFound()
|
||||||
|
return
|
||||||
|
|
||||||
|
@renderScreenPlaceholder(
|
||||||
|
icon: App.Utils.icon('mood-ok')
|
||||||
|
detail: 'Not available in selected language'
|
||||||
|
el: @answerBody
|
||||||
|
action: 'Create a translation'
|
||||||
|
actionCallback: =>
|
||||||
|
url = answer.uiUrl(@parentController.kb_locale(), 'edit')
|
||||||
|
@navigate url
|
||||||
|
)
|
||||||
|
|
||||||
|
buildSearchReturnUrl: ->
|
||||||
|
if @parentController.lastParams.action != 'search-return'
|
||||||
|
return
|
||||||
|
|
||||||
|
decodeURIComponent @parentController.lastParams.arguments
|
|
@ -0,0 +1,79 @@
|
||||||
|
class App.KnowledgeBaseReaderListContainer extends App.Controller
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
|
@parentRefreshed()
|
||||||
|
|
||||||
|
tag: 'ul'
|
||||||
|
className: 'sections'
|
||||||
|
|
||||||
|
parentRefreshed: ->
|
||||||
|
newIds = @children().map (elem) -> elem.id
|
||||||
|
oldIds = @el.children().toArray().map (elem) -> parseInt(elem.dataset.id)
|
||||||
|
|
||||||
|
if _.isEqual(newIds, oldIds)
|
||||||
|
return
|
||||||
|
|
||||||
|
App.Delay.set(=>
|
||||||
|
@render()
|
||||||
|
, 200, "#{@constructor.className}_#{@parent.constructor.className}:#{@parent.id}", 'kb_category_refresh')
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@el.empty()
|
||||||
|
|
||||||
|
for child in @children()
|
||||||
|
@el.append new App.KnowledgeBaseReaderListItem(
|
||||||
|
item: child
|
||||||
|
isEditor: @isEditor
|
||||||
|
iconset: @parent.knowledge_base().iconset
|
||||||
|
kb_locale: @kb_locale
|
||||||
|
parentController: @
|
||||||
|
).el
|
||||||
|
|
||||||
|
class App.KnowledgeBaseReaderListContainer.Answers extends App.KnowledgeBaseReaderListContainer
|
||||||
|
children: ->
|
||||||
|
if !(@parent instanceof App.KnowledgeBaseCategory)
|
||||||
|
return []
|
||||||
|
|
||||||
|
answers = @parent.answers()
|
||||||
|
|
||||||
|
if !@isEditor
|
||||||
|
answers = answers.filter (elem) => elem.is_internally_published(@kb_locale)
|
||||||
|
|
||||||
|
answers
|
||||||
|
|
||||||
|
class App.KnowledgeBaseReaderListContainer.Categories extends App.KnowledgeBaseReaderListContainer
|
||||||
|
render: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@el.addClass "sections--#{@layout()}"
|
||||||
|
@el[0].dataset['size'] = @size()
|
||||||
|
|
||||||
|
children: ->
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
items = if @parent instanceof App.KnowledgeBase
|
||||||
|
@parent.rootCategories()
|
||||||
|
else if @parent instanceof App.KnowledgeBaseCategory
|
||||||
|
@parent.children()
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
if !@isEditor
|
||||||
|
items = items.filter (elem) => elem.visibleInternally(@kb_locale)
|
||||||
|
|
||||||
|
items
|
||||||
|
|
||||||
|
layout: ->
|
||||||
|
if @parent instanceof App.KnowledgeBase
|
||||||
|
@parent.knowledge_base().homepage_layout
|
||||||
|
else
|
||||||
|
@parent.knowledge_base().category_layout
|
||||||
|
|
||||||
|
size: ->
|
||||||
|
if @parent instanceof App.KnowledgeBase
|
||||||
|
'large'
|
||||||
|
else
|
||||||
|
'medium'
|
|
@ -0,0 +1,64 @@
|
||||||
|
class App.KnowledgeBaseReaderListController extends App.Controller
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
|
if !@objectVisibleInternally()
|
||||||
|
@parentController.renderNotAvailableAnymore()
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-readerListContainer': 'container'
|
||||||
|
|
||||||
|
objectVisibleInternally: ->
|
||||||
|
@object.visibleInternally(@parentController.kb_locale())
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
if !@parentController.isEditor() && (!@object || !@object.exists() || !@objectVisibleInternally())
|
||||||
|
@parentController.renderNotFound()
|
||||||
|
return
|
||||||
|
|
||||||
|
if @object.isEmpty()
|
||||||
|
@renderScreenPlaceholder(
|
||||||
|
icon: App.Utils.icon('mood-ok')
|
||||||
|
detail: 'This category is empty'
|
||||||
|
action: 'Start Editing'
|
||||||
|
actionCallback: =>
|
||||||
|
url = @object.uiUrl(@parentController.kb_locale(), 'edit')
|
||||||
|
@navigate url
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
@html App.view('knowledge_base/reader_list')()
|
||||||
|
|
||||||
|
@searchFieldPanel = new App.KnowledgeBaseSearchFieldPanel(
|
||||||
|
el: @$('.js-searchFieldContainer')
|
||||||
|
|
||||||
|
context: @object
|
||||||
|
kb_locale: @parentController.kb_locale()
|
||||||
|
return_path: @object.uiUrl(@parentController.kb_locale(), 'search-inline')
|
||||||
|
|
||||||
|
willStart: @searchPanelWillStart
|
||||||
|
didEnd: @searchPanelDidEnd
|
||||||
|
)
|
||||||
|
|
||||||
|
if @parentController.lastParams.action is 'search-inline'
|
||||||
|
@searchFieldPanel.widget.startSearch(@parentController.lastParams.arguments)
|
||||||
|
|
||||||
|
isEditor = @parentController.isEditor()
|
||||||
|
kb_locale = @parentController.kb_locale()
|
||||||
|
|
||||||
|
setTimeout =>
|
||||||
|
for kind in ['Categories', 'Answers']
|
||||||
|
@container.append new App.KnowledgeBaseReaderListContainer[kind](
|
||||||
|
parent: @object
|
||||||
|
isEditor: isEditor
|
||||||
|
kb_locale: kb_locale
|
||||||
|
).el
|
||||||
|
, 100
|
||||||
|
|
||||||
|
searchPanelWillStart: =>
|
||||||
|
@container.addClass('hide')
|
||||||
|
|
||||||
|
searchPanelDidEnd: =>
|
||||||
|
@container.removeClass('hide')
|
|
@ -0,0 +1,34 @@
|
||||||
|
class App.KnowledgeBaseReaderListItem extends App.Controller
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
@el[0].dataset.id = @item.id
|
||||||
|
|
||||||
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
|
@render()
|
||||||
|
|
||||||
|
tag: 'li'
|
||||||
|
className: 'section'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
if @sort_order != null && @sort_order != @item.position
|
||||||
|
App.Delay.set(=>
|
||||||
|
@parentController.parentRefreshed()
|
||||||
|
, 1000, 'kb_reader_list_resort')
|
||||||
|
|
||||||
|
@sort_order = @item.position
|
||||||
|
|
||||||
|
attrs = @item.attributesForRendering(@kb_locale, isEditor: @isEditor)
|
||||||
|
|
||||||
|
@el
|
||||||
|
.prop('className')
|
||||||
|
.split(' ')
|
||||||
|
.filter (elem) -> elem.match 'kb-item--'
|
||||||
|
.forEach (elem) -> @el.removeClass(elem)
|
||||||
|
|
||||||
|
@el.addClass attrs.className
|
||||||
|
|
||||||
|
@html App.view('knowledge_base/_reader_list_item')(
|
||||||
|
item: attrs
|
||||||
|
iconset: @iconset
|
||||||
|
)
|
|
@ -0,0 +1,35 @@
|
||||||
|
class Router extends App.ControllerPermanent
|
||||||
|
requiredPermission: 'knowledge_base.*'
|
||||||
|
|
||||||
|
constructor: (params) ->
|
||||||
|
super
|
||||||
|
|
||||||
|
if params['locale']
|
||||||
|
params.selectedSystemLocale = App.Locale.findByAttribute('locale', params['locale'])
|
||||||
|
params.selectedSystemLocalePresent = true
|
||||||
|
|
||||||
|
# check authentication
|
||||||
|
@authenticateCheckRedirect()
|
||||||
|
|
||||||
|
App.TaskManager.execute(
|
||||||
|
key: 'KnowledgeBase'
|
||||||
|
controller: 'KnowledgeBaseAgentController'
|
||||||
|
params: params
|
||||||
|
show: true
|
||||||
|
persistent: true
|
||||||
|
)
|
||||||
|
|
||||||
|
[
|
||||||
|
'/category/:category_id'
|
||||||
|
'/answer/:answer_id'
|
||||||
|
''
|
||||||
|
]
|
||||||
|
.reduce((memo, elem) ->
|
||||||
|
memo.concat [elem, elem + '/:action', elem + '/:action/:arguments']
|
||||||
|
, [])
|
||||||
|
.forEach (elem) ->
|
||||||
|
url = "knowledge_base/:knowledge_base_id/locale/:locale#{elem}" # App.Utils not yet available, thus not using App.Utils.joinUrlComponents
|
||||||
|
App.Config.set(url, Router, 'Routes')
|
||||||
|
|
||||||
|
App.Config.set('knowledge_base', Router, 'Routes')
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
class App.KnowledgeBaseScheduledWidget extends App.Controller
|
||||||
|
className: 'scheduled-widget'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@el.attr('data-state', @state)
|
||||||
|
@el.data('date', @getDate())
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
|
getDate: ->
|
||||||
|
if string = @object["#{@state}_at"]
|
||||||
|
new Date(string)
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('knowledge_base/scheduled_widget')(
|
||||||
|
timestamp: App.i18n.translateTimestamp(@getDate())
|
||||||
|
state: @state
|
||||||
|
)
|
|
@ -0,0 +1,20 @@
|
||||||
|
class App.KnowledgeBaseSearchController extends App.Controller
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@html App.view('knowledge_base/search')(
|
||||||
|
knowledge_base: @parentController.getKnowledgeBase()
|
||||||
|
kb_locale: @parentController.kb_locale()
|
||||||
|
)
|
||||||
|
|
||||||
|
@searchFieldPanel = new App.KnowledgeBaseSearchFieldPanel(
|
||||||
|
el: @$('.js-searchFieldContainer')
|
||||||
|
|
||||||
|
context: @parentController.getKnowledgeBase()
|
||||||
|
kb_locale: @parentController.kb_locale()
|
||||||
|
return_path: @parentController.getKnowledgeBase().uiUrl(@parentController.kb_locale(), 'search')
|
||||||
|
)
|
||||||
|
|
||||||
|
if query = @parentController.lastParams.arguments
|
||||||
|
@searchFieldPanel.widget.startSearch(query)
|
||||||
|
|
||||||
|
@searchFieldPanel.widget.focus()
|
|
@ -0,0 +1,84 @@
|
||||||
|
class App.KnowledgeBaseSearchFieldPanel extends App.Controller
|
||||||
|
elements:
|
||||||
|
'.js-placeholderEmpty': 'emptyPlaceholder'
|
||||||
|
'.js-placeholderError': 'errorPlaceholder'
|
||||||
|
'.js-results': 'resultsContainer'
|
||||||
|
|
||||||
|
context: undefined
|
||||||
|
kb_locale: null
|
||||||
|
|
||||||
|
#callbacks
|
||||||
|
willStart: null
|
||||||
|
didEnd: null
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@html App.view('knowledge_base/search_field_panel')()
|
||||||
|
|
||||||
|
@widget = new App.KnowledgeBaseSearchFieldWidget(
|
||||||
|
el: @$('.searchfield')
|
||||||
|
kb_locale: @kb_locale
|
||||||
|
context: @context
|
||||||
|
|
||||||
|
willStart: @widgetWillStart
|
||||||
|
didEnd: @widgetDidEnd
|
||||||
|
|
||||||
|
willStartLoading: @widgetWillStartLoading
|
||||||
|
|
||||||
|
renderError: @renderError
|
||||||
|
renderResults: @renderResults
|
||||||
|
)
|
||||||
|
|
||||||
|
clear: =>
|
||||||
|
@resultsContainer.empty()
|
||||||
|
@errorPlaceholder.addClass('hide')
|
||||||
|
@emptyPlaceholder.addClass('hide')
|
||||||
|
|
||||||
|
widgetWillStart: =>
|
||||||
|
@willStart?()
|
||||||
|
|
||||||
|
widgetDidEnd: =>
|
||||||
|
@clear()
|
||||||
|
@didEnd?()
|
||||||
|
|
||||||
|
widgetWillStartLoading: =>
|
||||||
|
@clear()
|
||||||
|
|
||||||
|
renderError: (text) =>
|
||||||
|
@errorPlaceholder
|
||||||
|
.removeClass('hide')
|
||||||
|
.find('.help-block--inner')
|
||||||
|
.text(App.i18n.translateInline(text))
|
||||||
|
|
||||||
|
renderResults: (results, originalQuery) =>
|
||||||
|
@clear()
|
||||||
|
|
||||||
|
if results.result.length == 0
|
||||||
|
@emptyPlaceholder.removeClass('hide')
|
||||||
|
return
|
||||||
|
|
||||||
|
suffix = @buildReturnSuffix(originalQuery)
|
||||||
|
return_path = App.Utils.joinUrlComponents(@return_path, originalQuery)
|
||||||
|
|
||||||
|
views = results
|
||||||
|
.result
|
||||||
|
.map (elem, index) ->
|
||||||
|
details = results.details[index]
|
||||||
|
klass_name = elem.type.replace /::/g, ''
|
||||||
|
|
||||||
|
object = App[klass_name].find(elem.id)
|
||||||
|
|
||||||
|
new App.KnowledgeBaseSearchItem(
|
||||||
|
object: object
|
||||||
|
meta: elem
|
||||||
|
details: details
|
||||||
|
pathSuffix: suffix
|
||||||
|
return_path: return_path
|
||||||
|
)
|
||||||
|
|
||||||
|
.map (elem) -> elem.el
|
||||||
|
|
||||||
|
@resultsContainer.append views
|
||||||
|
|
||||||
|
buildReturnSuffix: (query) ->
|
||||||
|
encodeURIComponent App.Utils.joinUrlComponents(@return_path, query)
|
|
@ -0,0 +1,117 @@
|
||||||
|
class App.KnowledgeBaseSearchFieldWidget extends App.Controller
|
||||||
|
className: 'searchfield'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-searchField': 'searchField'
|
||||||
|
'.js-emptySearchButton': 'emptySearchButton'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'input .js-searchField': 'input'
|
||||||
|
'click .js-emptySearchButton': 'clear'
|
||||||
|
|
||||||
|
isActive: false
|
||||||
|
|
||||||
|
context: undefined
|
||||||
|
kb_locale: null
|
||||||
|
|
||||||
|
# callbacks
|
||||||
|
renderError: null
|
||||||
|
renderResults: null
|
||||||
|
willStartLoading: null
|
||||||
|
willStart: null
|
||||||
|
didEnd: null
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@cache = {}
|
||||||
|
|
||||||
|
@html App.view('knowledge_base/search_field_widget')(
|
||||||
|
placeholder_suffix: @context?.guaranteedTitle(@kb_locale.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
clear: ->
|
||||||
|
@searchField.val('')
|
||||||
|
@emptySearchButton.addClass 'hide'
|
||||||
|
|
||||||
|
@isActive = false
|
||||||
|
@didEnd?()
|
||||||
|
|
||||||
|
input: ->
|
||||||
|
query = @searchField.val()
|
||||||
|
|
||||||
|
@emptySearchButton.toggleClass 'hide', query.length == 0
|
||||||
|
|
||||||
|
if query == ''
|
||||||
|
@abortAjaxCalls()
|
||||||
|
@isActive = false
|
||||||
|
@didEnd?()
|
||||||
|
return
|
||||||
|
|
||||||
|
if !@isActive
|
||||||
|
@isActive = true
|
||||||
|
@willStart?()
|
||||||
|
|
||||||
|
@willStartLoading?()
|
||||||
|
|
||||||
|
@searchField.addClass('loading')
|
||||||
|
|
||||||
|
@delay( =>
|
||||||
|
@makeRequest(query)
|
||||||
|
, 100, 'makeRequest')
|
||||||
|
|
||||||
|
data: (query) ->
|
||||||
|
attrs = {
|
||||||
|
query: query,
|
||||||
|
flavor: 'agent',
|
||||||
|
knowledge_base_id: @context.knowledge_base().id
|
||||||
|
locale: @kb_locale.systemLocale().locale
|
||||||
|
}
|
||||||
|
|
||||||
|
if @context instanceof App.KnowledgeBaseCategory
|
||||||
|
attrs['scope_id'] = @context.id
|
||||||
|
|
||||||
|
attrs
|
||||||
|
|
||||||
|
url: ->
|
||||||
|
App.Utils.joinUrlComponents(App.KnowledgeBase.url, 'search')
|
||||||
|
|
||||||
|
makeRequest: (query) ->
|
||||||
|
if (cachedResult = @cache[query])
|
||||||
|
@onSuccess(cachedResult)
|
||||||
|
return
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'kb_search_loading'
|
||||||
|
type: 'POST'
|
||||||
|
url: @url()
|
||||||
|
data: JSON.stringify(@data(query))
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@cache[query] = data
|
||||||
|
@onSuccess(data, query)
|
||||||
|
error: @onError
|
||||||
|
)
|
||||||
|
|
||||||
|
onError: (xhr) =>
|
||||||
|
if xhr.status == 0
|
||||||
|
if @ajaxCalls.length == 0
|
||||||
|
@searchField.removeClass('loading')
|
||||||
|
return
|
||||||
|
|
||||||
|
@searchField.removeClass('loading')
|
||||||
|
|
||||||
|
text = xhr.responseJSON?.error_human || xhr.responseJSON?.errorr || 'Unable to load'
|
||||||
|
@renderError(text)
|
||||||
|
|
||||||
|
onSuccess: (data, originalQuery) =>
|
||||||
|
@searchField.removeClass('loading')
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@renderResults?(data, originalQuery)
|
||||||
|
|
||||||
|
focus: ->
|
||||||
|
@searchField.focus()
|
||||||
|
|
||||||
|
startSearch: (query) ->
|
||||||
|
@searchField
|
||||||
|
.val(query)
|
||||||
|
.trigger('input')
|
|
@ -0,0 +1,23 @@
|
||||||
|
class App.KnowledgeBaseSearchItem extends App.Controller
|
||||||
|
tag: 'li'
|
||||||
|
className: 'section'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click a': 'searchLinkClicked'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
|
data: ->
|
||||||
|
output = @details || {}
|
||||||
|
output['url'] = @object?.uiUrl("search-return/#{@pathSuffix}") || '#'
|
||||||
|
output
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('knowledge_base/search_item')(data: @data())
|
||||||
|
|
||||||
|
searchLinkClicked: -> # setup history and let it continue, no need to prevent default action or bubbling
|
||||||
|
if window.history? and @return_path?
|
||||||
|
window.history.replaceState(null, null, @return_path)
|
|
@ -0,0 +1,63 @@
|
||||||
|
class App.KnowledgeBaseSidebar extends App.Controller
|
||||||
|
@extend(Spine.Events)
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .js-content-actions-container a': 'contentActionClicked'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@show()
|
||||||
|
|
||||||
|
@bind 'knowledge_base::sidebar::rerender', => @rerender()
|
||||||
|
|
||||||
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
|
@rerender()
|
||||||
|
true
|
||||||
|
|
||||||
|
rerender: ->
|
||||||
|
@show(@savedParams, @savedAction)
|
||||||
|
|
||||||
|
contentActionClicked: (e) ->
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
actionName = switch e.target.dataset.action
|
||||||
|
when 'delete' then 'clickedDelete'
|
||||||
|
when 'visibility' then 'clickedCanBePublished'
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
@parentController.bodyModal = @parentController.coordinator[actionName]?(@savedParams)
|
||||||
|
|
||||||
|
show: (object, action) ->
|
||||||
|
isEdit = action is 'edit'
|
||||||
|
|
||||||
|
@el.toggleClass('hidden', !isEdit)
|
||||||
|
@savedParams = object
|
||||||
|
@savedAction = action
|
||||||
|
@el.empty()
|
||||||
|
|
||||||
|
if !isEdit
|
||||||
|
return
|
||||||
|
|
||||||
|
for widget in @widgets(object)
|
||||||
|
@el.append new widget(
|
||||||
|
object: object
|
||||||
|
kb_locale: @parentController.kb_locale()
|
||||||
|
parentController: @parentController
|
||||||
|
).el
|
||||||
|
|
||||||
|
hide: ->
|
||||||
|
@el.addClass('hidden')
|
||||||
|
|
||||||
|
widgets: (object) ->
|
||||||
|
output = [App.KnowledgeBaseSidebarActions]
|
||||||
|
|
||||||
|
if object instanceof App.KnowledgeBase || object instanceof App.KnowledgeBaseCategory
|
||||||
|
output.push App.KnowledgeBaseSidebarCategories
|
||||||
|
|
||||||
|
if object instanceof App.KnowledgeBaseCategory
|
||||||
|
output.push App.KnowledgeBaseSidebarAnswers
|
||||||
|
|
||||||
|
if object instanceof App.KnowledgeBaseAnswer
|
||||||
|
output.push App.KnowledgeBaseSidebarLinkedTickets
|
||||||
|
output.push App.KnowledgeBaseSidebarAttachments
|
||||||
|
|
||||||
|
output
|
|
@ -0,0 +1,54 @@
|
||||||
|
class App.KnowledgeBaseSidebarGenericList extends App.Controller
|
||||||
|
className: 'sidebar-block'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .js-reorder': 'openReorder'
|
||||||
|
'click .js-add': 'openAdd'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@html App.view('knowledge_base/sidebar/generic_list')(@templateOptions())
|
||||||
|
|
||||||
|
templateOptions: ->
|
||||||
|
iconset: @object.knowledge_base().iconset
|
||||||
|
items: @items()
|
||||||
|
urlNew: @urlNew()
|
||||||
|
enabled: true
|
||||||
|
title: @title
|
||||||
|
emptyNote: @emptyNote
|
||||||
|
|
||||||
|
openReorder: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
@parentController.bodyModal = new App.ControllerReorderModal(
|
||||||
|
container: @parentController.body
|
||||||
|
items: @items()
|
||||||
|
url: @reorderSaveUrl()
|
||||||
|
)
|
||||||
|
|
||||||
|
openAdd: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
newObject = @newObject()
|
||||||
|
newObject.isFresh = true
|
||||||
|
|
||||||
|
@parentController.bodyModal = new App.KnowledgeBaseAddForm(
|
||||||
|
object: newObject
|
||||||
|
container: @parentController.body
|
||||||
|
parentController: @parentController
|
||||||
|
)
|
||||||
|
|
||||||
|
newObject: ->
|
||||||
|
#has to be overridden
|
||||||
|
|
||||||
|
reorderSaveUrl: ->
|
||||||
|
#has to be overridden
|
||||||
|
|
||||||
|
items: ->
|
||||||
|
#has to be overridden
|
||||||
|
|
||||||
|
urlNew: ->
|
||||||
|
#has to be overridden
|
|
@ -0,0 +1,14 @@
|
||||||
|
class App.KnowledgeBaseSidebarActions extends App.Controller
|
||||||
|
className: 'sidebar-block'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
actions = @object?.contentSidebarActions(@kb_locale)
|
||||||
|
|
||||||
|
html = if actions
|
||||||
|
App.view('knowledge_base/sidebar/actions')(actions: actions)
|
||||||
|
else
|
||||||
|
''
|
||||||
|
|
||||||
|
@html html
|
|
@ -0,0 +1,23 @@
|
||||||
|
class App.KnowledgeBaseSidebarAnswers extends App.KnowledgeBaseSidebarGenericList
|
||||||
|
templateName: 'answers'
|
||||||
|
title: 'Answers'
|
||||||
|
emptyNote: 'No answers'
|
||||||
|
|
||||||
|
urlNew: ->
|
||||||
|
"#knowledge_base/#{@object.knowledge_base().id}/category/#{@object.id}/answers/new"
|
||||||
|
|
||||||
|
answers: ->
|
||||||
|
@object.answers()
|
||||||
|
|
||||||
|
items: ->
|
||||||
|
@answers()
|
||||||
|
.sort (a, b) ->
|
||||||
|
a.position - b.position
|
||||||
|
.map (elem) =>
|
||||||
|
elem.attributesForRendering(@kb_locale, action: 'edit', isEditor: true)
|
||||||
|
|
||||||
|
reorderSaveUrl: ->
|
||||||
|
@object.generateURL('reorder_answers')
|
||||||
|
|
||||||
|
newObject: ->
|
||||||
|
new App.KnowledgeBaseAnswer(category_id: @object.id)
|
|
@ -0,0 +1,135 @@
|
||||||
|
class App.KnowledgeBaseSidebarAttachments extends App.Controller
|
||||||
|
className: 'sidebar-block'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .js-delete': 'delete'
|
||||||
|
'html5Upload.dropZone.show': 'showDropZone'
|
||||||
|
'html5Upload.dropZone.hide': 'hideDropZone'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.attachmentUpload-progressBar': 'progressBar'
|
||||||
|
'.js-percentage': 'progressText'
|
||||||
|
'.attachmentPlaceholder': 'attachmentPlaceholder'
|
||||||
|
'.attachmentUpload': 'attachmentUpload'
|
||||||
|
'.js-cancel': 'cancelContainer'
|
||||||
|
'input': 'input'
|
||||||
|
'.dropContainer': 'dropContainer'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@render()
|
||||||
|
@listenTo @object, 'refresh', @needsUpdate
|
||||||
|
|
||||||
|
needsUpdate: =>
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('knowledge_base/sidebar/attachments')(
|
||||||
|
attachments: @object.attachments
|
||||||
|
)
|
||||||
|
|
||||||
|
html5Upload.initialize(
|
||||||
|
uploadUrl: @object.generateURL('attachments')
|
||||||
|
dropContainer: @el.get(0)
|
||||||
|
cancelContainer: @cancelContainer
|
||||||
|
inputField: @input.get(0)
|
||||||
|
maxSimultaneousUploads: 1,
|
||||||
|
key: 'file'
|
||||||
|
onFileAdded: @onFileAdded
|
||||||
|
)
|
||||||
|
|
||||||
|
delete: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
id = parseInt($(e.currentTarget).attr('data-object-id'))
|
||||||
|
attachment = @object.attachments.filter((elem) -> elem.id == id)[0]
|
||||||
|
|
||||||
|
new DeleteConfirm(
|
||||||
|
container: @container
|
||||||
|
answer: @object
|
||||||
|
attachment: attachment
|
||||||
|
parentController: @
|
||||||
|
)
|
||||||
|
|
||||||
|
fetch: =>
|
||||||
|
@ajax(
|
||||||
|
id: "attachments_#{@object.id}_knowledge_base_answer"
|
||||||
|
type: 'GET'
|
||||||
|
url: @object.generateURL() + '?full=true'
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@render()
|
||||||
|
)
|
||||||
|
|
||||||
|
onFileAdded: (file) =>
|
||||||
|
file.on(
|
||||||
|
onStart: @onStart
|
||||||
|
onAborted: @onAborted
|
||||||
|
onCompleted: @onCompleted
|
||||||
|
onProgress: @onProgress
|
||||||
|
)
|
||||||
|
|
||||||
|
onStart: =>
|
||||||
|
@attachmentPlaceholder.addClass('hide')
|
||||||
|
@attachmentUpload.removeClass('hide')
|
||||||
|
@cancelContainer.removeClass('hide')
|
||||||
|
|
||||||
|
onAborted: =>
|
||||||
|
@attachmentPlaceholder.removeClass('hide')
|
||||||
|
@attachmentUpload.addClass('hide')
|
||||||
|
@input.val('')
|
||||||
|
|
||||||
|
onCompleted: (response) =>
|
||||||
|
@attachmentPlaceholder.removeClass('hide')
|
||||||
|
@attachmentUpload.addClass('hide')
|
||||||
|
|
||||||
|
@progressBar.width(parseInt(0) + '%')
|
||||||
|
@progressText.text('')
|
||||||
|
|
||||||
|
@input.val('')
|
||||||
|
|
||||||
|
data = JSON.parse(response)
|
||||||
|
App.Collection.loadAssets(data)
|
||||||
|
|
||||||
|
onProgress: (progress, fileSize, uploadedBytes) =>
|
||||||
|
@progressBar.width(parseInt(progress) + '%')
|
||||||
|
@progressText.text(parseInt(progress))
|
||||||
|
# hide cancel on 90%
|
||||||
|
if parseInt(progress) >= 90
|
||||||
|
@cancelContainer.addClass('hide')
|
||||||
|
|
||||||
|
showDropZone: ->
|
||||||
|
if @dropContainer.hasClass('is-dropTarget')
|
||||||
|
return
|
||||||
|
|
||||||
|
@dropContainer.addClass('is-dropTarget')
|
||||||
|
|
||||||
|
hideDropZone: ->
|
||||||
|
@dropContainer.removeClass('is-dropTarget')
|
||||||
|
|
||||||
|
class DeleteConfirm extends App.ControllerConfirm
|
||||||
|
content: ->
|
||||||
|
sentence = App.i18n.translateContent('Are you sure to delete')
|
||||||
|
"#{sentence} #{@attachment.filename}?"
|
||||||
|
buttonSubmit: 'delete'
|
||||||
|
onSubmit: ->
|
||||||
|
@formDisable(@el)
|
||||||
|
|
||||||
|
@ajax(
|
||||||
|
id: 'attachment_delete'
|
||||||
|
type: 'DELETE'
|
||||||
|
url: @answer.generateURL("attachments/#{@attachment.id}")
|
||||||
|
processData: true
|
||||||
|
success: @success
|
||||||
|
error: @error
|
||||||
|
)
|
||||||
|
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@close()
|
||||||
|
App.Collection.loadAssets(data)
|
||||||
|
@parentController.render()
|
||||||
|
|
||||||
|
error: (xhr) =>
|
||||||
|
@formEnable(@el)
|
||||||
|
@showAlert(xhr.responseJSON?.error || 'Unable to save changes')
|
|
@ -0,0 +1,45 @@
|
||||||
|
class App.KnowledgeBaseSidebarCategories extends App.KnowledgeBaseSidebarGenericList
|
||||||
|
templateName: 'categories'
|
||||||
|
title: 'Categories'
|
||||||
|
emptyNote: 'No categories'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
templateOptions: ->
|
||||||
|
attrs = super
|
||||||
|
attrs.isRoot = @object instanceof App.KnowledgeBase
|
||||||
|
attrs
|
||||||
|
|
||||||
|
urlNew: ->
|
||||||
|
prefix = "#knowledge_base/#{@object.knowledge_base().id}/category/"
|
||||||
|
|
||||||
|
if @object instanceof App.KnowledgeBaseCategory
|
||||||
|
prefix + "#{@object.id}/new"
|
||||||
|
else if @object instanceof App.KnowledgeBase
|
||||||
|
prefix + 'category/new'
|
||||||
|
|
||||||
|
categories: ->
|
||||||
|
if @object instanceof App.KnowledgeBaseCategory
|
||||||
|
@object.children()
|
||||||
|
else if @object instanceof App.KnowledgeBase
|
||||||
|
@object.rootCategories()
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
|
||||||
|
items: ->
|
||||||
|
@categories()
|
||||||
|
.sort (a, b) ->
|
||||||
|
a.position - b.position
|
||||||
|
.map (elem) =>
|
||||||
|
elem.attributesForRendering(@kb_locale, action: 'edit', isEditor: true)
|
||||||
|
|
||||||
|
reorderSaveUrl: ->
|
||||||
|
if @object instanceof App.KnowledgeBaseCategory
|
||||||
|
@object.generateURL('reorder_categories')
|
||||||
|
else
|
||||||
|
@object.url() + '/categories/reorder_root_categories'
|
||||||
|
|
||||||
|
newObject: ->
|
||||||
|
parent = if @object instanceof App.KnowledgeBaseCategory then @object
|
||||||
|
new App.KnowledgeBaseCategory(parent_id: parent?.id, knowledge_base_id: @object.knowledge_base().id)
|
|
@ -0,0 +1,68 @@
|
||||||
|
class App.KnowledgeBaseSidebarLinkedTickets extends App.Controller
|
||||||
|
@extend App.PopoverProvidable
|
||||||
|
@registerPopovers 'Ticket'
|
||||||
|
|
||||||
|
className: 'sidebar-block'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .js-add': 'clickedAdd'
|
||||||
|
'click .js-delete': 'delete'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@render()
|
||||||
|
@listenTo @object, 'refresh', @needsUpdate
|
||||||
|
|
||||||
|
needsUpdate: =>
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('knowledge_base/sidebar/linked_tickets')(
|
||||||
|
tickets: @object.translation(@kb_locale.id)?.linked_tickets() || []
|
||||||
|
)
|
||||||
|
|
||||||
|
@renderPopovers()
|
||||||
|
|
||||||
|
fetch: =>
|
||||||
|
@ajax(
|
||||||
|
id: "links_#{@object.id}_knowledge_base_answer"
|
||||||
|
type: 'GET'
|
||||||
|
url: @object.generateURL() + '?full=true'
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@render()
|
||||||
|
)
|
||||||
|
|
||||||
|
clickedAdd: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
new App.TicketLinkAdd(
|
||||||
|
link_object: 'KnowledgeBase::Answer::Translation'
|
||||||
|
link_object_id: @object.translation(@kb_locale.id)?.id
|
||||||
|
link_types: [['normal', 'Normal']]
|
||||||
|
object: @object.translation(@kb_locale.id)
|
||||||
|
parent: @
|
||||||
|
container: @el.closest('.content')
|
||||||
|
)
|
||||||
|
|
||||||
|
delete: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
data =
|
||||||
|
link_type: $(e.currentTarget).data('link-type')
|
||||||
|
link_object_source: $(e.currentTarget).data('object')
|
||||||
|
link_object_source_value: $(e.currentTarget).data('object-id')
|
||||||
|
link_object_target: 'KnowledgeBase::Answer::Translation'
|
||||||
|
link_object_target_value: @object.translation(@kb_locale.id)?.id
|
||||||
|
|
||||||
|
# get data
|
||||||
|
@ajax(
|
||||||
|
id: "links_remove_#{@object.id}_#{@object_type}"
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/links/remove"
|
||||||
|
data: data
|
||||||
|
processData: true
|
||||||
|
success: @fetch
|
||||||
|
)
|
|
@ -2239,4 +2239,137 @@ class ChatToTicketRef extends App.ControllerContent
|
||||||
y2: y1 + @attachments.outerHeight()
|
y2: y1 + @attachments.outerHeight()
|
||||||
|
|
||||||
App.Config.set('layout_ref/chat_to_ticket', ChatToTicketRef, 'Routes')
|
App.Config.set('layout_ref/chat_to_ticket', ChatToTicketRef, 'Routes')
|
||||||
|
|
||||||
|
class KnowledgeBaseAgentReaderRef extends App.ControllerContent
|
||||||
|
className: 'flex knowledge-base vertical'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-search': 'searchInput'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click [data-target]': 'onTargetClicked'
|
||||||
|
'click .js-open-search': 'toggleSearch'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
App.Utils.loadIconFont('anticon')
|
||||||
|
@render()
|
||||||
|
@level(1)
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('layout_ref/kb_agent_reader_ref')()
|
||||||
|
|
||||||
|
toggleSearch: (event) ->
|
||||||
|
active = $(event.currentTarget).toggleClass('btn--primary')
|
||||||
|
if $(event.currentTarget).is('.btn--primary')
|
||||||
|
@el.find('.main[data-level]').addClass('hidden')
|
||||||
|
@el.find('[data-level~="search"]').removeClass('hidden')
|
||||||
|
@searchInput.focus()
|
||||||
|
else
|
||||||
|
@el.find("[data-level~=\"#{@currentLevel}\"]").removeClass('hidden')
|
||||||
|
@el.find('[data-level~="search"]').addClass('hidden')
|
||||||
|
|
||||||
|
onTargetClicked: (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
@level(event.currentTarget.dataset.target)
|
||||||
|
|
||||||
|
level: (level) ->
|
||||||
|
@currentLevel = level
|
||||||
|
@el.find('[data-level]').addClass('hidden')
|
||||||
|
@el.find("[data-level~=\"#{@currentLevel}\"]").removeClass('hidden')
|
||||||
|
|
||||||
|
App.Config.set('layout_ref/kb_agent_reader', KnowledgeBaseAgentReaderRef, 'Routes')
|
||||||
|
|
||||||
|
class KnowledgeBaseLinkTicketToAnswerRef extends App.ControllerContent
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
App.Utils.loadIconFont('anticon')
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
new App.ControllerModal
|
||||||
|
head: 'Link Answer'
|
||||||
|
buttonSubmit: false
|
||||||
|
container: @el
|
||||||
|
content: App.view('layout_ref/kb_link_ticket_to_answer_ref')
|
||||||
|
|
||||||
|
App.Config.set('layout_ref/kb_link_ticket_to_answer', KnowledgeBaseLinkTicketToAnswerRef, 'Routes')
|
||||||
|
|
||||||
|
class KnowledgeBaseLinkAnswerToAnswerRef extends App.ControllerContent
|
||||||
|
elements:
|
||||||
|
'.js-form': 'form'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('layout_ref/kb_link_answer_to_answer_ref')()
|
||||||
|
|
||||||
|
new App.ControllerForm(
|
||||||
|
grid: true
|
||||||
|
params:
|
||||||
|
category_id: 2
|
||||||
|
translation_ids: [
|
||||||
|
1
|
||||||
|
2
|
||||||
|
]
|
||||||
|
archived_at: null
|
||||||
|
internal_at: null
|
||||||
|
published_at: '2018-10-22T13:58:08.730Z'
|
||||||
|
attachments: []
|
||||||
|
id: 1
|
||||||
|
translation:
|
||||||
|
title: 'Lithium en-us'
|
||||||
|
content:
|
||||||
|
body:
|
||||||
|
text: 'Lithium (from Greek: λίθος, translit. lithos, lit. "stone") is a chemical element with symbol Li and atomic number 3. It is a soft, silvery-white alkali metal. Under standard conditions, it is the lightest metal and the lightest solid element. Like all alkali metals, lithium is highly reactive and flammable, and is stored in mineral oil.'
|
||||||
|
attachments: []
|
||||||
|
id: 1
|
||||||
|
answer_id: 1
|
||||||
|
id: 1
|
||||||
|
screen: 'agent'
|
||||||
|
autofocus: true
|
||||||
|
el: @form
|
||||||
|
model:
|
||||||
|
configure_attributes: [
|
||||||
|
{
|
||||||
|
name: 'translation::title'
|
||||||
|
model: 'translation'
|
||||||
|
display: 'Title'
|
||||||
|
tag: 'input'
|
||||||
|
grid_width: '1/2'
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name: 'category_id'
|
||||||
|
model: 'answer'
|
||||||
|
display: 'Category'
|
||||||
|
tag: 'select'
|
||||||
|
null: true
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 1
|
||||||
|
name: 'Metal'
|
||||||
|
}
|
||||||
|
{
|
||||||
|
value: 2
|
||||||
|
name: 'Alkali metal'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
grid_width: '1/2'
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name: 'translation::content::body'
|
||||||
|
model: 'translation'
|
||||||
|
display: 'Content'
|
||||||
|
tag: 'richtext'
|
||||||
|
buttons: [
|
||||||
|
'link'
|
||||||
|
'link_answer'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
App.Config.set('layout_ref/kb_link_answer_to_answer', KnowledgeBaseLinkAnswerToAnswerRef, 'Routes')
|
||||||
App.Config.set('LayoutRef', { prio: 1600, parent: '#current_user', name: 'Layout Reference', translate: true, target: '#layout_ref', permission: [ 'admin' ] }, 'NavBarRight')
|
App.Config.set('LayoutRef', { prio: 1600, parent: '#current_user', name: 'Layout Reference', translate: true, target: '#layout_ref', permission: [ 'admin' ] }, 'NavBarRight')
|
||||||
|
|
|
@ -21,6 +21,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
|
||||||
'click .js-global-search-result': 'emptyAndCloseDelayed'
|
'click .js-global-search-result': 'emptyAndCloseDelayed'
|
||||||
'click .js-details-link': 'openExtendedSearch'
|
'click .js-details-link': 'openExtendedSearch'
|
||||||
'change .js-menu .js-switch input': 'switch'
|
'change .js-menu .js-switch input': 'switch'
|
||||||
|
'click .js-onclick': 'click'
|
||||||
|
|
||||||
constructor: ->
|
constructor: ->
|
||||||
super
|
super
|
||||||
|
@ -97,6 +98,10 @@ class App.Navigation extends App.ControllerWidgetPermanent
|
||||||
item.switch = worker.switch()
|
item.switch = worker.switch()
|
||||||
if worker.active && worker.active()
|
if worker.active && worker.active()
|
||||||
activeTab[item.target] = true
|
activeTab[item.target] = true
|
||||||
|
if worker.onclick
|
||||||
|
item.onclick = worker.onclick()
|
||||||
|
if worker.accessoryIcon
|
||||||
|
item.accessoryIcon = worker.accessoryIcon()
|
||||||
if worker.featureActive
|
if worker.featureActive
|
||||||
if worker.featureActive()
|
if worker.featureActive()
|
||||||
shown = true
|
shown = true
|
||||||
|
@ -120,6 +125,13 @@ class App.Navigation extends App.ControllerWidgetPermanent
|
||||||
activeTab: activeTab
|
activeTab: activeTab
|
||||||
)
|
)
|
||||||
|
|
||||||
|
click: (e) ->
|
||||||
|
@preventDefaultAndStopPropagation(e)
|
||||||
|
|
||||||
|
key = $(e.currentTarget).data('key')
|
||||||
|
worker = App.TaskManager.worker(key)
|
||||||
|
worker.clicked(e)
|
||||||
|
|
||||||
# on switch changes and execute it on controller
|
# on switch changes and execute it on controller
|
||||||
switch: (e) ->
|
switch: (e) ->
|
||||||
val = $(e.target).prop('checked')
|
val = $(e.target).prop('checked')
|
||||||
|
|
|
@ -85,9 +85,9 @@ class App.Search extends App.Controller
|
||||||
|
|
||||||
@tabs = []
|
@tabs = []
|
||||||
for model in App.Config.get('models_searchable')
|
for model in App.Config.get('models_searchable')
|
||||||
model = model.replace(/::/, '')
|
model = model.replace(/::/g, '')
|
||||||
tab =
|
tab =
|
||||||
name: model
|
name: App[model]?.display_name || model
|
||||||
model: model
|
model: model
|
||||||
count: 0
|
count: 0
|
||||||
active: false
|
active: false
|
||||||
|
|
|
@ -131,6 +131,9 @@ class ArticleViewItem extends App.ObserverController
|
||||||
attachments = App.TicketArticle.contentAttachments(article)
|
attachments = App.TicketArticle.contentAttachments(article)
|
||||||
if article.attachments
|
if article.attachments
|
||||||
for attachment in article.attachments
|
for attachment in article.attachments
|
||||||
|
attachment.url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?disposition=attachment"
|
||||||
|
attachment.preview_url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?view=preview"
|
||||||
|
|
||||||
if attachment && attachment.preferences && attachment.preferences['original-format'] is true
|
if attachment && attachment.preferences && attachment.preferences['original-format'] is true
|
||||||
link =
|
link =
|
||||||
url: "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?disposition=attachment"
|
url: "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?disposition=attachment"
|
||||||
|
@ -192,7 +195,7 @@ class ArticleViewItem extends App.ObserverController
|
||||||
@html App.view('ticket_zoom/article_view')(
|
@html App.view('ticket_zoom/article_view')(
|
||||||
ticket: @ticket
|
ticket: @ticket
|
||||||
article: article
|
article: article
|
||||||
attachments: attachments
|
attachments: App.view('generic/attachments')(attachments: attachments)
|
||||||
links: links
|
links: links
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,19 @@ class Edit extends App.ObserverController
|
||||||
)
|
)
|
||||||
|
|
||||||
class SidebarTicket extends App.Controller
|
class SidebarTicket extends App.Controller
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@bind 'config_update_local', (data) => @configUpdated(data)
|
||||||
|
|
||||||
|
configUpdated: (data) ->
|
||||||
|
if data.name != 'kb_active'
|
||||||
|
return
|
||||||
|
|
||||||
|
if data.value
|
||||||
|
return
|
||||||
|
|
||||||
|
@editTicket(@el)
|
||||||
|
|
||||||
sidebarItem: =>
|
sidebarItem: =>
|
||||||
@item = {
|
@item = {
|
||||||
name: 'ticket'
|
name: 'ticket'
|
||||||
|
@ -96,6 +109,9 @@ class SidebarTicket extends App.Controller
|
||||||
if @linkWidget && args.links
|
if @linkWidget && args.links
|
||||||
@linkWidget.reload(args.links)
|
@linkWidget.reload(args.links)
|
||||||
|
|
||||||
|
if @linkKbAnswerWidget && args.links
|
||||||
|
@linkKbAnswerWidget.reload(args.links)
|
||||||
|
|
||||||
editTicket: (el) =>
|
editTicket: (el) =>
|
||||||
@el = el
|
@el = el
|
||||||
localEl = $(App.view('ticket_zoom/sidebar_ticket')())
|
localEl = $(App.view('ticket_zoom/sidebar_ticket')())
|
||||||
|
@ -121,6 +137,15 @@ class SidebarTicket extends App.Controller
|
||||||
object: @ticket
|
object: @ticket
|
||||||
links: @links
|
links: @links
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if @permissionCheck('knowledge_base.*') and App.Config.get('kb_active')
|
||||||
|
@linkKbAnswerWidget = new App.WidgetLinkKbAnswer(
|
||||||
|
el: localEl.filter('.link_kb_answers')
|
||||||
|
object_type: 'Ticket'
|
||||||
|
object: @ticket
|
||||||
|
links: @links
|
||||||
|
)
|
||||||
|
|
||||||
@timeUnitWidget = new App.TicketZoomTimeUnit(
|
@timeUnitWidget = new App.TicketZoomTimeUnit(
|
||||||
el: localEl.filter('.js-timeUnit')
|
el: localEl.filter('.js-timeUnit')
|
||||||
object_id: @ticket.id
|
object_id: @ticket.id
|
||||||
|
|
|
@ -61,8 +61,8 @@ class Index extends App.ControllerSubContent
|
||||||
display: 'Action'
|
display: 'Action'
|
||||||
className: 'actionCell'
|
className: 'actionCell'
|
||||||
translation: true
|
translation: true
|
||||||
width: '200px'
|
width: '250px'
|
||||||
displayWidth: 200
|
displayWidth: 250
|
||||||
unresizable: true
|
unresizable: true
|
||||||
header.push attribute
|
header.push attribute
|
||||||
header
|
header
|
||||||
|
@ -70,7 +70,7 @@ class Index extends App.ControllerSubContent
|
||||||
callbackAttributes = (value, object, attribute, header) ->
|
callbackAttributes = (value, object, attribute, header) ->
|
||||||
text = App.i18n.translateInline('View from user\'s perspective')
|
text = App.i18n.translateInline('View from user\'s perspective')
|
||||||
value = ' '
|
value = ' '
|
||||||
attribute.raw = ' <span class="btn btn--primary btn--table switchView" title="' + text + '">' + App.Utils.icon('switchView') + text + '</span>'
|
attribute.raw = ' <span class="btn btn--primary btn--small btn--slim switchView" title="' + text + '">' + App.Utils.icon('switchView') + '<span>' + text + '</span></span>'
|
||||||
attribute.class = ''
|
attribute.class = ''
|
||||||
attribute.parentClass = 'actionCell no-padding'
|
attribute.parentClass = 'actionCell no-padding'
|
||||||
attribute.link = ''
|
attribute.link = ''
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
class App.AnswerList extends App.Controller
|
||||||
|
@extend App.PopoverProvidable
|
||||||
|
@registerPopovers 'Organization', 'User'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
|
||||||
|
openTicket = (id,e) =>
|
||||||
|
ticket = App.Ticket.findNative(id)
|
||||||
|
@navigate ticket.uiUrl()
|
||||||
|
callbackTicketTitleAdd = (value, object, attribute, attributes) ->
|
||||||
|
attribute.title = object.title
|
||||||
|
value
|
||||||
|
'1111'
|
||||||
|
callbackLinkToTicket = (value, object, attribute, attributes) ->
|
||||||
|
attribute.link = object.uiUrl()
|
||||||
|
value
|
||||||
|
'22222'
|
||||||
|
callbackUserPopover = (value, object, attribute, attributes) ->
|
||||||
|
return value if !object
|
||||||
|
refObjectId = undefined
|
||||||
|
if attribute.name is 'customer_id'
|
||||||
|
refObjectId = object.customer_id
|
||||||
|
if attribute.name is 'owner_id'
|
||||||
|
refObjectId = object.owner_id
|
||||||
|
return value if !refObjectId
|
||||||
|
attribute.class = 'user-popover'
|
||||||
|
attribute.data =
|
||||||
|
id: refObjectId
|
||||||
|
value
|
||||||
|
callbackOrganizationPopover = (value, object, attribute, attributes) ->
|
||||||
|
return value if !object
|
||||||
|
return value if !object.organization_id
|
||||||
|
attribute.class = 'organization-popover'
|
||||||
|
attribute.data =
|
||||||
|
id: object.organization_id
|
||||||
|
value
|
||||||
|
|
||||||
|
callbackIconHeader = (headers) ->
|
||||||
|
attribute =
|
||||||
|
name: 'icon'
|
||||||
|
display: ''
|
||||||
|
translation: false
|
||||||
|
width: '28px'
|
||||||
|
displayWidth:28
|
||||||
|
unresizable: true
|
||||||
|
headers.unshift(0)
|
||||||
|
headers[0] = attribute
|
||||||
|
headers
|
||||||
|
|
||||||
|
callbackIcon = (value, object, attribute, header) ->
|
||||||
|
value = ' '
|
||||||
|
attribute.class = object.iconClass()
|
||||||
|
attribute.link = ''
|
||||||
|
attribute.title = object.iconTitle()
|
||||||
|
value
|
||||||
|
|
||||||
|
list = []
|
||||||
|
for ticket_id in @ticket_ids
|
||||||
|
ticketItem = App.KnowledgeBaseAnswer.fullLocal(ticket_id)
|
||||||
|
list.push ticketItem
|
||||||
|
@el.html('')
|
||||||
|
new App.ControllerTable(
|
||||||
|
tableId: @tableId
|
||||||
|
el: @el
|
||||||
|
overview: @columns || [ 'id', 'translation::title', 'customer', 'group', 'created_at' ]
|
||||||
|
model: App.KnowledgeBaseAnswer
|
||||||
|
objects: list
|
||||||
|
#bindRow:
|
||||||
|
# events:
|
||||||
|
# 'click': openTicket
|
||||||
|
callbackHeader: [ callbackIconHeader ]
|
||||||
|
callbackAttributes:
|
||||||
|
#icon:
|
||||||
|
#[ callbackIcon ]
|
||||||
|
#customer_id:
|
||||||
|
#[ callbackUserPopover ]
|
||||||
|
organization_id:
|
||||||
|
[ callbackOrganizationPopover ]
|
||||||
|
owner_id:
|
||||||
|
[ callbackUserPopover ]
|
||||||
|
title:
|
||||||
|
[ callbackLinkToTicket, callbackTicketTitleAdd ]
|
||||||
|
number:
|
||||||
|
[ callbackLinkToTicket, callbackTicketTitleAdd ]
|
||||||
|
radio: @radio
|
||||||
|
)
|
||||||
|
|
||||||
|
@renderPopovers()
|
|
@ -0,0 +1,31 @@
|
||||||
|
class App.WidgetButtonWithDropdown extends App.Controller
|
||||||
|
elements:
|
||||||
|
'.dropdown-menu-accessories': 'accessoriesContainer'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click li': 'clickedOption'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
mainActionLabel: 'Submit'
|
||||||
|
mainActionIdentifier: 'js-submit'
|
||||||
|
accessoryActionsIdentifier: 'js-submit-action'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@el.addClass 'buttonDropdown dropdown dropup'
|
||||||
|
|
||||||
|
@html App.view('widget/button_with_dropdown')(
|
||||||
|
mainActionIdentifier: @mainActionIdentifier
|
||||||
|
accessoryActionsIdentifier: @accessoryActionsIdentifier
|
||||||
|
mainActionLabel: @mainActionLabel
|
||||||
|
actions: @actions || []
|
||||||
|
)
|
||||||
|
|
||||||
|
clickedOption: (e) ->
|
||||||
|
if e.currentTarget.hasAttribute('disabled')
|
||||||
|
@preventDefaultAndStopPropagation(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
@accessoriesContainer.blur()
|
|
@ -0,0 +1,105 @@
|
||||||
|
class App.WidgetLinkKbAnswer extends App.WidgetLink
|
||||||
|
@registerPopovers 'KnowledgeBaseAnswer'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-add': 'addButton'
|
||||||
|
'.searchableSelect': 'searchableSelect'
|
||||||
|
'.js-shadow': 'shadowField'
|
||||||
|
'.js-input': 'inputField'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'change .js-shadow': 'didSubmit'
|
||||||
|
'blur .js-input': 'didBlur'
|
||||||
|
|
||||||
|
getAjaxAttributes: (field, attributes) ->
|
||||||
|
@apiPath = App.Config.get('api_path')
|
||||||
|
|
||||||
|
attributes.type = 'POST'
|
||||||
|
attributes.url = "#{@apiPath}/knowledge_bases/search"
|
||||||
|
attributes.data.flavor = 'agent'
|
||||||
|
attributes.data.include_locale = true
|
||||||
|
attributes.data.index = 'KnowledgeBase::Answer::Translation'
|
||||||
|
attributes.data.highlight_enabled = false
|
||||||
|
|
||||||
|
attributes.data = JSON.stringify(attributes.data)
|
||||||
|
|
||||||
|
attributes
|
||||||
|
|
||||||
|
linksForRendering: ->
|
||||||
|
@localLinks
|
||||||
|
.map (elem) ->
|
||||||
|
switch elem.link_object
|
||||||
|
when 'KnowledgeBase::Answer::Translation'
|
||||||
|
if translation = App.KnowledgeBaseAnswerTranslation.fullLocal( elem.link_object_value )
|
||||||
|
title: translation.title
|
||||||
|
id: translation.id
|
||||||
|
url: translation.uiUrl()
|
||||||
|
.filter (elem) ->
|
||||||
|
elem?
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('link/kb_answer')(
|
||||||
|
list: @linksForRendering()
|
||||||
|
)
|
||||||
|
|
||||||
|
@renderPopovers()
|
||||||
|
|
||||||
|
@el.append(new App.SearchableAjaxSelect(
|
||||||
|
delegate: @
|
||||||
|
useAjaxDetails: true
|
||||||
|
attribute:
|
||||||
|
id: 'link_kb_answer'
|
||||||
|
name: 'input'
|
||||||
|
placeholder: App.i18n.translateInline('Search...')
|
||||||
|
limit: 40
|
||||||
|
object: 'KnowledgeBaseAnswerTranslation'
|
||||||
|
ajax: true
|
||||||
|
).element())
|
||||||
|
|
||||||
|
@refreshElements()
|
||||||
|
@searchableSelect.addClass('hidden')
|
||||||
|
|
||||||
|
didSubmit: =>
|
||||||
|
@clearDelay('hideField')
|
||||||
|
@inputField.attr('disabled', true)
|
||||||
|
@saveToServer(@shadowField.val())
|
||||||
|
|
||||||
|
didBlur: (e) =>
|
||||||
|
@delay( =>
|
||||||
|
@setInputVisible(false)
|
||||||
|
, 200, 'hideField')
|
||||||
|
|
||||||
|
add: ->
|
||||||
|
@shadowField.val('')
|
||||||
|
@inputField.attr('disabled', false).val('')
|
||||||
|
|
||||||
|
@setInputVisible(true)
|
||||||
|
@inputField.focus()
|
||||||
|
|
||||||
|
setInputVisible: (setInputVisible) ->
|
||||||
|
@searchableSelect.toggleClass('hidden', !setInputVisible)
|
||||||
|
@addButton.toggleClass('hidden', setInputVisible)
|
||||||
|
|
||||||
|
saveToServer: (id) ->
|
||||||
|
@ajax(
|
||||||
|
id: "links_add_#{@object.id}_#{@object_type}"
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/links/add"
|
||||||
|
data:
|
||||||
|
link_type: 'normal'
|
||||||
|
link_object_target: 'Ticket'
|
||||||
|
link_object_target_value: @object.id
|
||||||
|
link_object_source: 'KnowledgeBase::Answer::Translation'
|
||||||
|
link_object_source_number: id
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@fetch()
|
||||||
|
@setInputVisible(false)
|
||||||
|
error: (xhr, statusText, error) =>
|
||||||
|
@setInputVisible(false)
|
||||||
|
@notify(
|
||||||
|
type: 'error'
|
||||||
|
msg: App.i18n.translateContent(xhr.responseJSON?.error || "Couldn't save changes")
|
||||||
|
removeAll: true
|
||||||
|
)
|
||||||
|
)
|
|
@ -28,7 +28,7 @@ class App.Track
|
||||||
class _trackSingleton
|
class _trackSingleton
|
||||||
constructor: ->
|
constructor: ->
|
||||||
@trackId = "track-#{new Date().getTime()}-#{Math.floor(Math.random() * 99999)}"
|
@trackId = "track-#{new Date().getTime()}-#{Math.floor(Math.random() * 99999)}"
|
||||||
@browser = App.Browser.detection()
|
@browser = App.Browser.detection() if App.Browser
|
||||||
@data = []
|
@data = []
|
||||||
# @url = 'http://localhost:3005/api/v1/ui'
|
# @url = 'http://localhost:3005/api/v1/ui'
|
||||||
@url = 'https://log.zammad.com/api/v1/ui'
|
@url = 'https://log.zammad.com/api/v1/ui'
|
||||||
|
|
140
app/assets/javascripts/app/lib/app_post/color.coffee
Normal file
140
app/assets/javascripts/app/lib/app_post/color.coffee
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.Color extends Spine.Controller
|
||||||
|
hsl: undefined
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-input': 'input'
|
||||||
|
'.js-shadow': 'shadow'
|
||||||
|
'.js-swatch': 'swatch'
|
||||||
|
'.js-colorpicker-hue-saturation': 'hueSaturation'
|
||||||
|
'.js-colorpicker-lightness-plane': 'lightnessPlane'
|
||||||
|
'.js-colorpicker-saturation-gradient': 'saturationGradient'
|
||||||
|
'.js-colorpicker-circle': 'circle'
|
||||||
|
'.js-colorpicker-lightness': 'lightness'
|
||||||
|
'.js-colorpicker-hue-plane': 'huePlane'
|
||||||
|
'.js-colorpicker-slider': 'slider'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'input .js-input': 'onInput'
|
||||||
|
'mousedown .js-colorpicker-hue-saturation': 'onHueSaturationMousedown'
|
||||||
|
'mousedown .js-colorpicker-lightness': 'onLightnessMousedown'
|
||||||
|
'click .js-dropdown': 'stopPropagation'
|
||||||
|
|
||||||
|
stopPropagation: (event) ->
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
element: =>
|
||||||
|
@el
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@hsl = @rgbToHsl(@parseColor(@attribute.value))
|
||||||
|
@html App.view('generic/color')
|
||||||
|
attribute: @attribute
|
||||||
|
hsl: @hsl
|
||||||
|
|
||||||
|
onInput: ->
|
||||||
|
@update @input.val()
|
||||||
|
@output()
|
||||||
|
|
||||||
|
update: (color) ->
|
||||||
|
@updateSwatch(color)
|
||||||
|
@hsl = @rgbToHsl(@parseColor(color))
|
||||||
|
@renderPicker()
|
||||||
|
|
||||||
|
updateSwatch: (color) ->
|
||||||
|
@swatch.css 'background-color', ''
|
||||||
|
@swatch.css 'background-color', color
|
||||||
|
|
||||||
|
output: ->
|
||||||
|
hslString = @hslString(@hsl)
|
||||||
|
@input.val hslString
|
||||||
|
@updateSwatch hslString
|
||||||
|
@shadow.val @rgbToHex(@parseColor(hslString))
|
||||||
|
|
||||||
|
componentToHex: (c) ->
|
||||||
|
hex = c.toString(16)
|
||||||
|
if hex.length == 1 then '0' + hex else hex
|
||||||
|
|
||||||
|
rgbToHex: (rgba) ->
|
||||||
|
'#' + @componentToHex(rgba[0]) + @componentToHex(rgba[1]) + @componentToHex(rgba[2])
|
||||||
|
|
||||||
|
parseColor: (color) ->
|
||||||
|
canvas = document.createElement('canvas')
|
||||||
|
canvas.width = canvas.height = 1
|
||||||
|
ctx = canvas.getContext('2d')
|
||||||
|
ctx.clearRect(0, 0, 1, 1)
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.fillRect(0, 0, 1, 1)
|
||||||
|
ctx.getImageData(0, 0, 1, 1).data
|
||||||
|
|
||||||
|
rgbToHsl: (rgb) ->
|
||||||
|
return [0, 0, 0] if !rgb
|
||||||
|
|
||||||
|
r = rgb[0] / 255
|
||||||
|
g = rgb[1] / 255
|
||||||
|
b = rgb[2] / 255
|
||||||
|
|
||||||
|
max = Math.max(r, g, b)
|
||||||
|
min = Math.min(r, g, b)
|
||||||
|
l = (max + min) / 2
|
||||||
|
|
||||||
|
if (max == min)
|
||||||
|
h = s = 0 # achromatic
|
||||||
|
else
|
||||||
|
d = max - min
|
||||||
|
s = if l > 0.5 then d / (2 - max - min) else d / (max + min)
|
||||||
|
|
||||||
|
h = switch
|
||||||
|
when r is max then (g - b) / d + (g < b ? 6 : 0)
|
||||||
|
when g is max then (b - r) / d + 2
|
||||||
|
when b is max then (r - g) / d + 4
|
||||||
|
|
||||||
|
h /= 6
|
||||||
|
|
||||||
|
[h, s, l]
|
||||||
|
|
||||||
|
hslString: ->
|
||||||
|
"hsl(#{Math.round(360 * @hsl[0])},#{Math.round(100 * @hsl[1])}%,#{Math.round(100 * @hsl[2])}%)"
|
||||||
|
|
||||||
|
onHueSaturationMousedown: (event) ->
|
||||||
|
@offset = @hueSaturation.offset()
|
||||||
|
$(document).on 'mousemove.colorpicker', @onHueSaturationMousemove
|
||||||
|
$(document).on 'mouseup.colorpicker', @onMouseup
|
||||||
|
@onHueSaturationMousemove(event)
|
||||||
|
|
||||||
|
onHueSaturationMousemove: (event) =>
|
||||||
|
@hsl[0] = Math.max(0, Math.min(1, (event.pageX - @offset.left)/@hueSaturation.width()))
|
||||||
|
@hsl[1] = Math.max(0, Math.min(1, 1-(event.pageY - @offset.top)/@hueSaturation.height()))
|
||||||
|
@renderPicker()
|
||||||
|
@output()
|
||||||
|
|
||||||
|
onLightnessMousedown: (event) ->
|
||||||
|
@offset = @lightness.offset()
|
||||||
|
$(document).on 'mousemove.colorpicker', @onLightnessMousemove
|
||||||
|
$(document).on 'mouseup.colorpicker', @onMouseup
|
||||||
|
@onLightnessMousemove(event)
|
||||||
|
|
||||||
|
onLightnessMousemove: (event) =>
|
||||||
|
@hsl[2] = Math.max(0, Math.min(1, 1-(event.pageY - @offset.top)/@lightness.height()))
|
||||||
|
@renderPicker()
|
||||||
|
@output()
|
||||||
|
|
||||||
|
onMouseup: ->
|
||||||
|
$(document).off 'mousemove.colorpicker'
|
||||||
|
$(document).off 'mouseup.colorpicker'
|
||||||
|
|
||||||
|
renderPicker: ->
|
||||||
|
@lightnessPlane.css 'background-color': "hsla(0,0%,#{if @hsl[2] > 0.5 then 100 else 0}%,#{2*Math.abs(@hsl[2]-0.5)})"
|
||||||
|
@saturationGradient.css 'background-image': "linear-gradient(transparent, hsl(0, 0%, #{@hsl[2]*100}%))"
|
||||||
|
@circle.css
|
||||||
|
left: @hsl[0]*100 +'%'
|
||||||
|
top: 100 - @hsl[1]*100 +'%'
|
||||||
|
borderColor: if @hsl[2] > 0.5 then 'black' else 'white'
|
||||||
|
@huePlane.css 'background-color': "hsl(#{@hsl[0]*360}, 100%, 50%)"
|
||||||
|
@slider.css top: 100 - @hsl[2]*100 +'%'
|
||||||
|
|
||||||
|
|
154
app/assets/javascripts/app/lib/app_post/icon_picker.coffee
Normal file
154
app/assets/javascripts/app/lib/app_post/icon_picker.coffee
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.IconPicker extends Spine.Controller
|
||||||
|
library: null
|
||||||
|
empty: false
|
||||||
|
columns: 8
|
||||||
|
currentItem: null
|
||||||
|
|
||||||
|
events:
|
||||||
|
'focus .js-input': 'onFocus'
|
||||||
|
'input .js-filter-icons': 'filterIcons'
|
||||||
|
'click .js-filter-icons': 'stopPropagation'
|
||||||
|
'click .js-pick': 'onIconClick'
|
||||||
|
'mouseenter .js-pick': 'highlightItem'
|
||||||
|
'shown.bs.dropdown': 'onPickerOpen'
|
||||||
|
'hidden.bs.dropdown': 'onPickerClose'
|
||||||
|
'focus .js-shadow': 'onShadowFocus'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-iconGrid': 'iconGrid'
|
||||||
|
'.js-noMatch': 'noMatch'
|
||||||
|
'.js-shadow': 'shadow'
|
||||||
|
'.js-input': 'input'
|
||||||
|
'.js-filter-icons': 'filter'
|
||||||
|
'.js-pick': 'icons'
|
||||||
|
|
||||||
|
stopPropagation: (event) ->
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@throttledRenderIcons = _.throttle(@renderIcons, 300)
|
||||||
|
@render()
|
||||||
|
App.Utils.loadIconFont(@attribute.iconset)
|
||||||
|
App.Utils.loadIconFontInfo @attribute.iconset, (icons) =>
|
||||||
|
@library = icons
|
||||||
|
@renderIcons()
|
||||||
|
|
||||||
|
element: =>
|
||||||
|
@el
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
attributeValue = @attribute.value
|
||||||
|
@html App.view('generic/icon_picker')
|
||||||
|
attribute: @attribute
|
||||||
|
value: @attribute.value
|
||||||
|
|
||||||
|
renderIcons: (filter) =>
|
||||||
|
fragment = document.createDocumentFragment()
|
||||||
|
regex = new RegExp(filter, 'i') if filter
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
_.each @library, (icon) =>
|
||||||
|
if !filter || filter && (regex.test(icon.name) || icon.filter && _.some(icon.filter, (w) -> regex.test(w)))
|
||||||
|
count++
|
||||||
|
fragment.appendChild $("<li class=\"icon js-pick\" data-font=\"#{@attribute.iconset}\" data-unicode=\"#{icon.unicode}\">#{String.fromCharCode('0x'+ icon.unicode)}</li>").get(0)
|
||||||
|
|
||||||
|
if count
|
||||||
|
@iconGrid.html fragment
|
||||||
|
@empty = false
|
||||||
|
@refreshElements()
|
||||||
|
else
|
||||||
|
if not @empty
|
||||||
|
# show a random placeholder
|
||||||
|
next = Math.floor(Math.random() * @noMatch.length)
|
||||||
|
if next == @noMatch.filter('.is-active').index()
|
||||||
|
next = (next + 1) % @noMatch.length
|
||||||
|
@noMatch.removeClass('is-active').eq(next).addClass('is-active')
|
||||||
|
@empty = true
|
||||||
|
@iconGrid.empty()
|
||||||
|
|
||||||
|
filterIcons: (event) =>
|
||||||
|
@throttledRenderIcons event.currentTarget.value
|
||||||
|
|
||||||
|
onIconClick: (event) ->
|
||||||
|
@pick event.currentTarget.getAttribute('data-unicode')
|
||||||
|
|
||||||
|
pick: (unicode) ->
|
||||||
|
@shadow.val unicode
|
||||||
|
@input.text String.fromCharCode("0x#{unicode}")
|
||||||
|
@el.closest('form').trigger('input')
|
||||||
|
|
||||||
|
# propergate focus to our visible input
|
||||||
|
onShadowFocus: ->
|
||||||
|
@input.focus()
|
||||||
|
|
||||||
|
onPickerOpen: ->
|
||||||
|
@filter.focus()
|
||||||
|
@isOpen = true
|
||||||
|
|
||||||
|
onPickerClose: ->
|
||||||
|
@isOpen = false
|
||||||
|
@filter.val ''
|
||||||
|
@renderIcons()
|
||||||
|
$(document).off 'keydown.icon_picker'
|
||||||
|
|
||||||
|
onFocus: ->
|
||||||
|
$(document).on 'keydown.icon_picker', @navigate
|
||||||
|
|
||||||
|
navigate: (event) =>
|
||||||
|
switch event.keyCode
|
||||||
|
when 40 then @nudge event, 0, 1 # down
|
||||||
|
when 38 then @nudge event, 0, -1 # up
|
||||||
|
when 39 then @nudge event, 1 # right
|
||||||
|
when 37 then @nudge event, -1 # left
|
||||||
|
when 13 then @onEnter event
|
||||||
|
when 27 then @onEscape()
|
||||||
|
|
||||||
|
onEscape: ->
|
||||||
|
@currentItem = null
|
||||||
|
@toggle() if @isOpen
|
||||||
|
|
||||||
|
onEnter: (event) ->
|
||||||
|
if !@isOpen
|
||||||
|
return @toggle()
|
||||||
|
if @currentItem
|
||||||
|
@pick @currentItem.attr('data-unicode')
|
||||||
|
@toggle()
|
||||||
|
|
||||||
|
toggle: ->
|
||||||
|
@$('[data-toggle="dropdown"]').dropdown('toggle')
|
||||||
|
|
||||||
|
nudge: (event, x, y) ->
|
||||||
|
event.preventDefault()
|
||||||
|
if !@currentItem
|
||||||
|
selectedIndex = 0
|
||||||
|
else
|
||||||
|
selectedIndex = @currentItem.index()
|
||||||
|
|
||||||
|
distance = switch
|
||||||
|
when x > 0 then 1
|
||||||
|
when x < 0 then -1
|
||||||
|
when y > 0 then @columns
|
||||||
|
when y < 0 then -@columns
|
||||||
|
|
||||||
|
if selectedIndex + distance >= @icons.length or selectedIndex + distance < 0
|
||||||
|
# out of boundary
|
||||||
|
return
|
||||||
|
|
||||||
|
selectedIndex += distance
|
||||||
|
@unhighlightCurrentItem()
|
||||||
|
|
||||||
|
@currentItem = @icons.eq(selectedIndex)
|
||||||
|
@currentItem.addClass('is-active').get(0).scrollIntoView(behavior: 'instant')
|
||||||
|
|
||||||
|
highlightItem: (event) =>
|
||||||
|
@unhighlightCurrentItem()
|
||||||
|
@currentItem = $(event.currentTarget)
|
||||||
|
@currentItem.addClass('is-active')
|
||||||
|
|
||||||
|
unhighlightCurrentItem: ->
|
||||||
|
return if !@currentItem
|
||||||
|
@currentItem.removeClass('is-active')
|
||||||
|
@currentItem = null
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.IconsetPicker extends Spine.Controller
|
||||||
|
sets:
|
||||||
|
FontAwesome:
|
||||||
|
name: 'Font Awesome'
|
||||||
|
version: '4.7'
|
||||||
|
website: 'https://fontawesome.com/v4.7.0/'
|
||||||
|
anticon:
|
||||||
|
name: 'Anticon'
|
||||||
|
version: '2.10'
|
||||||
|
website: 'https://2x.ant.design/components/icon/'
|
||||||
|
material:
|
||||||
|
name: 'Material'
|
||||||
|
version: '2.2.0'
|
||||||
|
website: 'https://material.io/icons/'
|
||||||
|
ionicons:
|
||||||
|
name: 'Ionicons'
|
||||||
|
version: '2.0.1'
|
||||||
|
website: 'https://ionicons.com/v2/'
|
||||||
|
'Simple-Line-Icons':
|
||||||
|
name: 'Simple Line Icons'
|
||||||
|
version: '0.0.1'
|
||||||
|
website: 'https://simplelineicons.github.io/'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-set': 'setElements'
|
||||||
|
'input': 'input'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .js-set': 'pick'
|
||||||
|
# 'mouseenter .icon': 'flip'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
element: =>
|
||||||
|
@el
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('generic/iconset_picker')
|
||||||
|
attribute: @attribute
|
||||||
|
sets: @sets
|
||||||
|
|
||||||
|
for family, set of @sets
|
||||||
|
App.Utils.loadIconFont(family)
|
||||||
|
App.Utils.loadIconFontInfo family, @initializePreview.bind(@, family)
|
||||||
|
|
||||||
|
initializePreview: (family, icons) ->
|
||||||
|
@sets[family].icons = icons
|
||||||
|
@renderPreview(family, icons)
|
||||||
|
|
||||||
|
renderPreview: (family) ->
|
||||||
|
fragment = document.createDocumentFragment()
|
||||||
|
icons = _.shuffle(@sets[family].icons)
|
||||||
|
|
||||||
|
for i in [0..(11*5-1)]
|
||||||
|
fragment.appendChild $("<i class=\"icon\" data-font=\"#{family}\">#{String.fromCharCode('0x'+ icons[i].unicode)}</i>").get(0)
|
||||||
|
|
||||||
|
@el.find("[data-family=\"#{family}\"] .js-preview").html fragment
|
||||||
|
|
||||||
|
pick: (event) ->
|
||||||
|
family = $(event.currentTarget).attr('data-family')
|
||||||
|
@input.val family
|
||||||
|
@setElements.removeClass('is-active')
|
||||||
|
event.currentTarget.classList.add('is-active')
|
||||||
|
|
||||||
|
flip: (event) ->
|
||||||
|
$icon = $(event.currentTarget)
|
||||||
|
family = $icon.closest('.js-set').attr('data-family')
|
||||||
|
|
||||||
|
if $icon.hasClass('do-flash')
|
||||||
|
$icon.removeClass('do-flash')
|
||||||
|
# force redraw
|
||||||
|
$icon.get(0).offsetWidth
|
||||||
|
|
||||||
|
$icon.text String.fromCharCode('0x'+ _.sample(@sets[family].icons).unicode)
|
||||||
|
$icon.addClass('do-flash')
|
95
app/assets/javascripts/app/lib/app_post/multi_locales.coffee
Normal file
95
app/assets/javascripts/app/lib/app_post/multi_locales.coffee
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
class App.MultiLocales extends App.Controller
|
||||||
|
events:
|
||||||
|
'click .js-remove': 'remove'
|
||||||
|
'click .js-primary': 'primary'
|
||||||
|
'change .js-shadow': 'changeOnRow'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@multiple_rows_supported = App.Config.get('kb_multi_lingual_support')
|
||||||
|
@rows = []
|
||||||
|
@render()
|
||||||
|
|
||||||
|
if @object
|
||||||
|
@listenTo @object, 'refresh', @parentObjectUpdated
|
||||||
|
|
||||||
|
parentObjectUpdated: =>
|
||||||
|
App.Delay.set =>
|
||||||
|
@attribute.value = @object.attributes()[@attribute.name]
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('generic/multi_locales')(attribute: @attribute, vc: @)
|
||||||
|
|
||||||
|
if Array.isArray(@attribute.value)
|
||||||
|
for locale in @attribute.value
|
||||||
|
@appendRow @renderRow(locale, @attribute.value.length == 1)
|
||||||
|
|
||||||
|
if @multiple_rows_supported || !Array.isArray(@attribute.value) || @attribute.value.length == 0
|
||||||
|
@appendRow @renderRow()
|
||||||
|
|
||||||
|
renderRow: (kb_locale_attributes, solo = false) ->
|
||||||
|
kb_locale = App.KnowledgeBaseLocale.find kb_locale_attributes?.id
|
||||||
|
|
||||||
|
new App.MultiLocalesRow(
|
||||||
|
attribute: @attribute
|
||||||
|
kb_locale: kb_locale
|
||||||
|
available_locales: @selectableLocales(kb_locale?.systemLocale()?.id)
|
||||||
|
solo: solo
|
||||||
|
)
|
||||||
|
|
||||||
|
selectableLocales: (self_value) ->
|
||||||
|
takenCodes = @$('.js-shadow')
|
||||||
|
.toArray()
|
||||||
|
.map (elem) -> $(elem).val()
|
||||||
|
.filter (elem) -> elem && elem != self_value
|
||||||
|
|
||||||
|
App.Locale.all().filter (elem) ->
|
||||||
|
!takenCodes.includes(String(elem.id))
|
||||||
|
|
||||||
|
remove: (e) ->
|
||||||
|
domRow = $(e.currentTarget).closest('tr')[0]
|
||||||
|
row = _.find @rows, (elem) -> elem.el[0] == domRow
|
||||||
|
|
||||||
|
if row?.primaryCheckbox.prop('checked')
|
||||||
|
return
|
||||||
|
else if row?.kb_locale?.id
|
||||||
|
row.toggleDelete()
|
||||||
|
else
|
||||||
|
row.el.remove()
|
||||||
|
@rows.splice @rows.indexOf(row), 1
|
||||||
|
|
||||||
|
@changeOnRow()
|
||||||
|
|
||||||
|
primary: (e) ->
|
||||||
|
input = $(e.currentTarget).find('input')
|
||||||
|
|
||||||
|
if input.attr('disabled')
|
||||||
|
return
|
||||||
|
|
||||||
|
input.prop('checked', true)
|
||||||
|
|
||||||
|
@changeOnRow()
|
||||||
|
|
||||||
|
changeOnRow: (e) ->
|
||||||
|
if !@hasEmptyRow() && @multiple_rows_supported
|
||||||
|
@appendRow @renderRow()
|
||||||
|
|
||||||
|
nonempty_rows = @rows.filter (row) -> row.selector.shadowInput.val()
|
||||||
|
|
||||||
|
if nonempty_rows.length == 1
|
||||||
|
nonempty_rows[0].updateButtons(true, true)
|
||||||
|
else
|
||||||
|
for row in nonempty_rows
|
||||||
|
row.updateButtons(false)
|
||||||
|
|
||||||
|
for row in @rows
|
||||||
|
row.updateOptions( @selectableLocales(row.selector.shadowInput.val()) )
|
||||||
|
|
||||||
|
hasEmptyRow: ->
|
||||||
|
@$('.js-shadow').is (i, elem) -> !$(elem).val()
|
||||||
|
|
||||||
|
appendRow: (row) ->
|
||||||
|
@rows.push row
|
||||||
|
@$('tbody').append row.el
|
|
@ -0,0 +1,77 @@
|
||||||
|
class App.MultiLocalesRow extends App.Controller
|
||||||
|
tag: 'tr'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-primary input': 'primaryCheckbox'
|
||||||
|
'.js-remove input': 'removeButton'
|
||||||
|
'.js-selectorContainer': 'selectorContainer'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'change .js-shadow': 'change'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@el.data('kbLocaleId', @kb_locale?.id)
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('generic/multi_locales_row')(
|
||||||
|
attribute: @attribute
|
||||||
|
kb_locale: @kb_locale
|
||||||
|
)
|
||||||
|
|
||||||
|
value = @kb_locale?.systemLocale()?.id
|
||||||
|
|
||||||
|
@_updateButtons(value, @solo , @kb_locale?.primary)
|
||||||
|
|
||||||
|
@selector = @localesSelectBuild(@attribute.name, value, @selectorContainer)
|
||||||
|
@updateOptions(@available_locales)
|
||||||
|
|
||||||
|
localesSelectBuild: (name, value, el) ->
|
||||||
|
new App.SearchableSelect(
|
||||||
|
el: el
|
||||||
|
attribute:
|
||||||
|
name: name
|
||||||
|
value: value
|
||||||
|
null: false
|
||||||
|
placeholder: 'Select locale:'
|
||||||
|
options: [] #formattedLocales
|
||||||
|
class: 'form-control--small'
|
||||||
|
)
|
||||||
|
|
||||||
|
updateOptions: (options) ->
|
||||||
|
value = @selector.shadowInput.val() # @selector.attribute.value
|
||||||
|
|
||||||
|
formattedLocales = options
|
||||||
|
.map (elem) ->
|
||||||
|
{
|
||||||
|
name: elem.name
|
||||||
|
value: elem.id
|
||||||
|
selected: (elem.id + '') == value
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedLocales.sort (a, b) -> a.name.localeCompare(b.name)
|
||||||
|
|
||||||
|
@selector.attribute.options = formattedLocales
|
||||||
|
@selector.render()
|
||||||
|
|
||||||
|
updateButtons: (is_solo, is_primary = undefined) ->
|
||||||
|
if is_primary == undefined
|
||||||
|
is_primary = @primaryCheckbox[0].checked
|
||||||
|
|
||||||
|
@_updateButtons(@selector.shadowInput.val(), is_solo, is_primary)
|
||||||
|
|
||||||
|
_updateButtons: (value, is_solo, is_primary) ->
|
||||||
|
is_deleted = @el.hasClass('settings-list--deleted')
|
||||||
|
|
||||||
|
@removeButton.attr('disabled', is_solo || !value || is_primary)
|
||||||
|
@primaryCheckbox.attr('disabled', is_solo || !value || is_deleted)
|
||||||
|
@primaryCheckbox.prop('checked' , is_primary)
|
||||||
|
|
||||||
|
change: ->
|
||||||
|
@primaryCheckbox.attr 'value', @selector.shadowInput.val()
|
||||||
|
|
||||||
|
toggleDelete: ->
|
||||||
|
@el.toggleClass('settings-list--deleted')
|
||||||
|
@removeButton.prop('checked', @el.hasClass('settings-list--deleted'))
|
||||||
|
@selector.el.toggleClass('u-unclickable')
|
|
@ -0,0 +1,5 @@
|
||||||
|
class App.KbPopoverProvider extends App.SingleObjectPopoverProvider
|
||||||
|
@templateName = 'kb_generic'
|
||||||
|
@includeData = false
|
||||||
|
displayTitleUsing: (object) ->
|
||||||
|
object.title
|
|
@ -0,0 +1,5 @@
|
||||||
|
class KnowledgeBaseAnswer extends App.KbPopoverProvider
|
||||||
|
@klass = App.KnowledgeBaseAnswerTranslation
|
||||||
|
@selectorCssClassPrefix = 'kb-answer'
|
||||||
|
|
||||||
|
App.PopoverProvider.registerProvider('KnowledgeBaseAnswer', KnowledgeBaseAnswer)
|
|
@ -0,0 +1,5 @@
|
||||||
|
class KnowledgeBaseCategory extends App.KbPopoverProvider
|
||||||
|
@klass = App.KnowledgeBaseCategoryTranslation
|
||||||
|
@selectorCssClassPrefix = 'kb-category'
|
||||||
|
|
||||||
|
App.PopoverProvider.registerProvider('KnowledgeBaseCategory', KnowledgeBaseCategory)
|
|
@ -0,0 +1,5 @@
|
||||||
|
class KnowledgeBase extends App.KbPopoverProvider
|
||||||
|
@klass = App.KnowledgeBaseTranslation
|
||||||
|
@selectorCssClassPrefix = 'kb'
|
||||||
|
|
||||||
|
App.PopoverProvider.registerProvider('KnowledgeBase', KnowledgeBase)
|
|
@ -115,7 +115,6 @@ class App.SearchableSelect extends Spine.Controller
|
||||||
onDropdownShown: =>
|
onDropdownShown: =>
|
||||||
@input.on 'click', @stopPropagation
|
@input.on 'click', @stopPropagation
|
||||||
@highlightFirst()
|
@highlightFirst()
|
||||||
$(document).on 'keydown.searchable_select', @navigate
|
|
||||||
if @level > 0
|
if @level > 0
|
||||||
@showSubmenu(@currentMenu)
|
@showSubmenu(@currentMenu)
|
||||||
@isOpen = true
|
@isOpen = true
|
||||||
|
@ -123,7 +122,6 @@ class App.SearchableSelect extends Spine.Controller
|
||||||
onDropdownHidden: =>
|
onDropdownHidden: =>
|
||||||
@input.off 'click', @stopPropagation
|
@input.off 'click', @stopPropagation
|
||||||
@unhighlightCurrentItem()
|
@unhighlightCurrentItem()
|
||||||
$(document).off 'keydown.searchable_select'
|
|
||||||
@isOpen = false
|
@isOpen = false
|
||||||
|
|
||||||
if !@input.val()
|
if !@input.val()
|
||||||
|
@ -359,8 +357,10 @@ class App.SearchableSelect extends Spine.Controller
|
||||||
|
|
||||||
onBlur: ->
|
onBlur: ->
|
||||||
@clearAutocomplete()
|
@clearAutocomplete()
|
||||||
|
@input.off 'keydown.searchable_select'
|
||||||
|
|
||||||
onFocus: ->
|
onFocus: ->
|
||||||
|
@input.on 'keydown.searchable_select', @navigate
|
||||||
textEnd = @input.val().length
|
textEnd = @input.val().length
|
||||||
@input.prop('selectionStart', textEnd)
|
@input.prop('selectionStart', textEnd)
|
||||||
@input.prop('selectionEnd', textEnd)
|
@input.prop('selectionEnd', textEnd)
|
||||||
|
@ -372,8 +372,9 @@ class App.SearchableSelect extends Spine.Controller
|
||||||
onShadowChange: ->
|
onShadowChange: ->
|
||||||
value = @shadowInput.val()
|
value = @shadowInput.val()
|
||||||
|
|
||||||
for option in @attribute.options
|
if Array.isArray(@attribute.options)
|
||||||
option.selected = (option.value + '') == value # makes sure option value is always a string
|
for option in @attribute.options
|
||||||
|
option.selected = (option.value + '') == value # makes sure option value is always a string
|
||||||
|
|
||||||
onInput: (event) =>
|
onInput: (event) =>
|
||||||
@toggle() if not @isOpen
|
@toggle() if not @isOpen
|
||||||
|
|
|
@ -1213,6 +1213,37 @@ class App.Utils
|
||||||
ctx.drawImage(img, 0, 0)
|
ctx.drawImage(img, 0, 0)
|
||||||
canvas.toDataURL('image/png')
|
canvas.toDataURL('image/png')
|
||||||
|
|
||||||
|
# works asynchronously to make sure images are loaded before converting to base64
|
||||||
|
# output is passed to callback
|
||||||
|
@htmlImage2DataUrlAsync: (html, callback) ->
|
||||||
|
output = @_checkTypeOf("<div>#{html}</div>")
|
||||||
|
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
elems = output
|
||||||
|
.find('img')
|
||||||
|
.toArray()
|
||||||
|
.filter (elem) -> !elem.src.match(/^(data|cid):/i)
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
cacheOrDone = ->
|
||||||
|
if (nextElem = elems.pop())
|
||||||
|
App.Utils._htmlImage2DataUrlAsync(nextElem, (data) ->
|
||||||
|
$(nextElem).attr('src', data)
|
||||||
|
cacheOrDone()
|
||||||
|
)
|
||||||
|
else
|
||||||
|
callback(output[0].innerHTML)
|
||||||
|
|
||||||
|
cacheOrDone()
|
||||||
|
|
||||||
|
@_htmlImage2DataUrlAsync: (originalImage, callback) ->
|
||||||
|
imageCache = new Image()
|
||||||
|
imageCache.onload = ->
|
||||||
|
data = App.Utils._htmlImage2DataUrl(originalImage)
|
||||||
|
callback(data)
|
||||||
|
|
||||||
|
imageCache.src = originalImage.src
|
||||||
|
|
||||||
@baseUrl: ->
|
@baseUrl: ->
|
||||||
fqdn = App.Config.get('fqdn')
|
fqdn = App.Config.get('fqdn')
|
||||||
http_type = App.Config.get('http_type')
|
http_type = App.Config.get('http_type')
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
class App.SearchableAjaxSelect extends App.SearchableSelect
|
class App.SearchableAjaxSelect extends App.SearchableSelect
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
# create cache
|
||||||
|
@searchResultCache = {}
|
||||||
|
|
||||||
onInput: (event) =>
|
onInput: (event) =>
|
||||||
super
|
super
|
||||||
|
@ -7,67 +12,62 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
|
||||||
# e.g. Ticket to ticket or AnotherObject to another_object
|
# e.g. Ticket to ticket or AnotherObject to another_object
|
||||||
objectString = underscored(@options.attribute.object)
|
objectString = underscored(@options.attribute.object)
|
||||||
|
|
||||||
# create common accessors
|
query = @input.val()
|
||||||
@apiPath = App.Config.get('api_path')
|
|
||||||
|
|
||||||
# create cache and cache key
|
# create cache key
|
||||||
@searchResultCache = @searchResultCache || {}
|
cacheKey = "#{objectString}+#{query}"
|
||||||
|
|
||||||
cacheKey = "#{objectString}+#{@query}"
|
|
||||||
|
|
||||||
# use cache for search result
|
# use cache for search result
|
||||||
if @searchResultCache[cacheKey]
|
if @searchResultCache[cacheKey]
|
||||||
return @renderResponse( @searchResultCache[cacheKey] )
|
App.Ajax.abort @options.attribute.id
|
||||||
|
@renderResponse @searchResultCache[cacheKey], query
|
||||||
|
return
|
||||||
|
|
||||||
# add timeout for loader icon
|
# add timeout for loader icon
|
||||||
clearTimeout @loaderTimeoutId
|
if !@loaderTimeoutId
|
||||||
@loaderTimeoutId = setTimeout @showLoader, 1000
|
@loaderTimeoutId = setTimeout @showLoader, 1000
|
||||||
|
|
||||||
# start search request and update options
|
attributes =
|
||||||
App.Ajax.request(
|
|
||||||
id: @options.attribute.id
|
id: @options.attribute.id
|
||||||
type: 'GET'
|
type: 'GET'
|
||||||
url: "#{@apiPath}/search/#{objectString}"
|
url: "#{App.Config.get('api_path')}/search/#{objectString}"
|
||||||
data:
|
data:
|
||||||
query: @query
|
query: query
|
||||||
limit: @options.attribute.limit
|
limit: @options.attribute.limit
|
||||||
processData: true
|
processData: true
|
||||||
success: (data, status, xhr) =>
|
success: (data, status, xhr) =>
|
||||||
# cache search result
|
# cache search result
|
||||||
@searchResultCache[cacheKey] = data
|
@searchResultCache[cacheKey] = data
|
||||||
|
|
||||||
@renderResponse(data)
|
@renderResponse(data, query)
|
||||||
)
|
|
||||||
|
|
||||||
renderResponse: (data) =>
|
# if delegate is given and provides getAjaxAttributes method, try to extend ajax call
|
||||||
|
# this is needed for autocompletion field in KB answer-to-answer linking to submit search context
|
||||||
|
if @delegate?.getAjaxAttributes
|
||||||
|
attributes = @delegate?.getAjaxAttributes?(@, attributes)
|
||||||
|
|
||||||
|
# start search request and update options
|
||||||
|
App.Ajax.request(attributes)
|
||||||
|
|
||||||
|
renderResponse: (data, originalQuery) =>
|
||||||
# clear timout and remove loader icon
|
# clear timout and remove loader icon
|
||||||
clearTimeout @loaderTimeoutId
|
clearTimeout @loaderTimeoutId
|
||||||
|
@loaderTimeoutId = undefined
|
||||||
@el.removeClass('is-loading')
|
@el.removeClass('is-loading')
|
||||||
|
|
||||||
# load assets
|
# load assets
|
||||||
App.Collection.loadAssets(data.assets)
|
App.Collection.loadAssets(data.assets)
|
||||||
|
|
||||||
# get options from search result
|
# get options from search result
|
||||||
options = []
|
options = data
|
||||||
for object in data.result
|
.result
|
||||||
if object.type is 'Ticket'
|
.map (elem) =>
|
||||||
ticket = App.Ticket.find(object.id)
|
# use search results directly to avoid loading KB assets in Ticket view
|
||||||
data =
|
if @useAjaxDetails
|
||||||
name: "##{ticket.number} - #{ticket.title}"
|
@renderResponseItemAjax(elem, data)
|
||||||
value: ticket.id
|
else
|
||||||
options.push data
|
@renderResponseItem(elem)
|
||||||
else if object.type is 'User'
|
.filter (elem) -> elem?
|
||||||
user = App.User.find( object.id )
|
|
||||||
data =
|
|
||||||
name: "#{user.displayName()}"
|
|
||||||
value: user.id
|
|
||||||
options.push data
|
|
||||||
else if object.type is 'Organization'
|
|
||||||
organization = App.Organization.find(object.id)
|
|
||||||
data =
|
|
||||||
name: "#{organization.displayName()}"
|
|
||||||
value: organization.id
|
|
||||||
options.push data
|
|
||||||
|
|
||||||
# fill template with gathered options
|
# fill template with gathered options
|
||||||
@optionsList.html @renderOptions options
|
@optionsList.html @renderOptions options
|
||||||
|
@ -76,7 +76,32 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
|
||||||
@refreshElements()
|
@refreshElements()
|
||||||
|
|
||||||
# execute filter
|
# execute filter
|
||||||
@filterByQuery @query
|
@filterByQuery originalQuery
|
||||||
|
|
||||||
|
renderResponseItemAjax: (elem, data) ->
|
||||||
|
result = _.find(data.details, (detailElem) -> detailElem.type == elem.type and detailElem.id == elem.id)
|
||||||
|
|
||||||
|
if result
|
||||||
|
{
|
||||||
|
name: result.title
|
||||||
|
value: elem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResponseItem: (elem) ->
|
||||||
|
object = App[elem.type.replace(/::/g, '')]?.find(elem.id)
|
||||||
|
|
||||||
|
if !object
|
||||||
|
return
|
||||||
|
|
||||||
|
name = if object instanceof App.Ticket
|
||||||
|
"##{object.number} - #{object.title}"
|
||||||
|
else
|
||||||
|
object.displayName()
|
||||||
|
|
||||||
|
{
|
||||||
|
name: name
|
||||||
|
value: object.id
|
||||||
|
}
|
||||||
|
|
||||||
showLoader: =>
|
showLoader: =>
|
||||||
@el.addClass('is-loading')
|
@el.addClass('is-loading')
|
||||||
|
|
|
@ -97,11 +97,15 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
showDropZone = function(dropContainer) {
|
showDropZone = function(dropContainer) {
|
||||||
|
$(dropContainer).trigger('html5Upload.dropZone.show')
|
||||||
|
|
||||||
if ( !$(dropContainer).find('.article-content, .richtext').hasClass('is-dropTarget') ) {
|
if ( !$(dropContainer).find('.article-content, .richtext').hasClass('is-dropTarget') ) {
|
||||||
$(dropContainer).find('.article-content, .richtext').addClass('is-dropTarget')
|
$(dropContainer).find('.article-content, .richtext').addClass('is-dropTarget')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hideDropZone = function(dropContainer) {
|
hideDropZone = function(dropContainer) {
|
||||||
|
$(dropContainer).trigger('html5Upload.dropZone.hide')
|
||||||
|
|
||||||
if ( $(dropContainer).find('.article-content, .richtext').hasClass('is-dropTarget') ) {
|
if ( $(dropContainer).find('.article-content, .richtext').hasClass('is-dropTarget') ) {
|
||||||
$(dropContainer).find('.article-content, .richtext').removeClass('is-dropTarget')
|
$(dropContainer).find('.article-content, .richtext').removeClass('is-dropTarget')
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,18 +68,18 @@
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
var id = this.$widget.find('.dropdown-menu li.is-active').data('id')
|
var elem = this.$widget.find('.dropdown-menu li.is-active')[0]
|
||||||
|
|
||||||
// as fallback use hovered element
|
// as fallback use hovered element
|
||||||
if (!id) {
|
if (!elem) {
|
||||||
id = this.$widget.find('.dropdown-menu li:hover').data('id')
|
elem = this.$widget.find('.dropdown-menu li:hover')[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// as fallback first element
|
// as fallback first element
|
||||||
if (!id) {
|
if (!elem) {
|
||||||
id = this.$widget.find('.dropdown-menu li:first-child').data('id')
|
elem = this.$widget.find('.dropdown-menu li:first-child')[0]
|
||||||
}
|
}
|
||||||
this.take(id)
|
this.take(elem)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,8 +132,9 @@
|
||||||
// backspace
|
// backspace
|
||||||
if (e.keyCode === 8 && this.buffer) {
|
if (e.keyCode === 8 && this.buffer) {
|
||||||
|
|
||||||
|
var trigger = this.findTrigger(this.buffer)
|
||||||
// backspace + buffer === :: -> close textmodule
|
// backspace + buffer === :: -> close textmodule
|
||||||
if (this.buffer === '::') {
|
if (trigger && trigger.trigger === this.buffer) {
|
||||||
this.close(true)
|
this.close(true)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
|
@ -143,7 +144,7 @@
|
||||||
var length = this.buffer.length
|
var length = this.buffer.length
|
||||||
this.buffer = this.buffer.substr(0, length-1)
|
this.buffer = this.buffer.substr(0, length-1)
|
||||||
this.log('BS backspace', this.buffer)
|
this.log('BS backspace', this.buffer)
|
||||||
this.result(this.buffer.substr(2, length-1))
|
this.result(trigger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,41 +160,53 @@
|
||||||
// arrow keys
|
// arrow keys
|
||||||
if (e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) return
|
if (e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) return
|
||||||
|
|
||||||
// observer other second key
|
var newChar = String.fromCharCode(e.which)
|
||||||
if (this.buffer === ':' && String.fromCharCode(e.which) !== ':') {
|
|
||||||
this.buffer = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// oberserve second :
|
// observe other keys
|
||||||
if (this.buffer === ':' && String.fromCharCode(e.which) === ':') {
|
if (this.hasAvailableTriggers(this.buffer)) {
|
||||||
this.buffer = this.buffer + ':'
|
if(this.hasAvailableTriggers(this.buffer + newChar)) {
|
||||||
|
this.buffer = this.buffer + newChar
|
||||||
|
} else if (!this.findTrigger(this.buffer)) {
|
||||||
|
this.buffer = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// oberserve first :
|
// oberserve first :
|
||||||
if (!this.buffer && String.fromCharCode(e.which) === ':') {
|
if (!this.buffer && this.hasAvailableTriggers(newChar)) {
|
||||||
this.buffer = this.buffer + ':'
|
this.buffer = this.buffer + newChar
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.buffer && this.buffer.substr(0,2) === '::') {
|
var trigger = this.findTrigger(this.buffer)
|
||||||
|
if (trigger) {
|
||||||
var sign = String.fromCharCode(e.which)
|
|
||||||
if ( sign && sign !== ':' && e.which != 8 ) { // 8 == backspace
|
|
||||||
this.buffer = this.buffer + sign
|
|
||||||
//this.log('BUFF ADD', sign, this.buffer, sign.length, e.which)
|
|
||||||
}
|
|
||||||
this.log('BUFF HINT', this.buffer, this.buffer.length, e.which, String.fromCharCode(e.which))
|
this.log('BUFF HINT', this.buffer, this.buffer.length, e.which, String.fromCharCode(e.which))
|
||||||
|
|
||||||
if (!this.isActive()) {
|
if (!this.isActive()) {
|
||||||
this.open()
|
this.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.result(this.buffer.substr(2, this.buffer.length))
|
this.result(trigger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if at least one trigger is available with the given prefix
|
||||||
|
Plugin.prototype.hasAvailableTriggers = function(prefix) {
|
||||||
|
var result = _.find(this.helpers, function(helper) {
|
||||||
|
var trigger = helper.trigger
|
||||||
|
return trigger.substr(0, prefix.length) == prefix.substr(0, trigger.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result != undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// find a matching trigger
|
||||||
|
Plugin.prototype.findTrigger = function(string) {
|
||||||
|
return _.find(this.helpers, function(helper) {
|
||||||
|
return helper.trigger == string.substr(0, helper.trigger.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// create base template
|
// create base template
|
||||||
Plugin.prototype.renderBase = function() {
|
Plugin.prototype.renderBase = function() {
|
||||||
this.untouched = true
|
|
||||||
this.$element.after('<div class="shortcut dropdown"><ul class="dropdown-menu" style="max-height: 200px;"></ul></div>')
|
this.$element.after('<div class="shortcut dropdown"><ul class="dropdown-menu" style="max-height: 200px;"></ul></div>')
|
||||||
this.$widget = this.$element.next()
|
this.$widget = this.$element.next()
|
||||||
this.$widget.on('mousedown', 'li', $.proxy(this.onEntryClick, this))
|
this.$widget.on('mousedown', 'li', $.proxy(this.onEntryClick, this))
|
||||||
|
@ -344,27 +357,27 @@
|
||||||
|
|
||||||
Plugin.prototype.onEntryClick = function(event) {
|
Plugin.prototype.onEntryClick = function(event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
var id = $(event.currentTarget).data('id')
|
this.take(event.currentTarget)
|
||||||
this.take(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// select text module and insert into text
|
// select text module and insert into text
|
||||||
Plugin.prototype.take = function(id) {
|
Plugin.prototype.take = function(elem) {
|
||||||
if (!id) {
|
if (!elem) {
|
||||||
this.close(true)
|
this.close(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (var i = 0; i < this.collection.length; i++) {
|
|
||||||
var item = this.collection[i]
|
var trigger = this.findTrigger(this.buffer)
|
||||||
if (item.id == id) {
|
|
||||||
var content = item.content
|
if (trigger) {
|
||||||
this.cutInput()
|
var _this = this;
|
||||||
this.paste(content)
|
|
||||||
this.close(true)
|
trigger.renderValue(this, elem, function(text) {
|
||||||
return
|
_this.cutInput()
|
||||||
}
|
_this.paste(text)
|
||||||
|
_this.close(true)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.prototype.getFirstRange = function() {
|
Plugin.prototype.getFirstRange = function() {
|
||||||
|
@ -381,47 +394,33 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// render result
|
// render result
|
||||||
Plugin.prototype.result = function(term) {
|
Plugin.prototype.result = function(trigger) {
|
||||||
var _this = this
|
if (!trigger) return
|
||||||
var result = _.filter(this.collection, function(item) {
|
|
||||||
var reg = new RegExp(term, 'i')
|
|
||||||
if (item.name && item.name.match(reg)) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
if (item.keywords && item.keywords.match(reg)) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
|
||||||
|
|
||||||
result.reverse()
|
var term = this.buffer.substr(trigger.trigger.length, this.buffer.length)
|
||||||
|
trigger.renderResults(this, term)
|
||||||
|
}
|
||||||
|
|
||||||
this.$widget.find('ul').html('')
|
Plugin.prototype.emptyResultsContainer = function() {
|
||||||
this.log('result', term, result)
|
this.$widget.find('ul').empty()
|
||||||
|
}
|
||||||
|
|
||||||
var elements = $()
|
Plugin.prototype.appendResults = function(collection) {
|
||||||
|
this.$widget.find('ul').append(collection).scrollTop(9999)
|
||||||
for (var i = 0; i < result.length; i++) {
|
this.afterResultRendering()
|
||||||
var item = result[i]
|
}
|
||||||
var element = $('<li>')
|
|
||||||
element.attr('data-id', item.id)
|
|
||||||
element.text(item.name)
|
|
||||||
element.addClass('u-clickable u-textTruncate')
|
|
||||||
if (i == result.length-1) {
|
|
||||||
element.addClass('is-active')
|
|
||||||
}
|
|
||||||
if (item.keywords) {
|
|
||||||
element.append($('<kbd>').text(item.keywords))
|
|
||||||
}
|
|
||||||
elements = elements.add(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$widget.find('ul').append(elements).scrollTop(9999)
|
|
||||||
|
|
||||||
|
Plugin.prototype.afterResultRendering = function() {
|
||||||
// keep the width of the dropdown the same even when longer items got filtered out
|
// keep the width of the dropdown the same even when longer items got filtered out
|
||||||
if(this._fixedWidth && this.untouched){
|
if(this._fixedWidth){
|
||||||
this.$widget.find('ul').css('width', this.$widget.find('ul').width());
|
var elem = this.$widget.find('ul')
|
||||||
this.untouched = false;
|
|
||||||
|
var currentMinWidth = parseInt(elem.css('min-width'))
|
||||||
|
var realWidth = elem.width()
|
||||||
|
|
||||||
|
if(!currentMinWidth || realWidth > currentMinWidth) {
|
||||||
|
elem.css('min-width', realWidth + 'px')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.movePosition()
|
this.movePosition()
|
||||||
|
@ -445,4 +444,144 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Collection() {}
|
||||||
|
|
||||||
|
Collection.renderValue = function(textmodule, elem, callback) {
|
||||||
|
var id = $(elem).data('id')
|
||||||
|
var item = _.find(textmodule.collection, function(elem) { return elem.id == id })
|
||||||
|
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
callback(item.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.renderResults = function(textmodule, term) {
|
||||||
|
var reg = new RegExp(term, 'i')
|
||||||
|
var result = textmodule.collection.filter(function(item) {
|
||||||
|
return (item.name && item.name.match(reg)) || (item.keywords && item.keywords.match(reg))
|
||||||
|
})
|
||||||
|
result.reverse()
|
||||||
|
|
||||||
|
textmodule.emptyResultsContainer()
|
||||||
|
|
||||||
|
var elements = result.map(function(elem, index, array){
|
||||||
|
var element = $('<li>')
|
||||||
|
.attr('data-id', elem.id)
|
||||||
|
.text(elem.name)
|
||||||
|
.addClass('u-clickable u-textTruncate')
|
||||||
|
|
||||||
|
if (index == array.length-1) {
|
||||||
|
element.addClass('is-active')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elem.keywords) {
|
||||||
|
element.append($('<kbd>').text(elem.keywords))
|
||||||
|
}
|
||||||
|
|
||||||
|
return element
|
||||||
|
})
|
||||||
|
|
||||||
|
textmodule.appendResults(elements)
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.trigger = '::'
|
||||||
|
|
||||||
|
function KbAnswer() {}
|
||||||
|
|
||||||
|
KbAnswer.renderValue = function(textmodule, elem, callback) {
|
||||||
|
textmodule.emptyResultsContainer()
|
||||||
|
|
||||||
|
var element = $('<li>').text(App.i18n.translateInline('Please wait...'))
|
||||||
|
textmodule.appendResults(element)
|
||||||
|
|
||||||
|
App.Ajax.request({
|
||||||
|
id: 'textmoduleKbAnswer',
|
||||||
|
type: 'GET',
|
||||||
|
url: $(elem).data('url'),
|
||||||
|
success: function(data, status, xhr) {
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
|
||||||
|
var translation = App.KnowledgeBaseAnswerTranslation.find($(elem).data('id'))
|
||||||
|
|
||||||
|
var body = translation.content().bodyWithPublicURLs()
|
||||||
|
|
||||||
|
App.Utils.htmlImage2DataUrlAsync(body, function(output){
|
||||||
|
callback(output)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
callback('')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
KbAnswer.renderResults = function(textmodule, term) {
|
||||||
|
textmodule.emptyResultsContainer()
|
||||||
|
|
||||||
|
if(!term) {
|
||||||
|
var element = $('<li>').text(App.i18n.translateInline('Start typing to search in Knowledge Base...'))
|
||||||
|
textmodule.appendResults(element)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var element = $('<li>').text(App.i18n.translateInline('Loading...'))
|
||||||
|
textmodule.appendResults(element)
|
||||||
|
|
||||||
|
App.Delay.set(function() {
|
||||||
|
App.Ajax.request({
|
||||||
|
id: 'textmoduleKbAnswer',
|
||||||
|
type: 'POST',
|
||||||
|
url: App.Config.get('api_path') + '/knowledge_bases/search',
|
||||||
|
data: JSON.stringify({
|
||||||
|
'query': term,
|
||||||
|
'flavor': 'agent',
|
||||||
|
'index': 'KnowledgeBase::Answer::Translation',
|
||||||
|
'url_type': 'agent',
|
||||||
|
'highlight_enabled': false
|
||||||
|
}),
|
||||||
|
processData: true,
|
||||||
|
success: function(data, status, xhr) {
|
||||||
|
textmodule.emptyResultsContainer()
|
||||||
|
|
||||||
|
var items = data
|
||||||
|
.result
|
||||||
|
.map(function(elem){
|
||||||
|
if(result = _.find(data.details, function(detailElem) { return detailElem.type == elem.type && detailElem.id == elem.id })) {
|
||||||
|
return {
|
||||||
|
'name': result.title,
|
||||||
|
'value': elem.id,
|
||||||
|
'url': result.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(function(elem){ return elem != undefined })
|
||||||
|
.map(function(elem, index, array) {
|
||||||
|
var element = $('<li>')
|
||||||
|
.attr('data-id', elem.value)
|
||||||
|
.attr('data-url', elem.url)
|
||||||
|
.text(elem.name)
|
||||||
|
.addClass('u-clickable u-textTruncate')
|
||||||
|
|
||||||
|
if (index == array.length-1) {
|
||||||
|
element.addClass('is-active')
|
||||||
|
}
|
||||||
|
|
||||||
|
return element
|
||||||
|
})
|
||||||
|
|
||||||
|
if(items.length == 0) {
|
||||||
|
items.push($('<li>').text(App.i18n.translateInline('No results found')))
|
||||||
|
}
|
||||||
|
|
||||||
|
textmodule.appendResults(items)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 200, 'textmoduleKbAnswerDelay', 'textmodule')
|
||||||
|
}
|
||||||
|
|
||||||
|
KbAnswer.trigger = '??'
|
||||||
|
|
||||||
|
Plugin.prototype.helpers = [Collection, KbAnswer]
|
||||||
|
|
||||||
}(jQuery, window));
|
}(jQuery, window));
|
|
@ -83,8 +83,8 @@
|
||||||
.show()
|
.show()
|
||||||
.scrollTop(0)
|
.scrollTop(0)
|
||||||
|
|
||||||
if (that.options.backdrop) that.adjustBackdrop()
|
|
||||||
that.adjustDialog()
|
that.adjustDialog()
|
||||||
|
if (that.options.backdrop) that.adjustBackdrop()
|
||||||
|
|
||||||
if (transition) {
|
if (transition) {
|
||||||
that.$element[0].offsetWidth // force reflow
|
that.$element[0].offsetWidth // force reflow
|
||||||
|
@ -241,8 +241,8 @@
|
||||||
// these following methods are used to handle overflowing modals
|
// these following methods are used to handle overflowing modals
|
||||||
|
|
||||||
Modal.prototype.handleUpdate = function () {
|
Modal.prototype.handleUpdate = function () {
|
||||||
if (this.options.backdrop) this.adjustBackdrop()
|
|
||||||
this.adjustDialog()
|
this.adjustDialog()
|
||||||
|
if (this.options.backdrop) this.adjustBackdrop()
|
||||||
}
|
}
|
||||||
|
|
||||||
Modal.prototype.adjustBackdrop = function () {
|
Modal.prototype.adjustBackdrop = function () {
|
||||||
|
|
|
@ -33,12 +33,14 @@
|
||||||
animation: true,
|
animation: true,
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
selector: false,
|
selector: false,
|
||||||
|
backdrop: false,
|
||||||
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
|
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
|
||||||
trigger: 'hover focus',
|
trigger: 'hover focus',
|
||||||
title: '',
|
title: '',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
html: false,
|
html: false,
|
||||||
container: false,
|
container: false,
|
||||||
|
theme: 'light',
|
||||||
viewport: {
|
viewport: {
|
||||||
selector: 'body',
|
selector: 'body',
|
||||||
padding: 0
|
padding: 0
|
||||||
|
@ -161,9 +163,12 @@
|
||||||
var tipId = this.getUID(this.type)
|
var tipId = this.getUID(this.type)
|
||||||
|
|
||||||
this.setContent()
|
this.setContent()
|
||||||
$tip.attr('id', tipId)
|
$tip.attr('id', tipId).attr('data-theme', this.options.theme)
|
||||||
this.$element.attr('aria-describedby', tipId)
|
this.$element.attr('aria-describedby', tipId)
|
||||||
|
|
||||||
|
if(this.options.backdrop)
|
||||||
|
this.$tip.on('click.bs.tooltip.stopPropagation', function(event){ event.stopPropagation() })
|
||||||
|
|
||||||
if (this.options.animation) $tip.addClass('fade')
|
if (this.options.animation) $tip.addClass('fade')
|
||||||
|
|
||||||
var placement = typeof this.options.placement == 'function' ?
|
var placement = typeof this.options.placement == 'function' ?
|
||||||
|
@ -210,6 +215,8 @@
|
||||||
var prevHoverState = that.hoverState
|
var prevHoverState = that.hoverState
|
||||||
that.$element.trigger('shown.bs.' + that.type)
|
that.$element.trigger('shown.bs.' + that.type)
|
||||||
that.hoverState = null
|
that.hoverState = null
|
||||||
|
if(that.options.backdrop)
|
||||||
|
$(document).one('click.bs.tooltip', function(){ that.hide() })
|
||||||
|
|
||||||
if (prevHoverState == 'out') that.leave(that)
|
if (prevHoverState == 'out') that.leave(that)
|
||||||
}
|
}
|
||||||
|
@ -436,6 +443,9 @@
|
||||||
clearTimeout(this.timeout)
|
clearTimeout(this.timeout)
|
||||||
this.hide(function () {
|
this.hide(function () {
|
||||||
that.$element.off('.' + that.type).removeData('bs.' + that.type)
|
that.$element.off('.' + that.type).removeData('bs.' + that.type)
|
||||||
|
|
||||||
|
if(that.options.backdrop)
|
||||||
|
that.$tip.off('click.bs.tooltip.stopPropagation')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1518
app/assets/javascripts/app/lib/core/jquery-ui-1.11.4.js
vendored
1518
app/assets/javascripts/app/lib/core/jquery-ui-1.11.4.js
vendored
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,27 @@
|
||||||
|
InstanceMethods =
|
||||||
|
contentSidebarActions: (kb_locale) ->
|
||||||
|
buttons = []
|
||||||
|
|
||||||
|
if @constructor.canBePublished?()
|
||||||
|
buttons.push {
|
||||||
|
iconName: 'eye'
|
||||||
|
name: 'Visibility'
|
||||||
|
action: 'visibility'
|
||||||
|
cssClass: 'btn--success'
|
||||||
|
disabled: @isNew()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(@ instanceof App.KnowledgeBase)
|
||||||
|
buttons.push {
|
||||||
|
iconName: 'trash'
|
||||||
|
name: 'Delete'
|
||||||
|
action: 'delete'
|
||||||
|
cssClass: 'btn--danger'
|
||||||
|
disabled: @isNew()
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons
|
||||||
|
|
||||||
|
App.KnowledgeBaseActions =
|
||||||
|
extended: ->
|
||||||
|
@include InstanceMethods
|
|
@ -0,0 +1,101 @@
|
||||||
|
InstanceMethods =
|
||||||
|
canBePublished: -> true
|
||||||
|
|
||||||
|
can_be_published_state: ->
|
||||||
|
matching_time = (new Date()).getTime()
|
||||||
|
|
||||||
|
switch
|
||||||
|
when @date(@archived_at) < matching_time
|
||||||
|
'archived'
|
||||||
|
when @date(@published_at) < matching_time
|
||||||
|
'published'
|
||||||
|
when @date(@internal_at) < matching_time
|
||||||
|
'internal'
|
||||||
|
else
|
||||||
|
'draft'
|
||||||
|
|
||||||
|
can_be_published_by: (state = @can_be_published_state()) ->
|
||||||
|
if state == 'draft'
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = @["#{state}_by_id"]
|
||||||
|
|
||||||
|
App.User.find user_id
|
||||||
|
|
||||||
|
can_be_published_at: (state = @can_be_published_state()) ->
|
||||||
|
if state == 'draft'
|
||||||
|
return @created_at
|
||||||
|
|
||||||
|
@["#{state}_at"]
|
||||||
|
|
||||||
|
can_be_published_state_css: ->
|
||||||
|
switch @can_be_published_state()
|
||||||
|
when 'archived'
|
||||||
|
'label-danger'
|
||||||
|
when 'published'
|
||||||
|
'label-success'
|
||||||
|
when 'draft'
|
||||||
|
'label-subtle'
|
||||||
|
|
||||||
|
can_be_published_quick_actions: ->
|
||||||
|
switch @can_be_published_state()
|
||||||
|
when 'published'
|
||||||
|
['archive']
|
||||||
|
when 'internal'
|
||||||
|
['publish', 'archive']
|
||||||
|
when 'draft'
|
||||||
|
['publish', 'internal']
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
|
||||||
|
next_call_to_action: ->
|
||||||
|
switch @can_be_published_state()
|
||||||
|
when 'archived'
|
||||||
|
['unarchive']
|
||||||
|
when 'published'
|
||||||
|
['archive']
|
||||||
|
when 'internal'
|
||||||
|
['publish', 'archive']
|
||||||
|
else
|
||||||
|
['publish', 'internal']
|
||||||
|
|
||||||
|
can_be_published_publish_in_future: ->
|
||||||
|
@date(@published_at) > (new Date()).getTime()
|
||||||
|
|
||||||
|
can_be_published_archive_in_future: ->
|
||||||
|
@date(@archived_at) > (new Date()).getTime()
|
||||||
|
|
||||||
|
can_be_published_internal_in_future: ->
|
||||||
|
@date(@internal_at) > (new Date()).getTime()
|
||||||
|
|
||||||
|
is_internally_published: (kb_locale) ->
|
||||||
|
state = @can_be_published_state()
|
||||||
|
object_published = state == 'internal' || state == 'published'
|
||||||
|
|
||||||
|
if !object_published
|
||||||
|
return false
|
||||||
|
|
||||||
|
if !@translation(kb_locale.id)
|
||||||
|
return false
|
||||||
|
|
||||||
|
true
|
||||||
|
|
||||||
|
is_published: (kb_locale) ->
|
||||||
|
if @can_be_published_state() isnt 'published'
|
||||||
|
return false
|
||||||
|
|
||||||
|
if !@translation(kb_locale.id)
|
||||||
|
return false
|
||||||
|
|
||||||
|
true
|
||||||
|
|
||||||
|
|
||||||
|
date: (string) ->
|
||||||
|
return undefined if !string
|
||||||
|
new Date(string).getTime()
|
||||||
|
|
||||||
|
App.KnowledgeBaseCanBePublished =
|
||||||
|
canBePublished: -> true
|
||||||
|
|
||||||
|
extended: ->
|
||||||
|
@include InstanceMethods
|
|
@ -0,0 +1,110 @@
|
||||||
|
InstanceMethods =
|
||||||
|
translations: ->
|
||||||
|
klass = @constructor.translatableClass()
|
||||||
|
key = @constructor.translatableForeignKey()
|
||||||
|
|
||||||
|
klass.all().filter (elem) => elem[key] == @id
|
||||||
|
|
||||||
|
translation: (kb_locale_id) ->
|
||||||
|
return null if kb_locale_id is null or kb_locale_id is undefined
|
||||||
|
@translations().filter((elem) -> elem.kb_locale_id == kb_locale_id)[0]
|
||||||
|
|
||||||
|
primaryTranslation: ->
|
||||||
|
primaryKbLocale = @knowledge_base().primaryKbLocale()
|
||||||
|
@translation(primaryKbLocale.id)
|
||||||
|
|
||||||
|
attributesIncludingTranslation: (kb_locale_id) ->
|
||||||
|
output = @attributes()
|
||||||
|
output.translation = @translation(kb_locale_id)?.attributes()
|
||||||
|
output
|
||||||
|
|
||||||
|
attributesForRendering: (kb_locale, options = {}) ->
|
||||||
|
attrs = {
|
||||||
|
id: @id
|
||||||
|
url: @uiUrl(kb_locale, options.action)
|
||||||
|
title: @guaranteedTitle(kb_locale.id)
|
||||||
|
missingTranslation: @translation(kb_locale.id) is undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if @ instanceof App.KnowledgeBase
|
||||||
|
attrs.icon = 'knowledge-base'
|
||||||
|
attrs.title = ''
|
||||||
|
|
||||||
|
if @ instanceof App.KnowledgeBaseCategory
|
||||||
|
attrs.iconFont = true
|
||||||
|
attrs.icon = @category_icon
|
||||||
|
attrs.count = @countDeepAnswers()
|
||||||
|
|
||||||
|
if options.isEditor
|
||||||
|
attrs.editorOnly = !@visibleInternally(kb_locale)
|
||||||
|
else
|
||||||
|
attrs.editorOnly = false
|
||||||
|
|
||||||
|
if @ instanceof App.KnowledgeBaseAnswer
|
||||||
|
attrs.icon = 'knowledge-base-answer'
|
||||||
|
|
||||||
|
if options.isEditor
|
||||||
|
attrs.editorOnly = !@is_internally_published(kb_locale)
|
||||||
|
else
|
||||||
|
attrs.editorOnly = false
|
||||||
|
|
||||||
|
# attrs.className = if attrs.missingTranslation
|
||||||
|
# 'kb-item--missing-translation'
|
||||||
|
# else if attrs.editorOnly
|
||||||
|
# 'kb-item--invisible'
|
||||||
|
|
||||||
|
attrs.className = if attrs.editorOnly
|
||||||
|
'kb-item--invisible'
|
||||||
|
|
||||||
|
attrs.icons = {}
|
||||||
|
|
||||||
|
if attrs.missingTranslation
|
||||||
|
attrs.icons['danger'] = true
|
||||||
|
|
||||||
|
attrs
|
||||||
|
|
||||||
|
writeMethod: ->
|
||||||
|
if @id then 'PATCH' else 'POST'
|
||||||
|
|
||||||
|
prepareNestedParams: (params, kb_locale_id) ->
|
||||||
|
if @baseParams
|
||||||
|
params = _.extendOwn(@baseParams(), params)
|
||||||
|
|
||||||
|
translation_params = params['translation']
|
||||||
|
delete params['translation']
|
||||||
|
|
||||||
|
if translation = @translation(kb_locale_id)
|
||||||
|
translation_params['id'] = translation.id
|
||||||
|
else
|
||||||
|
translation_params['kb_locale_id'] = kb_locale_id
|
||||||
|
|
||||||
|
if @constructor.translatableClass().processAttributes
|
||||||
|
translation_params = @constructor.translatableClass().processAttributes(translation_params)
|
||||||
|
|
||||||
|
params['translations_attributes'] = [translation_params]
|
||||||
|
params
|
||||||
|
|
||||||
|
objectActionName: ->
|
||||||
|
action = if @isNew() then 'New' else 'Edit'
|
||||||
|
"#{action} #{@objectName()}"
|
||||||
|
|
||||||
|
removeTranslations: (options = {}) ->
|
||||||
|
for translation in @translations()
|
||||||
|
translation.remove(options)
|
||||||
|
|
||||||
|
removeIncludingTranslations: (options = {}) ->
|
||||||
|
@removeTranslations(options)
|
||||||
|
@remove(options)
|
||||||
|
|
||||||
|
guaranteedTranslation: (kb_locale_id) ->
|
||||||
|
@translation(kb_locale_id) || @primaryTranslation() || @translations()[0]
|
||||||
|
|
||||||
|
guaranteedTitle: (kb_locale, placeholder = '-') ->
|
||||||
|
@guaranteedTranslation(kb_locale)?.title || placeholder
|
||||||
|
|
||||||
|
translationBindlableObject: (kb_locale_id) ->
|
||||||
|
@translation(kb_locale_id) || @constructor.translatableClass()
|
||||||
|
|
||||||
|
App.KnowledgeBaseTranslatable =
|
||||||
|
extended: ->
|
||||||
|
@include InstanceMethods
|
|
@ -0,0 +1,29 @@
|
||||||
|
InstanceMethods =
|
||||||
|
parent: ->
|
||||||
|
throw 'Please implement parent method, fetching parent object'
|
||||||
|
|
||||||
|
uiUrl: ->
|
||||||
|
kb_locale = App.KnowledgeBaseLocale.localeFor(@)
|
||||||
|
@parent().uiUrl(kb_locale)
|
||||||
|
|
||||||
|
fullyLoaded: ->
|
||||||
|
if @ instanceof App.KnowledgeBaseAnswerTranslation
|
||||||
|
return @content() isnt null
|
||||||
|
|
||||||
|
true
|
||||||
|
|
||||||
|
defaultSearchResultAttributes: ->
|
||||||
|
kb_locale = App.KnowledgeBaseLocale.localeFor(@)
|
||||||
|
|
||||||
|
{
|
||||||
|
display: @title
|
||||||
|
id: @id
|
||||||
|
url: @uiUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResultAttributes: ->
|
||||||
|
@defaultSearchResultAttributes()
|
||||||
|
|
||||||
|
App.KnowledgeBaseTranslationable =
|
||||||
|
extended: ->
|
||||||
|
@include InstanceMethods
|
230
app/assets/javascripts/app/models/knowledge_base.coffee
Normal file
230
app/assets/javascripts/app/models/knowledge_base.coffee
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
class App.KnowledgeBase extends App.Model
|
||||||
|
@configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', 'translation_ids', 'locale_ids', 'homepage_layout', 'category_layout', 'custom_address'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@extend App.KnowledgeBaseActions
|
||||||
|
@url: @apiPath + '/knowledge_bases'
|
||||||
|
|
||||||
|
@manageUrl: @apiPath + '/knowledge_bases/manage'
|
||||||
|
manageUrl: (action = null) ->
|
||||||
|
App.Utils.joinUrlComponents(@constructor.manageUrl, @id, action)
|
||||||
|
|
||||||
|
publicBaseUrl: (kb_locale = undefined) ->
|
||||||
|
# coffeelint: disable=indentation
|
||||||
|
components = if @custom_address? && @custom_address[0] != '/'
|
||||||
|
["http://#{@custom_address}"]
|
||||||
|
else if @custom_address?
|
||||||
|
[App.Utils.baseUrl(), @custom_address.substr(1, @custom_address.length - 1)]
|
||||||
|
else
|
||||||
|
[App.Utils.baseUrl(), 'help']
|
||||||
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
|
if kb_locale
|
||||||
|
components.push kb_locale.systemLocale().locale
|
||||||
|
|
||||||
|
App.Utils.joinUrlComponents components
|
||||||
|
|
||||||
|
uiUrl: (kb_locale, suffix = undefined) ->
|
||||||
|
App.Utils.joinUrlComponents @uiUrlComponent(), kb_locale.urlSuffix(), suffix
|
||||||
|
|
||||||
|
uiUrlComponent: ->
|
||||||
|
"#knowledge_base/#{@id}"
|
||||||
|
|
||||||
|
categories: ->
|
||||||
|
App.KnowledgeBaseCategory.all().filter (item) => item.knowledge_base_id == @id
|
||||||
|
|
||||||
|
rootCategories: ->
|
||||||
|
@categories()
|
||||||
|
.filter (item) -> item.parent_id is null
|
||||||
|
.sort (a, b) -> a.position - b.position
|
||||||
|
|
||||||
|
kb_locales: ->
|
||||||
|
App.KnowledgeBaseLocale.findAll(@kb_locale_ids)
|
||||||
|
|
||||||
|
primaryKbLocale: ->
|
||||||
|
@kb_locales().filter((elem) -> elem.primary)[0]
|
||||||
|
|
||||||
|
knowledge_base: ->
|
||||||
|
@
|
||||||
|
|
||||||
|
isEmpty: ->
|
||||||
|
@rootCategories().length is 0
|
||||||
|
|
||||||
|
@translatableClass: -> App.KnowledgeBaseTranslation
|
||||||
|
@translatableForeignKey: -> 'knowledge_base_id'
|
||||||
|
@extend App.KnowledgeBaseTranslatable
|
||||||
|
|
||||||
|
remove: (options = {}) ->
|
||||||
|
@rootCategories().forEach (elem) -> elem.remove(options)
|
||||||
|
@removeTranslations(options)
|
||||||
|
super
|
||||||
|
|
||||||
|
objectName: ->
|
||||||
|
'Knowledge Base'
|
||||||
|
|
||||||
|
categoriesForDropdown: (options = {}) ->
|
||||||
|
initial = []
|
||||||
|
if options.includeRoot
|
||||||
|
initial.push { value: null, name: '>> Homepage <<'}
|
||||||
|
|
||||||
|
initialNestLevel = if options.includeRoot
|
||||||
|
1
|
||||||
|
else
|
||||||
|
0
|
||||||
|
|
||||||
|
@rootCategories().reduce (memo, elem) ->
|
||||||
|
memo.concat elem.categoriesForDropdown(nested: initialNestLevel, kb_locale: options.kb_locale)
|
||||||
|
, initial
|
||||||
|
|
||||||
|
visibleInternally: (kb_locale) ->
|
||||||
|
@active
|
||||||
|
|
||||||
|
visiblePublicly: (kb_locale) ->
|
||||||
|
@active
|
||||||
|
|
||||||
|
attributes: ->
|
||||||
|
attrs = super()
|
||||||
|
|
||||||
|
attrs.kb_locales = @kb_locales().map (elem) -> elem.attributes()
|
||||||
|
|
||||||
|
attrs
|
||||||
|
|
||||||
|
@configure_attributes: [
|
||||||
|
{
|
||||||
|
name: 'translation::title'
|
||||||
|
model: 'translation'
|
||||||
|
display: 'Title'
|
||||||
|
tag: 'input'
|
||||||
|
null: false
|
||||||
|
screen:
|
||||||
|
agent_edit:
|
||||||
|
shown: true
|
||||||
|
#}, {
|
||||||
|
#name: 'homepage_layout'
|
||||||
|
#model: 'knowledge_base'
|
||||||
|
#display: 'Layout'
|
||||||
|
#tag: 'radio'
|
||||||
|
#null: true
|
||||||
|
#screen:
|
||||||
|
#agent:
|
||||||
|
#shown: true
|
||||||
|
#options: [
|
||||||
|
#value: 'grid',
|
||||||
|
#name: 'Grid'
|
||||||
|
#graphic: 'knowledge_base_grid.svg'
|
||||||
|
#,
|
||||||
|
#value: 'list'
|
||||||
|
#name: 'List'
|
||||||
|
#graphic: 'knowledge_base_list.svg'
|
||||||
|
#]
|
||||||
|
##relation: 'KnowledgeBaseLayout'
|
||||||
|
}, {
|
||||||
|
name: 'translation::footer_note'
|
||||||
|
model: 'translation'
|
||||||
|
display: 'Footer Note'
|
||||||
|
tag: 'input'
|
||||||
|
null: false
|
||||||
|
screen:
|
||||||
|
agent_edit:
|
||||||
|
shown: true
|
||||||
|
}, {
|
||||||
|
name: 'color_highlight'
|
||||||
|
display: 'Highlight Color'
|
||||||
|
tag: 'color'
|
||||||
|
style: 'block'
|
||||||
|
null: false
|
||||||
|
help: 'The highlight color is used to make elements of the interface stand out. For example the links and icons.'
|
||||||
|
screen:
|
||||||
|
admin_style_color_highlight:
|
||||||
|
display: false
|
||||||
|
horizontal: true
|
||||||
|
shown: true
|
||||||
|
}, {
|
||||||
|
name: 'color_header'
|
||||||
|
display: 'Header Color'
|
||||||
|
tag: 'color'
|
||||||
|
style: 'block'
|
||||||
|
null: false
|
||||||
|
screen:
|
||||||
|
admin_style_color_header:
|
||||||
|
display: false
|
||||||
|
horizontal: true
|
||||||
|
shown: true
|
||||||
|
# Layout picker is disabled in V1
|
||||||
|
#}, {
|
||||||
|
# name: 'homepage_layout'
|
||||||
|
# display: 'Landing page layout'
|
||||||
|
# tag: 'radio_graphic'
|
||||||
|
# null: false
|
||||||
|
# style: 'block'
|
||||||
|
# screen:
|
||||||
|
# admin_style_homepage:
|
||||||
|
# display: false
|
||||||
|
# horizontal: true
|
||||||
|
# shown: true
|
||||||
|
# options: [
|
||||||
|
# {
|
||||||
|
# value: 'grid',
|
||||||
|
# name: 'Grid'
|
||||||
|
# graphic: 'knowledge_base_grid.svg'
|
||||||
|
# }, {
|
||||||
|
# value: 'list'
|
||||||
|
# name: 'List'
|
||||||
|
# graphic: 'knowledge_base_list.svg'
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
#}, {
|
||||||
|
# name: 'category_layout'
|
||||||
|
# display: 'Category page layout'
|
||||||
|
# tag: 'radio_graphic'
|
||||||
|
# null: false
|
||||||
|
# style: 'block'
|
||||||
|
# screen:
|
||||||
|
# admin_style_category:
|
||||||
|
# display: false
|
||||||
|
# horizontal: true
|
||||||
|
# shown: true
|
||||||
|
# options: [
|
||||||
|
# {
|
||||||
|
# value: 'grid',
|
||||||
|
# name: 'Grid'
|
||||||
|
# graphic: 'knowledge_base_grid.svg'
|
||||||
|
# }, {
|
||||||
|
# value: 'list'
|
||||||
|
# name: 'List'
|
||||||
|
# graphic: 'knowledge_base_list.svg'
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
}, {
|
||||||
|
name: 'iconset'
|
||||||
|
display: 'Icon Set'
|
||||||
|
tag: 'iconset_picker'
|
||||||
|
style: 'block'
|
||||||
|
help: "Pick an iconset that fits your style. The icons from this set can be assigned to categories. Choose wisely because the icon sets don't match with each other. If you change it later on you'll have to reset every icon."
|
||||||
|
null: false
|
||||||
|
screen:
|
||||||
|
admin_style_iconset:
|
||||||
|
shown: true
|
||||||
|
}, {
|
||||||
|
name: 'kb_locales'
|
||||||
|
display: 'Languages'
|
||||||
|
tag: 'multi_locales'
|
||||||
|
style: 'block'
|
||||||
|
null: false
|
||||||
|
help: 'Set up the languages for the Knowledge Base. Zammad detects the prefered language of the visitor. When its not available it will fall back to the primary language.'
|
||||||
|
screen:
|
||||||
|
admin_languages:
|
||||||
|
shown: true
|
||||||
|
admin_create:
|
||||||
|
shown: true
|
||||||
|
}, {
|
||||||
|
name: 'custom_address'
|
||||||
|
display: 'Custom Address'
|
||||||
|
tag: 'input'
|
||||||
|
style: 'block'
|
||||||
|
null: true
|
||||||
|
help: 'Fill in full domain (e.g. example.com or example.com/help) or path (e.g. /support) to use custom address. See Apache or Nginx for further instructions'
|
||||||
|
screen:
|
||||||
|
admin_custom_address:
|
||||||
|
shown: true
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,94 @@
|
||||||
|
class App.KnowledgeBaseAnswer extends App.Model
|
||||||
|
@configure 'KnowledgeBaseAnswer', 'category_id', 'translation_ids', 'archived_at', 'internal_at', 'published_at', 'attachments'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@extend App.KnowledgeBaseActions
|
||||||
|
@extend App.KnowledgeBaseCanBePublished
|
||||||
|
|
||||||
|
url: ->
|
||||||
|
@knowledge_base().generateURL('answers')
|
||||||
|
|
||||||
|
uiUrl: (kb_locale, action = null) ->
|
||||||
|
App.Utils.joinUrlComponents @knowledge_base().uiUrl(kb_locale), @uiUrlComponent(), action
|
||||||
|
|
||||||
|
uiUrlComponent: ->
|
||||||
|
"answer/#{@id}"
|
||||||
|
|
||||||
|
knowledge_base: ->
|
||||||
|
App.KnowledgeBase.find(@category().knowledge_base_id)
|
||||||
|
|
||||||
|
category: ->
|
||||||
|
App.KnowledgeBaseCategory.find(@category_id)
|
||||||
|
|
||||||
|
@configure_attributes = [
|
||||||
|
{
|
||||||
|
name: 'translation::title'
|
||||||
|
model: 'translation'
|
||||||
|
display: 'Title'
|
||||||
|
tag: 'input'
|
||||||
|
grid_width: '1/2'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
configure_attributes: (kb_locale = undefined) ->
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'translation::title'
|
||||||
|
model: 'translation'
|
||||||
|
display: 'Title'
|
||||||
|
tag: 'input'
|
||||||
|
grid_width: '1/2'
|
||||||
|
null: false
|
||||||
|
screen:
|
||||||
|
agent_create:
|
||||||
|
shown: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'category_id'
|
||||||
|
model: 'answer'
|
||||||
|
display: 'Category'
|
||||||
|
tag: 'select'
|
||||||
|
null: false
|
||||||
|
options: @knowledge_base().categoriesForDropdown(kb_locale: kb_locale)
|
||||||
|
grid_width: '1/2'
|
||||||
|
screen:
|
||||||
|
agent_create:
|
||||||
|
tag: 'input'
|
||||||
|
type: 'hidden'
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'translation::content::body'
|
||||||
|
model: 'translation'
|
||||||
|
buttons: [
|
||||||
|
'link'
|
||||||
|
'link_answer'
|
||||||
|
]
|
||||||
|
display: 'Content'
|
||||||
|
tag: 'richtext'
|
||||||
|
null: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
publicBaseUrl: (kb_locale) ->
|
||||||
|
return null if @isNew()
|
||||||
|
App.Utils.joinUrlComponents [@category().publicBaseUrl(kb_locale), @id]
|
||||||
|
|
||||||
|
@translatableClass: -> App.KnowledgeBaseAnswerTranslation
|
||||||
|
@translatableForeignKey: -> 'answer_id'
|
||||||
|
@extend App.KnowledgeBaseTranslatable
|
||||||
|
|
||||||
|
remove: (options = {}) ->
|
||||||
|
@removeTranslations(options)
|
||||||
|
super
|
||||||
|
|
||||||
|
baseParams: ->
|
||||||
|
{ category_id: @category_id }
|
||||||
|
|
||||||
|
category: ->
|
||||||
|
App.KnowledgeBaseCategory.find(@category_id)
|
||||||
|
|
||||||
|
objectName: ->
|
||||||
|
'Answer'
|
||||||
|
|
||||||
|
visibleInternally: (kb_locale) =>
|
||||||
|
@is_internally_published(kb_locale)
|
|
@ -0,0 +1,112 @@
|
||||||
|
class App.KnowledgeBaseAnswerTranslation extends App.Model
|
||||||
|
@configure 'KnowledgeBaseAnswerTranslation', 'title', 'preview', 'content', 'note', 'source', 'outdated', 'promoted', 'answer_id', 'locale_id', 'state_id'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@extend App.KnowledgeBaseTranslationable
|
||||||
|
@configure_attributes = [
|
||||||
|
{ name: 'title', display: 'Name', tag: 'input', type: 'text', limit: 300, null: false, info: true },
|
||||||
|
{ name: 'created_at', display: 'Created at', tag: 'datetime', readonly: 1, info: false },
|
||||||
|
{ name: 'updated_at', display: 'Updated at', tag: 'datetime', readonly: 1, info: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
url: ->
|
||||||
|
@parent().generateURL('translations')
|
||||||
|
|
||||||
|
uiUrl: (action = null) ->
|
||||||
|
@parent().uiUrl(App.KnowledgeBaseLocale.localeFor(@), action)
|
||||||
|
|
||||||
|
publicBaseUrl: ->
|
||||||
|
@parent().publicBaseUrl(App.KnowledgeBaseLocale.localeFor(@))
|
||||||
|
|
||||||
|
content: ->
|
||||||
|
App.KnowledgeBaseAnswerTranslationContent.find(@content_id)
|
||||||
|
|
||||||
|
displayName: ->
|
||||||
|
@title
|
||||||
|
|
||||||
|
parent: ->
|
||||||
|
App.KnowledgeBaseAnswer.find(@answer_id)
|
||||||
|
|
||||||
|
remove: (options = {}) ->
|
||||||
|
@content()?.remove(options)
|
||||||
|
super
|
||||||
|
|
||||||
|
attributes: ->
|
||||||
|
attributes = super
|
||||||
|
attributes.content = @content()?.attributes()
|
||||||
|
attributes
|
||||||
|
|
||||||
|
loadFull: (callback) ->
|
||||||
|
url = @parent().generateURL() + "?full=1&include_contents=#{@content_id}"
|
||||||
|
|
||||||
|
App.Ajax.request(
|
||||||
|
url: url
|
||||||
|
success: (data, status, xhr) ->
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
callback(true)
|
||||||
|
error: (xhr) ->
|
||||||
|
callback(false)
|
||||||
|
|
||||||
|
App.Event.trigger 'notify', {
|
||||||
|
type: 'error'
|
||||||
|
msg: xhr.responseJSON?.error || 'Unable to load'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@processAttributes: (params) ->
|
||||||
|
if (content_params = params['content']) && _.isObject(content_params)
|
||||||
|
delete params['content']
|
||||||
|
params['content_attributes'] = content_params
|
||||||
|
|
||||||
|
params
|
||||||
|
|
||||||
|
searchResultAttributes: ->
|
||||||
|
_.extend {}, @defaultSearchResultAttributes(),
|
||||||
|
class: 'kb-answer-popover'
|
||||||
|
icon: 'knowledge-base'
|
||||||
|
|
||||||
|
@configure_overview = [
|
||||||
|
'title', 'updated_at'
|
||||||
|
]
|
||||||
|
|
||||||
|
@display_name = 'Knowledge Base Answer'
|
||||||
|
|
||||||
|
linked_tickets: ->
|
||||||
|
@linked_references
|
||||||
|
.filter (elem) -> elem['link_object'] == 'Ticket'
|
||||||
|
.map (elem) -> App.Ticket.find(elem['link_object_value'])
|
||||||
|
|
||||||
|
class App.KnowledgeBaseAnswerTranslationContent extends App.Model
|
||||||
|
@configure 'KnowledgeBaseAnswerTranslationContent', 'body'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@url: @apiPath + '/knowledge_base/translation/content'
|
||||||
|
@configure_attributes = [
|
||||||
|
{ name: 'body', display: 'Body', tag: 'input' },
|
||||||
|
]
|
||||||
|
|
||||||
|
attributes: ->
|
||||||
|
attributes = super
|
||||||
|
attributes.body =
|
||||||
|
text: @body
|
||||||
|
attachments: @attachments
|
||||||
|
attributes
|
||||||
|
|
||||||
|
bodyTruncated: ->
|
||||||
|
string = @body.replace(/<([^>]+)>/g, '')
|
||||||
|
|
||||||
|
if string.length < 100
|
||||||
|
return string
|
||||||
|
|
||||||
|
string.substring(0, 100) + '...'
|
||||||
|
|
||||||
|
bodyWithPublicURLs: ->
|
||||||
|
parsed = $("<div>#{@body}</div>")
|
||||||
|
|
||||||
|
for linkDom in parsed.find('a').andSelf('a').toArray()
|
||||||
|
switch $(linkDom).attr('data-target-type')
|
||||||
|
when 'knowledge-base-answer'
|
||||||
|
if object = App.KnowledgeBaseAnswerTranslation.find $(linkDom).attr('data-target-id')
|
||||||
|
$(linkDom).attr 'href', object.publicBaseUrl()
|
||||||
|
else
|
||||||
|
$(linkDom).attr 'href', '#'
|
||||||
|
|
||||||
|
parsed[0].innerHTML
|
|
@ -0,0 +1,4 @@
|
||||||
|
class App.KnowledgeBaseAnswerTranslationState extends App.Model
|
||||||
|
@configure 'KnowledgeBaseAnswerTranslationState', 'name'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@url: @apiPath + '/knowledge_base/answer/translation/states'
|
168
app/assets/javascripts/app/models/knowledge_base_category.coffee
Normal file
168
app/assets/javascripts/app/models/knowledge_base_category.coffee
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
class App.KnowledgeBaseCategory extends App.Model
|
||||||
|
@configure 'KnowledgeBaseCategory', 'category_icon', 'parent_id', 'child_ids', 'translation_ids'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@extend App.KnowledgeBaseActions
|
||||||
|
|
||||||
|
url: ->
|
||||||
|
@knowledge_base().generateURL('categories')
|
||||||
|
|
||||||
|
uiUrl: (kb_locale, action = null) ->
|
||||||
|
App.Utils.joinUrlComponents @knowledge_base().uiUrl(kb_locale), @uiUrlComponent(), action
|
||||||
|
|
||||||
|
uiUrlComponent: ->
|
||||||
|
"category/#{@id}"
|
||||||
|
|
||||||
|
knowledge_base: ->
|
||||||
|
App.KnowledgeBase.find(@knowledge_base_id)
|
||||||
|
|
||||||
|
isEmpty: ->
|
||||||
|
@children().length is 0 and @answers().length is 0
|
||||||
|
|
||||||
|
remove: (options = {}) ->
|
||||||
|
@removeTranslations(options)
|
||||||
|
@children().forEach (elem) -> elem.remove(options)
|
||||||
|
@answers().forEach (elem) -> elem.remove(options)
|
||||||
|
super
|
||||||
|
|
||||||
|
categoriesForDropdown: (options) ->
|
||||||
|
spacer = Array.apply(null, {length: options.nested}).map(-> '- ').join('')
|
||||||
|
|
||||||
|
initial = [
|
||||||
|
{
|
||||||
|
value: @id
|
||||||
|
name: spacer + @guaranteedTitle(options.kb_locale.id)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@children().reduce (memo, elem) ->
|
||||||
|
memo.concat elem.categoriesForDropdown(nested: options.nested + 1, kb_locale: options.kb_locale)
|
||||||
|
, initial
|
||||||
|
|
||||||
|
configure_attributes: (kb_locale = undefined) ->
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'category_icon'
|
||||||
|
model: 'category'
|
||||||
|
display: 'Icon'
|
||||||
|
tag: 'icon_picker'
|
||||||
|
iconset: @knowledge_base().iconset
|
||||||
|
grid_width: '1/5'
|
||||||
|
null: false
|
||||||
|
default: @constructor.defaultIconFor(@knowledge_base())
|
||||||
|
screen:
|
||||||
|
agent_create:
|
||||||
|
shown: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'translation::title'
|
||||||
|
model: 'translation'
|
||||||
|
display: 'Title'
|
||||||
|
tag: 'input'
|
||||||
|
grid_width: '4/5'
|
||||||
|
null: false
|
||||||
|
screen:
|
||||||
|
agent_create:
|
||||||
|
shown: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parent_id'
|
||||||
|
model: 'category'
|
||||||
|
display: 'Parent'
|
||||||
|
tag: 'select'
|
||||||
|
null: true
|
||||||
|
options: @knowledge_base().categoriesForDropdown(includeRoot: true, kb_locale: kb_locale)
|
||||||
|
grid_width: '1/2'
|
||||||
|
screen:
|
||||||
|
agent_create:
|
||||||
|
tag: 'input'
|
||||||
|
type: 'hidden'
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
publicBaseUrl: (kb_locale) ->
|
||||||
|
return null if @isNew()
|
||||||
|
App.Utils.joinUrlComponents [@knowledge_base().publicBaseUrl(kb_locale), @id]
|
||||||
|
|
||||||
|
@translatableClass: -> App.KnowledgeBaseCategoryTranslation
|
||||||
|
@translatableForeignKey: -> 'category_id'
|
||||||
|
@extend App.KnowledgeBaseTranslatable
|
||||||
|
|
||||||
|
baseParams: ->
|
||||||
|
{ parent_id: @parent_id }
|
||||||
|
|
||||||
|
children: ->
|
||||||
|
return [] if @id == undefined
|
||||||
|
|
||||||
|
App.KnowledgeBaseCategory
|
||||||
|
.findAllByAttribute('parent_id', @id)
|
||||||
|
.sort (a, b) -> a.position - b.position
|
||||||
|
|
||||||
|
deepChildrenIds: ->
|
||||||
|
children = @children()
|
||||||
|
|
||||||
|
ids = children.map (elem) -> elem.deepChildrenIds()
|
||||||
|
ids.push children.map (elem) -> elem.id
|
||||||
|
|
||||||
|
_.flatten(ids)
|
||||||
|
|
||||||
|
parent: ->
|
||||||
|
App.KnowledgeBaseCategory.find(@parent_id)
|
||||||
|
|
||||||
|
answers: ->
|
||||||
|
App.KnowledgeBaseAnswer
|
||||||
|
.findAllByAttribute('category_id', @id)
|
||||||
|
.sort (a, b) -> a.position - b.position
|
||||||
|
|
||||||
|
countDeepAnswers: ->
|
||||||
|
category_ids = @deepChildrenIds()
|
||||||
|
category_ids.push @id
|
||||||
|
|
||||||
|
App.KnowledgeBaseAnswer
|
||||||
|
.records
|
||||||
|
.filter (elem) -> _.contains(category_ids, elem.category_id)
|
||||||
|
.length
|
||||||
|
|
||||||
|
findDeepAnswer: (callback) =>
|
||||||
|
output = _.find(App.KnowledgeBaseAnswer.records, (record) =>
|
||||||
|
if record.category_id isnt @id
|
||||||
|
return false
|
||||||
|
|
||||||
|
callback(record)
|
||||||
|
)
|
||||||
|
|
||||||
|
if output?
|
||||||
|
return output
|
||||||
|
|
||||||
|
_.find(App.KnowledgeBaseCategory.records, (record) =>
|
||||||
|
if record.parent_id isnt @id
|
||||||
|
return false
|
||||||
|
|
||||||
|
record.findDeepAnswer(callback)
|
||||||
|
)
|
||||||
|
|
||||||
|
visibleInternally: (kb_locale) =>
|
||||||
|
@findDeepAnswer( (record) ->
|
||||||
|
record.is_internally_published(kb_locale)
|
||||||
|
)?
|
||||||
|
|
||||||
|
visiblePublicly: (kb_locale) =>
|
||||||
|
@findDeepAnswer( (record) ->
|
||||||
|
record.is_published(kb_locale)
|
||||||
|
)?
|
||||||
|
|
||||||
|
objectName: ->
|
||||||
|
'Category'
|
||||||
|
|
||||||
|
@defaultIconFor: (kb) ->
|
||||||
|
switch kb?.iconset
|
||||||
|
when 'FontAwesome'
|
||||||
|
'f115'
|
||||||
|
when 'anticon'
|
||||||
|
'e662'
|
||||||
|
when 'material'
|
||||||
|
'e94d'
|
||||||
|
when 'ionicons'
|
||||||
|
'f139'
|
||||||
|
when 'Simple-Line-Icons'
|
||||||
|
'e039'
|
|
@ -0,0 +1,8 @@
|
||||||
|
class App.KnowledgeBaseCategoryTranslation extends App.Model
|
||||||
|
@configure 'KnowledgeBaseCategoryTranslation', 'title', 'locale_id', 'category_id'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@extend App.KnowledgeBaseTranslationable
|
||||||
|
@url: @apiPath + '/knowledge_base/category/translations'
|
||||||
|
|
||||||
|
parent: ->
|
||||||
|
App.KnowledgeBaseCategory.find(@category_id)
|
|
@ -0,0 +1,4 @@
|
||||||
|
class App.KnowledgeBaseLayout extends App.Model
|
||||||
|
@configure 'KnowledgeBaseLayout', 'name'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@url: @apiPath + '/knowledge_base/layouts'
|
|
@ -0,0 +1,36 @@
|
||||||
|
class App.KnowledgeBaseLocale extends App.Model
|
||||||
|
@configure 'KnowledgeBaseLocale', 'knowledge_base_id', 'system_locale_id', 'primary'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@url: @apiPath + '/knowledge_base/locales'
|
||||||
|
|
||||||
|
systemLocale: ->
|
||||||
|
App.Locale.find(@system_locale_id)
|
||||||
|
|
||||||
|
urlSuffix: ->
|
||||||
|
"locale/#{@systemLocale().locale}"
|
||||||
|
|
||||||
|
@localeFor: (object) ->
|
||||||
|
if object.kb_locale_id is undefined
|
||||||
|
throw "This object doesn't have locale"
|
||||||
|
|
||||||
|
App.KnowledgeBaseLocale.find object.kb_locale_id
|
||||||
|
|
||||||
|
applyOntoPath: (path) ->
|
||||||
|
path.replace /\/locale\/[\w-]{2,5}/, "/#{@urlSuffix()}"
|
||||||
|
|
||||||
|
attributesForRendering: (path, options = {}) ->
|
||||||
|
{
|
||||||
|
url: @applyOntoPath(path)
|
||||||
|
title: @systemLocale().name
|
||||||
|
}
|
||||||
|
|
||||||
|
@detect: (knowledge_base) ->
|
||||||
|
locale = App.Locale.findByAttribute('locale', App.i18n.get())
|
||||||
|
|
||||||
|
kb_locale = App.KnowledgeBaseLocale
|
||||||
|
.all()
|
||||||
|
.filter (elem) ->
|
||||||
|
elem.knowledge_base_id is knowledge_base.id and elem.system_locale_id is locale.id
|
||||||
|
.pop()
|
||||||
|
|
||||||
|
kb_locale || knowledge_base.primaryKbLocale()
|
|
@ -0,0 +1,7 @@
|
||||||
|
class App.KnowledgeBaseMenuItem extends App.Model
|
||||||
|
@configure 'KnowledgeBaseMenuItem', 'kb_locale_id', 'position', 'title', 'url'
|
||||||
|
|
||||||
|
@using_kb_locale: (kb_locale) ->
|
||||||
|
items = @findAllByAttribute('kb_locale_id', kb_locale.id)
|
||||||
|
items.sort( (a, b) -> if a.position < b.position then -1 else 1)
|
||||||
|
items
|
|
@ -0,0 +1,11 @@
|
||||||
|
class App.KnowledgeBaseTranslation extends App.Model
|
||||||
|
@configure 'KnowledgeBaseTranslation', 'title', 'footer_note'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@extend App.KnowledgeBaseTranslationable
|
||||||
|
@url: @apiPath + '/knowledge_base/translations'
|
||||||
|
@configure_attributes = [
|
||||||
|
{ name: 'title', display: 'Title', tag: 'input' },
|
||||||
|
]
|
||||||
|
|
||||||
|
parent: ->
|
||||||
|
App.KnowledgeBase.find(@knowledge_base_id)
|
|
@ -7,6 +7,10 @@
|
||||||
<span class="tab-badge"><%= item.count %></span>
|
<span class="tab-badge"><%= item.count %></span>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<div class="tab tab-dropdown js-toggle" data-toggle="dropdown">
|
||||||
|
<%- @Icon('dropdown-list') %>
|
||||||
|
<%- @Icon('arrow-down', 'arrow') %>
|
||||||
|
</div>
|
||||||
<ul class="dropdown dropdown--actions dropdown--wide dropdown-menu dropdown-menu-right js-dropdown" role="menu" aria-labelledby="userAction">
|
<ul class="dropdown dropdown--actions dropdown--wide dropdown-menu dropdown-menu-right js-dropdown" role="menu" aria-labelledby="userAction">
|
||||||
<% for item in @items: %>
|
<% for item in @items: %>
|
||||||
<li class="js-dropdownItem hide<%= ' active' if item.active %>" role="presentation" data-target="<%= item.target %>" role="menuitem" tabindex="-1">
|
<li class="js-dropdownItem hide<%= ' active' if item.active %>" role="presentation" data-target="<%= item.target %>" role="menuitem" tabindex="-1">
|
||||||
|
@ -14,10 +18,6 @@
|
||||||
<span class="badge badge--text"><%= item.count %></span>
|
<span class="badge badge--text"><%= item.count %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab tab-dropdown js-toggle" data-toggle="dropdown">
|
|
||||||
<%- @Icon('dropdown-list') %>
|
|
||||||
<%- @Icon('arrow-down', 'arrow') %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
31
app/assets/javascripts/app/views/generic/attachments.jst.eco
Normal file
31
app/assets/javascripts/app/views/generic/attachments.jst.eco
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<% if !_.isEmpty(@attachments): %>
|
||||||
|
<div class="attachments attachments--list">
|
||||||
|
<%- @Icon('paperclip') %>
|
||||||
|
<div class="attachments-title"><%- @attachments.length %> <%- @T('Attached Files') %></div>
|
||||||
|
<% for attachment in @attachments: %>
|
||||||
|
<% if !@C('ui_ticket_zoom_attachments_preview'): %>
|
||||||
|
<div class="attachment attachment--row">
|
||||||
|
<a class="attachment-name u-highlight" href="<%= attachment.url %>" data-type="attachment" <% if @canDownload(content_type): %>download<% else: %>target="_blank"<% end %>><%= attachment.filename %></a>
|
||||||
|
<div class="attachment-size"><%- @humanFileSize(attachment.size) %></div>
|
||||||
|
</div>
|
||||||
|
<% else: %>
|
||||||
|
<% content_type = attachment.preferences['Content-Type'] || attachment.preferences['content_type'] %>
|
||||||
|
<a class="attachment attachment--preview" href="<%= attachment.url %>" data-type="attachment"<% if @canDownload(content_type): %> download<% else: %>target="_blank"<% end %>>
|
||||||
|
<div class="attachment-icon">
|
||||||
|
<% if attachment.preferences && content_type && @ContentTypeIcon(content_type): %>
|
||||||
|
<% if @canPreview(content_type): %>
|
||||||
|
<img src="<%= attachment.preview_url %>">
|
||||||
|
<% else: %>
|
||||||
|
<%- @Icon( @ContentTypeIcon(content_type) ) %>
|
||||||
|
<% end %>
|
||||||
|
<% else: %>
|
||||||
|
<%- @Icon('file-unknown') %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<span class="attachment-name u-highlight"><%= attachment.filename %></span>
|
||||||
|
<div class="attachment-size"><%- @humanFileSize(attachment.size) %></div>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue