Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27f4405b9a | ||
|
|
0cfe32ca87 | ||
|
|
67d518eab1 | ||
|
|
31b02d8770 | ||
|
|
483d36ec78 | ||
|
|
6e14152f45 | ||
|
|
6baf144b23 | ||
|
|
4cd46cfee4 | ||
|
|
90f23fd150 | ||
|
|
8a769b4f67 | ||
|
|
8fddcb562a | ||
|
|
50ac9d1425 | ||
|
|
86c0f001a8 | ||
|
|
fbaa76d386 | ||
|
|
260aeb1276 | ||
|
|
597e6a535b | ||
|
|
a51103363d | ||
|
|
fc028a09fc | ||
|
|
c71d937418 | ||
|
|
1d75238ede | ||
|
|
ec72163f80 | ||
|
|
bc703d4d5a | ||
|
|
8300e2b147 | ||
|
|
fe9ddad5e4 | ||
|
|
d1107375ed | ||
|
|
c5266fae1b | ||
|
|
979314a718 | ||
|
|
46bfcf1201 | ||
|
|
d6d4e789f1 | ||
|
|
f4557ca3c3 | ||
|
|
bf4df2a123 | ||
|
|
7663724cb1 | ||
|
|
5739774139 | ||
|
|
30572f2e5d | ||
|
|
1e2f95d33f | ||
|
|
f2800594bc | ||
|
|
8a0d1ba69d | ||
|
|
56fe514ade | ||
|
|
61523ae0e6 | ||
|
|
6155304366 | ||
|
|
a7f5c4ea2c | ||
|
|
6849ecc717 | ||
|
|
250a9abdb9 | ||
|
|
8fda74cdb3 | ||
|
|
9a37029906 | ||
|
|
89d64b2ef4 | ||
|
|
97a9499a00 | ||
|
|
7a054c275d | ||
|
|
aa63f44ceb | ||
|
|
6d42717cd2 | ||
|
|
9dbb3a96b8 | ||
|
|
46e1a1956a | ||
|
|
e13a4105b7 | ||
|
|
7f76afc0d4 | ||
|
|
f5d7b4a0e1 |
@@ -1,5 +1,5 @@
|
||||
default:
|
||||
image: $CI_REGISTRY/docker/zammad-ci:3.2.3
|
||||
image: $CI_REGISTRY/docker/zammad-ci:3.2.4
|
||||
|
||||
tags:
|
||||
- docker
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
alias: ci-service-proxy
|
||||
|
||||
auth:
|
||||
name: $CI_REGISTRY/docker/zammad-auth:develop
|
||||
name: $CI_REGISTRY/docker/zammad-auth:stable
|
||||
alias: ci-service-auth
|
||||
|
||||
mattermost:
|
||||
|
||||
@@ -77,6 +77,11 @@
|
||||
- echo "First characters of tmp/chat.js"
|
||||
- head -c300 tmp/chat.js; echo # Insert newline after chat content
|
||||
- cmp public/assets/chat/chat.js tmp/chat.js
|
||||
- echo "Verify that vendored gems are not world writable"
|
||||
- GEM_FILES=$(find vendor/ -name "*.rb" -perm -002)
|
||||
- if [[ ! -z "$GEM_FILES" ]]; then echo $GEM_FILES; exit 1; fi # Raise error if files were found.
|
||||
- echo "Finally, ensure cleanup.sh passes…"
|
||||
- script/build/cleanup.sh
|
||||
|
||||
'lint: coffee & css':
|
||||
extends:
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
- !reference [.scripts, zammad_db_init]
|
||||
# Then, switch to the current commit, migrate to it and run a few selected tests.
|
||||
- git checkout $CI_COMMIT_SHA
|
||||
- rvm use 3.2.3
|
||||
- rvm use 3.2.4
|
||||
- !reference [.scripts, bundle_install]
|
||||
# Force redis usage, even if it was disabled by the initial configure_environment script of stable.
|
||||
- export REDIS_URL=redis://redis
|
||||
|
||||
@@ -373,7 +373,7 @@ RSpec/DescribeMethod:
|
||||
RSpec/NestedGroups:
|
||||
Max: 6
|
||||
|
||||
RSpec/Rails/AvoidSetupHook:
|
||||
RSpecRails/AvoidSetupHook:
|
||||
Exclude:
|
||||
- "test/**/*"
|
||||
|
||||
@@ -388,7 +388,7 @@ RSpec/AnyInstance:
|
||||
RSpec/SubjectStub:
|
||||
Enabled: false
|
||||
|
||||
RSpec/Rails/InferredSpecType:
|
||||
RSpecRails/InferredSpecType:
|
||||
Description: 'Identifies redundant spec type.'
|
||||
Enabled: false # We use types to add DSL to rspec.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.2.3
|
||||
3.2.4
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,9 +1,27 @@
|
||||
# Change Log
|
||||
|
||||
## [6.3.0](https://github.com/zammad/zammad/tree/6.3.0) (2024-xx-xx)
|
||||
## [6.3.1](https://github.com/zammad/zammad/tree/6.3.1) (2024-05-15)
|
||||
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/6.2.0...6.3.0)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/6.3.0...6.3.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- User login flow is interrupted when two-factor authentication method security keys is used [5156](https://github.com/zammad/zammad/issues/5156) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[authentication](https://github.com/zammad/zammad/labels/authentication)]
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Ticket history shows html when updating articles [5168](https://github.com/zammad/zammad/issues/5168) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- contrib/packager.io/preinstall.sh does not check the db adapter setting and fails if both psql and mysql are installed [5142](https://github.com/zammad/zammad/issues/5142) [[needs verification](https://github.com/zammad/zammad/labels/needs%20verification)]
|
||||
- fixes #5142| fix db check in preinstall.sh [5143](https://github.com/zammad/zammad/pull/5143)
|
||||
- Public Knowledge Base Bread crumbs unexpectly translated [5145](https://github.com/zammad/zammad/issues/5145) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
|
||||
- Ticket templates creation interface is broken if a Ticket attribute has a lengthy name. [5000](https://github.com/zammad/zammad/issues/5000) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- IllegalArgumentException on reports with time range in report profile and time selection in the report UI [5105](https://github.com/zammad/zammad/issues/5105) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
|
||||
- [CTI] Caller is not identified if the caller has only entered a telephone number (no firstname, lastname, email). [5117](https://github.com/zammad/zammad/issues/5117) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- ".value" in text modules for external data sources breaks ticket zoom [5115](https://github.com/zammad/zammad/issues/5115) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[variable processing](https://github.com/zammad/zammad/labels/variable%20processing)]
|
||||
- Wrong background color for ticket creation links in light mode [5144](https://github.com/zammad/zammad/issues/5144) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
|
||||
- Fetching LdapSource for unknown users fails [5101](https://github.com/zammad/zammad/issues/5101) [[bug](https://github.com/zammad/zammad/labels/bug)] [[LDAP](https://github.com/zammad/zammad/labels/LDAP)]
|
||||
- Unable to send WhatsApp automatic reminders after 23 hours [5139](https://github.com/zammad/zammad/issues/5139) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)] [[localization](https://github.com/zammad/zammad/labels/localization)]
|
||||
- Reporting: Worse performance because UI always request all aggregation data instead of the selection [5138](https://github.com/zammad/zammad/issues/5138) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)] [[performance](https://github.com/zammad/zammad/labels/performance)]
|
||||
- Reporting: No button and output when no tickets found in a time range [5137](https://github.com/zammad/zammad/issues/5137) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
|
||||
- Once applied kb editor permissions cannot be revoked [5123](https://github.com/zammad/zammad/issues/5123) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
|
||||
- Duplicates in calendar [5076](https://github.com/zammad/zammad/issues/5076) [[bug](https://github.com/zammad/zammad/labels/bug)] [[calendar](https://github.com/zammad/zammad/labels/calendar)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:20-slim as node
|
||||
|
||||
|
||||
FROM ruby:3.2.3-slim AS builder
|
||||
FROM ruby:3.2.4-slim AS builder
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG RAILS_ENV=production
|
||||
ARG ZAMMAD_DIR=/opt/zammad
|
||||
@@ -15,7 +15,7 @@ RUN contrib/docker/setup.sh builder
|
||||
|
||||
# note: zammad is currently incompatible to alpine because of:
|
||||
# https://github.com/docker-library/ruby/issues/113
|
||||
FROM ruby:3.2.3-slim
|
||||
FROM ruby:3.2.4-slim
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG ZAMMAD_USER=zammad
|
||||
ENV RAILS_ENV=production
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -3,7 +3,7 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# core - base
|
||||
ruby '3.2.3'
|
||||
ruby '3.2.4'
|
||||
gem 'rails', '~> 7.0.8'
|
||||
|
||||
# core - rails additions
|
||||
|
||||
@@ -334,7 +334,7 @@ GEM
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.0305)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
mini_portile2 (2.8.6)
|
||||
minitest (5.22.3)
|
||||
minitest-profile (0.0.2)
|
||||
msgpack (1.7.2)
|
||||
@@ -361,7 +361,7 @@ GEM
|
||||
net-protocol
|
||||
nio4r (2.7.0)
|
||||
nkf (0.2.0)
|
||||
nokogiri (1.16.3)
|
||||
nokogiri (1.16.5)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nori (2.7.0)
|
||||
@@ -601,7 +601,7 @@ GEM
|
||||
activesupport (>= 5.2.0)
|
||||
simple_oauth (0.3.1)
|
||||
simple_po_parser (1.1.6)
|
||||
simpleidn (0.2.1)
|
||||
simpleidn (0.2.2)
|
||||
unf (~> 0.1.4)
|
||||
slack-notifier (2.4.0)
|
||||
slack-ruby-client (2.3.0)
|
||||
@@ -846,7 +846,7 @@ DEPENDENCIES
|
||||
zendesk_api
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.3p157
|
||||
ruby 3.2.4p170
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.1
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ------------------ |
|
||||
| 6.2.x | :white_check_mark: |
|
||||
| <= 6.1.x | :x: |
|
||||
| 6.3.x | :white_check_mark: |
|
||||
| <= 6.2.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ class App.GenericHistory extends App.ControllerModal
|
||||
for item in items
|
||||
if item.object is 'Ticket::Article'
|
||||
item.object = 'Article'
|
||||
if item.attribute is 'body'
|
||||
item.value_from = App.Utils.html2text(item.value_from)
|
||||
item.value_to = App.Utils.html2text(item.value_to)
|
||||
if item.object is 'Ticket::SharedDraftZoom'
|
||||
item.object = 'Draft'
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class App.UiElement.language extends App.UiElement.ApplicationUiElement
|
||||
|
||||
if attribute.show_system_default_option
|
||||
locale_default_name = App.Locale.findByAttribute('locale', App.Config.get('locale_default')).name
|
||||
attribute.options.unshift({ name: __('System default') + " (#{locale_default_name})", value: 'system' })
|
||||
attribute.options.unshift({ name: App.i18n.translatePlain('System default - %s', locale_default_name), value: 'system' })
|
||||
|
||||
# set default value
|
||||
attribute.default = if attribute.show_system_default_option then 'system' else App.Config.get('locale_default')
|
||||
|
||||
@@ -17,7 +17,7 @@ class App.UiElement.timezone extends App.UiElement.ApplicationUiElement
|
||||
|
||||
if attribute.show_system_default_option
|
||||
timezone_default_name = _.find(attribute.options, (option) -> option.value == App.Config.get('timezone_default')).name
|
||||
attribute.options.unshift({ name: __('System default') + " (#{timezone_default_name})", value: 'system' })
|
||||
attribute.options.unshift({ name: App.i18n.translatePlain('System default - %s', timezone_default_name), value: 'system' })
|
||||
|
||||
# set default value
|
||||
attribute.default = if attribute.show_system_default_option then 'system' else App.Config.get('timezone_default')
|
||||
|
||||
@@ -616,6 +616,11 @@ class App.TicketCreate extends App.Controller
|
||||
# to replace in text modules properly
|
||||
params.customer = App.User.find(params.customer_id) || {}
|
||||
|
||||
# show selected user display name in customer attribute in UI
|
||||
if _.isEmpty(@el.find('input[name=customer_id_completion]').val())
|
||||
if params.customer && params.customer.displayName
|
||||
@el.find('input[name=customer_id_completion]').val(params.customer.displayName())
|
||||
|
||||
@sidebarWidget.render(params)
|
||||
@textModule.reload(
|
||||
config: App.Config.all()
|
||||
|
||||
477
app/assets/javascripts/app/controllers/article_view/item.coffee
Normal file
477
app/assets/javascripts/app/controllers/article_view/item.coffee
Normal file
@@ -0,0 +1,477 @@
|
||||
class App.ArticleViewItem extends App.ControllerObserver
|
||||
model: 'TicketArticle'
|
||||
observe:
|
||||
from: true
|
||||
to: true
|
||||
cc: true
|
||||
subject: true
|
||||
body: true
|
||||
internal: true
|
||||
preferences: true
|
||||
|
||||
elements:
|
||||
'.textBubble-content': 'textBubbleContent'
|
||||
'.textBubble-content img': 'textBubbleImages'
|
||||
'.textBubble-overflowContainer': 'textBubbleOverflowContainer'
|
||||
|
||||
events:
|
||||
'click .article-meta-permanent': 'toggleMetaWithDelay'
|
||||
'click .textBubble': 'toggleMetaWithDelay'
|
||||
'click .textBubble a': 'stopPropagation'
|
||||
'click .js-toggleFold': 'toggleFold'
|
||||
'click .richtext-content img': 'imageView'
|
||||
'click .attachments img': 'imageView'
|
||||
'click .file-calendar .js-preview': 'calendarView'
|
||||
'click .js-securityRetryProcess': 'retrySecurityProcess'
|
||||
'click .js-retryWhatsAppAttachmentDownload': 'retryWhatsAppAttachmentDownload'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@seeMoreOpen = false
|
||||
|
||||
# set expand of text area only once
|
||||
@controllerBind('ui::ticket::shown', (data) =>
|
||||
return if data.ticket_id.toString() isnt @ticket.id.toString()
|
||||
|
||||
# set highlighter
|
||||
@setHighlighter()
|
||||
|
||||
# set see more
|
||||
@setSeeMore()
|
||||
)
|
||||
|
||||
setHighlighter: =>
|
||||
return if @el.is(':hidden')
|
||||
# use delay do no ui blocking
|
||||
#@highlighter.loadHighlights(@object_id)
|
||||
d = =>
|
||||
if @highlighter
|
||||
@highlighter.loadHighlights(@object_id)
|
||||
@delay(d, 200)
|
||||
|
||||
render: (article) =>
|
||||
|
||||
# set @el attributes
|
||||
@el.addClass("ticket-article-item #{article.sender.name.toLowerCase()}")
|
||||
@el.attr('data-id', article.id)
|
||||
@el.attr('id', "article-#{article.id}")
|
||||
if article.internal
|
||||
@el.addClass('is-internal')
|
||||
else
|
||||
@el.removeClass('is-internal')
|
||||
|
||||
# check if email link needs to be updated
|
||||
links = clone(article.preferences.links) || []
|
||||
if article.type.name is 'email'
|
||||
link =
|
||||
name: __('Raw')
|
||||
url: "#{@Config.get('api_path')}/ticket_article_plain/#{article.id}"
|
||||
target: '_blank'
|
||||
links.push link
|
||||
|
||||
# attachments prepare
|
||||
attachments = App.TicketArticle.contentAttachments(article)
|
||||
if article.attachments
|
||||
for attachment in article.attachments
|
||||
|
||||
dispositionParams = ''
|
||||
if attachment?.preferences['Content-Type'] isnt 'text/html'
|
||||
dispositionParams = '?disposition=attachment'
|
||||
|
||||
attachment.url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}#{dispositionParams}"
|
||||
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
|
||||
link =
|
||||
url: "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?disposition=attachment"
|
||||
name: __('Original Formatting')
|
||||
target: '_blank'
|
||||
links.push link
|
||||
|
||||
# prepare html body
|
||||
if article.content_type is 'text/html'
|
||||
body = article.body
|
||||
if article.preferences && article.preferences.signature_detection
|
||||
signatureDetected = '<span class="js-signatureMarker"></span>'
|
||||
body = body.replace(signatureDetected, '')
|
||||
body = body.split('<br>')
|
||||
body.splice(article.preferences.signature_detection, 0, signatureDetected)
|
||||
body = body.join('<br>')
|
||||
else
|
||||
body = App.Utils.signatureIdentifyByHtml(body)
|
||||
article['html'] = body
|
||||
else
|
||||
|
||||
# client signature detection
|
||||
bodyHtml = App.Utils.text2html(article.body)
|
||||
article['html'] = App.Utils.signatureIdentifyByPlaintext(bodyHtml)
|
||||
|
||||
# if no signature detected or within first 25 lines, check if signature got detected in backend
|
||||
if article['html'] is bodyHtml || (article.preferences && article.preferences.signature_detection < 25)
|
||||
signatureDetected = false
|
||||
body = article.body
|
||||
if article.preferences && article.preferences.signature_detection
|
||||
signatureDetected = '########SIGNATURE########'
|
||||
# coffeelint: disable=no_unnecessary_double_quotes
|
||||
body = body.split("\n")
|
||||
body.splice(article.preferences.signature_detection, 0, signatureDetected)
|
||||
body = body.join("\n")
|
||||
# coffeelint: enable=no_unnecessary_double_quotes
|
||||
if signatureDetected
|
||||
body = App.Utils.textCleanup(body)
|
||||
article['html'] = App.Utils.text2html(body)
|
||||
article['html'] = article['html'].replace(signatureDetected, '<span class="js-signatureMarker"></span>')
|
||||
|
||||
if article.preferences.delivery_message
|
||||
@html App.view('ticket_zoom/article_view_delivery_failed')(
|
||||
ticket: @ticket
|
||||
article: article
|
||||
attachments: attachments
|
||||
links: links
|
||||
)
|
||||
return
|
||||
if article.sender.name is 'System' && article.type.name isnt 'note'
|
||||
#if article.sender.name is 'System' && article.preferences.perform_origin is 'trigger'
|
||||
@html App.view('ticket_zoom/article_view_system')(
|
||||
ticket: @ticket
|
||||
article: article
|
||||
attachments: attachments
|
||||
links: links
|
||||
)
|
||||
return
|
||||
|
||||
if article.preferences?.whatsapp
|
||||
icon = null
|
||||
msg = null
|
||||
if article.preferences?.whatsapp?.timestamp_read
|
||||
icon = 'double-checkmark'
|
||||
msg = __('read by the customer')
|
||||
else if article.preferences?.whatsapp?.timestamp_delivered
|
||||
icon = 'double-checkmark-outline'
|
||||
msg = __('delivered to the customer')
|
||||
else if article.preferences?.whatsapp?.timestamp_sent
|
||||
icon = 'checkmark-outline'
|
||||
msg = __('sent to the customer')
|
||||
|
||||
article['delivery_status_icon'] = icon
|
||||
article['delivery_status_message'] = msg
|
||||
|
||||
@html App.view('ticket_zoom/article_view')(
|
||||
ticket: @ticket
|
||||
article: article
|
||||
attachments: App.view('generic/attachments')(attachments: attachments, has_body: !!article.html)
|
||||
links: links
|
||||
)
|
||||
|
||||
new App.WidgetAvatar(
|
||||
el: @$('.js-avatar')
|
||||
object_id: article.origin_by_id || article.created_by_id
|
||||
size: 40
|
||||
)
|
||||
|
||||
@articleActions = new App.TicketZoomArticleActions(
|
||||
el: @$('.js-article-actions')
|
||||
ticket: @ticket
|
||||
article: article
|
||||
lastAttributes: @lastAttributes
|
||||
form_id: @form_id
|
||||
)
|
||||
|
||||
# set see more
|
||||
@shown = false
|
||||
a = =>
|
||||
@setSeeMore()
|
||||
@delay(a, 50)
|
||||
|
||||
# set highlighter
|
||||
@setHighlighter()
|
||||
|
||||
# set see more options
|
||||
setSeeMore: =>
|
||||
return if @el.is(':hidden')
|
||||
return if @shown
|
||||
@shown = true
|
||||
|
||||
@textBubbleImages.each (i, el) =>
|
||||
if !el.complete
|
||||
$(el).one 'load', @measureSeeMore
|
||||
|
||||
@measureSeeMore()
|
||||
|
||||
measureSeeMore: =>
|
||||
maxHeight = 560
|
||||
minHeight = 90
|
||||
bubbleContent = @textBubbleContent
|
||||
bubbleOverflowContainer = @textBubbleOverflowContainer
|
||||
|
||||
# expand if see more is already clicked
|
||||
if @seeMoreOpen
|
||||
bubbleContent.css('height', 'auto')
|
||||
else
|
||||
# reset bubble height and "see more" opacity
|
||||
bubbleContent.css('height', '')
|
||||
bubbleOverflowContainer.css('opacity', '')
|
||||
|
||||
# remember offset of "see more"
|
||||
signatureMarker = bubbleContent.find('.js-signatureMarker')
|
||||
if !signatureMarker.get(0)
|
||||
signatureMarker = bubbleContent.find('div [data-signature=true]')
|
||||
offsetTop = signatureMarker.position()
|
||||
|
||||
# safari - workaround
|
||||
# in safari sometimes the marker is directly on top via .top and inspector but it isn't
|
||||
# in this case use the next element
|
||||
if offsetTop && offsetTop.top is 0
|
||||
offsetTop = signatureMarker.next('div, p, br').position()
|
||||
|
||||
# remember bubble content height
|
||||
bubbleContentHeight = bubbleContent.height()
|
||||
|
||||
# get marker height
|
||||
if offsetTop
|
||||
markerHeight = offsetTop.top
|
||||
|
||||
# if signature marker exists and height is within maxHeight
|
||||
if markerHeight && markerHeight < maxHeight
|
||||
newHeight = markerHeight + 30
|
||||
if newHeight < minHeight
|
||||
newHeight = minHeight
|
||||
|
||||
bubbleContent.attr('data-height', bubbleContentHeight + 30)
|
||||
bubbleContent.attr('data-height-origin', newHeight)
|
||||
bubbleContent.css('height', "#{newHeight}px")
|
||||
bubbleOverflowContainer.removeClass('hide')
|
||||
|
||||
# if height is higher then maxHeight
|
||||
else if bubbleContentHeight > maxHeight
|
||||
bubbleContent.attr('data-height', bubbleContentHeight + 30)
|
||||
bubbleContent.attr('data-height-origin', maxHeight)
|
||||
newHeight = if @seeMoreOpen then 'auto' else "#{maxHeight}px"
|
||||
bubbleContent.css('height', newHeight)
|
||||
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(@label)
|
||||
bubbleOverflowContainer.removeClass('hide')
|
||||
else
|
||||
bubbleOverflowContainer.addClass('hide')
|
||||
|
||||
retrySecurityProcess: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
article_id = $(e.target).closest('.ticket-article-item').data('id')
|
||||
article = App.TicketArticle.find(article_id)
|
||||
|
||||
@ajax(
|
||||
id: 'retrySecurityProcess'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/ticket_articles/#{article_id}/retry_security_process"
|
||||
processData: true
|
||||
success: (encryption_data, status, xhr) =>
|
||||
for data in encryption_data
|
||||
continue if article.preferences.security.type isnt data.type
|
||||
|
||||
if data.sign.success
|
||||
@notify
|
||||
type: 'success'
|
||||
msg: App.i18n.translateContent('The signature was successfully verified.')
|
||||
else if data.sign.comment
|
||||
comment = App.i18n.translateContent('Signature verification failed!') + ' ' + App.i18n.translateContent(data.sign.comment || '', data.sign.commentPlaceholders)
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: comment
|
||||
timeout: 2000
|
||||
|
||||
if data.encryption.success
|
||||
@notify
|
||||
type: 'success'
|
||||
msg: App.i18n.translateContent('Decryption was successful.')
|
||||
else if data.encryption.comment
|
||||
comment = App.i18n.translateContent('Decryption failed!') + ' ' + App.i18n.translateContent(data.encryption.comment || '', data.encryption.commentPlaceholders)
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: comment
|
||||
timeout: 2000
|
||||
|
||||
error: (xhr) =>
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent('The retried security process failed!')
|
||||
)
|
||||
|
||||
retryWhatsAppAttachmentDownload: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
article_id = $(e.target).closest('.ticket-article-item').data('id')
|
||||
|
||||
@ajax(
|
||||
id: 'retryWhatsAppAttachmentDownload'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/ticket_articles/#{article_id}/retry_whatsapp_attachment_download"
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
@notify
|
||||
type: 'success'
|
||||
msg: App.i18n.translateContent('Downloading attachments…')
|
||||
|
||||
error: (data, status, xhr) =>
|
||||
details = data.responseJSON || {}
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent(details.error)
|
||||
)
|
||||
|
||||
stopPropagation: (e) ->
|
||||
e.stopPropagation()
|
||||
|
||||
toggleMetaWithDelay: (e) =>
|
||||
# allow double click select
|
||||
# by adding a delay to the toggle
|
||||
delay = 300
|
||||
|
||||
article = $(e.target).closest('.ticket-article-item')
|
||||
if @elementContainsSelection(article.get(0))
|
||||
@stopPropagation(e)
|
||||
return false
|
||||
|
||||
if @lastClick and +new Date - @lastClick < delay
|
||||
clearTimeout(@toggleMetaTimeout)
|
||||
else
|
||||
@toggleMetaTimeout = setTimeout(@toggleMeta, delay, e)
|
||||
@lastClick = +new Date
|
||||
|
||||
toggleMeta: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
animSpeed = 300
|
||||
article = $(e.target).closest('.ticket-article-item')
|
||||
metaTopClip = article.find('.article-meta-clip.top')
|
||||
metaBottomClip = article.find('.article-meta-clip.bottom')
|
||||
metaTop = article.find('.article-content-meta.top')
|
||||
metaBottom = article.find('.article-content-meta.bottom')
|
||||
|
||||
if @elementContainsSelection(article.get(0))
|
||||
@stopPropagation(e)
|
||||
return false
|
||||
|
||||
if !metaTop.hasClass('hide')
|
||||
article.removeClass('state--folde-out')
|
||||
|
||||
# scroll back up
|
||||
article.velocity 'scroll',
|
||||
container: article.scrollParent()
|
||||
offset: -article.offset().top - metaTop.outerHeight()
|
||||
duration: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
|
||||
metaTop.velocity
|
||||
properties:
|
||||
translateY: 0
|
||||
opacity: [ 0, 1 ]
|
||||
options:
|
||||
speed: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
complete: -> metaTop.addClass('hide')
|
||||
|
||||
metaBottom.velocity
|
||||
properties:
|
||||
translateY: [ -metaBottom.outerHeight(), 0 ]
|
||||
opacity: [ 0, 1 ]
|
||||
options:
|
||||
speed: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
complete: -> metaBottom.addClass('hide')
|
||||
|
||||
metaTopClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
|
||||
metaBottomClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
|
||||
else
|
||||
article.addClass('state--folde-out')
|
||||
metaBottom.removeClass('hide')
|
||||
metaTop.removeClass('hide')
|
||||
|
||||
# balance out the top meta height by scrolling down
|
||||
article.velocity('scroll',
|
||||
container: article.scrollParent()
|
||||
offset: -article.offset().top + metaTop.outerHeight()
|
||||
duration: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
)
|
||||
|
||||
metaTop.velocity
|
||||
properties:
|
||||
translateY: [ 0, metaTop.outerHeight() ]
|
||||
opacity: [ 1, 0 ]
|
||||
options:
|
||||
speed: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
|
||||
metaBottom.velocity
|
||||
properties:
|
||||
translateY: [ 0, -metaBottom.outerHeight() ]
|
||||
opacity: [ 1, 0 ]
|
||||
options:
|
||||
speed: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
|
||||
metaTopClip.velocity({ height: metaTop.outerHeight() }, animSpeed, 'easeOutQuad')
|
||||
metaBottomClip.velocity({ height: metaBottom.outerHeight() }, animSpeed, 'easeOutQuad')
|
||||
|
||||
toggleFold: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
bubbleContent = @textBubbleContent
|
||||
bubbleOverflowContainer = @textBubbleOverflowContainer
|
||||
|
||||
if @seeMoreOpen
|
||||
@label = App.i18n.translateContent('See more')
|
||||
height = bubbleContent.attr('data-height-origin')
|
||||
@seeMoreOpen = false
|
||||
else
|
||||
@label = App.i18n.translateContent('See less')
|
||||
height = bubbleContent.attr('data-height')
|
||||
@seeMoreOpen = true
|
||||
|
||||
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(@label)
|
||||
|
||||
bubbleContent.velocity
|
||||
properties:
|
||||
height: height
|
||||
options:
|
||||
duration: 300
|
||||
|
||||
isOrContains: (node, container) ->
|
||||
while node
|
||||
if node is container
|
||||
return true
|
||||
node = node.parentNode
|
||||
false
|
||||
|
||||
elementContainsSelection: (el) ->
|
||||
sel = window.getSelection()
|
||||
if sel.rangeCount > 0 && sel.toString()
|
||||
for i in [0..sel.rangeCount-1]
|
||||
if !@isOrContains(sel.getRangeAt(i).commonAncestorContainer, el)
|
||||
return false
|
||||
return true
|
||||
false
|
||||
|
||||
remove: =>
|
||||
@el.remove()
|
||||
|
||||
imageView: (e) ->
|
||||
# take care of images surrounded by a link
|
||||
if e.target && e.target.parentNode && e.target.parentNode.nodeName.toLowerCase() == 'a'
|
||||
return false
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML, parentElement: $(e.currentTarget))
|
||||
|
||||
calendarView: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
parentElement = $(e.target).closest('.attachment.file-calendar')
|
||||
new App.TicketZoomArticleCalendarView(calendar: parentElement.get(0).outerHTML)
|
||||
|
||||
updateFormId: (newFormId) ->
|
||||
@articleActions?.form_id = newFormId
|
||||
@@ -6,7 +6,7 @@ class Job extends App.ControllerSubContent
|
||||
|
||||
@fetchTimezones()
|
||||
|
||||
@genericController = new App.ControllerGenericIndex(
|
||||
@genericController = new Index(
|
||||
el: @el
|
||||
id: @id
|
||||
genericObject: 'Job'
|
||||
@@ -46,4 +46,28 @@ class Job extends App.ControllerSubContent
|
||||
App.Config.set('timezones', data.timezones)
|
||||
)
|
||||
|
||||
class Index extends App.ControllerGenericIndex
|
||||
newControllerClass: -> New
|
||||
editControllerClass: -> Edit
|
||||
|
||||
ModalContentFormModelMixin =
|
||||
events:
|
||||
'change input[name="execution_localization"]': 'executionLocalizationChanged'
|
||||
|
||||
contentFormModel: ->
|
||||
params = @contentFormParams() or {}
|
||||
attrs = _.clone(App[ @genericObject ].configure_attributes)
|
||||
|
||||
attrs = @prepareExecutionLocalizationAttributes(params, attrs)
|
||||
|
||||
{ configure_attributes: attrs }
|
||||
|
||||
class Edit extends App.ControllerGenericEdit
|
||||
@include App.ExecutionLocalizationMixin
|
||||
@include ModalContentFormModelMixin
|
||||
|
||||
class New extends App.ControllerGenericNew
|
||||
@include App.ExecutionLocalizationMixin
|
||||
@include ModalContentFormModelMixin
|
||||
|
||||
App.Config.set('Job', { prio: 3400, name: __('Scheduler'), parent: '#manage', target: '#manage/job', controller: Job, permission: ['admin.scheduler'] }, 'NavBarAdmin')
|
||||
|
||||
@@ -295,7 +295,7 @@ class Download extends App.Controller
|
||||
@tableUpdate()
|
||||
|
||||
tableRender: (tickets, count) =>
|
||||
if _.isEmpty(tickets)
|
||||
if !@params.downloadBackendSelected
|
||||
@$('.js-dataDownloadButton').html('')
|
||||
@$('.js-dataDownloadTable').html('')
|
||||
return
|
||||
@@ -305,10 +305,14 @@ class Download extends App.Controller
|
||||
if value
|
||||
profile_id = key
|
||||
downloadUrl = "#{@apiPath}/reports/sets?sheet=true;metric=#{@params.metric};year=#{@params.year};month=#{@params.month};week=#{@params.week};day=#{@params.day};timeRange=#{@params.timeRange};profile_id=#{profile_id};downloadBackendSelected=#{@params.downloadBackendSelected}"
|
||||
@$('.js-dataDownloadButton').html(App.view('report/download_button')(
|
||||
count: count
|
||||
downloadUrl: downloadUrl
|
||||
))
|
||||
|
||||
if count > 0
|
||||
@$('.js-dataDownloadButton').html(App.view('report/download_button')(
|
||||
count: count
|
||||
downloadUrl: downloadUrl
|
||||
))
|
||||
else
|
||||
@$('.js-dataDownloadButton').html('')
|
||||
|
||||
openTicket = (id,e) =>
|
||||
ticket = App.Ticket.findNative(id)
|
||||
@@ -355,27 +359,30 @@ class Download extends App.Controller
|
||||
number:
|
||||
[ callbackLinkToTicket, callbackTicketTitleAdd ]
|
||||
|
||||
if !@table
|
||||
@table = new App.ControllerTable(params)
|
||||
else
|
||||
@table.update(objects: tickets)
|
||||
@table.releaseController() if @table
|
||||
@table = new App.ControllerTable(params)
|
||||
|
||||
tableUpdate: =>
|
||||
return @tableRender([], 0) if !@params.downloadBackendSelected
|
||||
|
||||
state = {
|
||||
metric: @params.metric
|
||||
year: @params.year
|
||||
month: @params.month
|
||||
week: @params.week
|
||||
day: @params.day
|
||||
timeRange: @params.timeRange
|
||||
profiles: @params.profileSelected
|
||||
downloadBackendSelected: @params.downloadBackendSelected
|
||||
}
|
||||
return if _.isEqual(@lastState, state)
|
||||
@lastState = $.extend(true, {}, state)
|
||||
|
||||
@ajax(
|
||||
id: 'report_download'
|
||||
type: 'POST'
|
||||
url: @apiPath + '/reports/sets'
|
||||
data: JSON.stringify(
|
||||
metric: @params.metric
|
||||
year: @params.year
|
||||
month: @params.month
|
||||
week: @params.week
|
||||
day: @params.day
|
||||
timeRange: @params.timeRange
|
||||
profiles: @params.profileSelected
|
||||
backends: @params.backendSelected
|
||||
downloadBackendSelected: @params.downloadBackendSelected
|
||||
)
|
||||
data: JSON.stringify(state)
|
||||
processData: true
|
||||
success: (data) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
@@ -582,7 +589,8 @@ class Sidebar extends App.Controller
|
||||
return if $(e.target).closest('.panel').find('.collapse.in').get(0)
|
||||
metric = $(e.target).closest('.panel').data('metric')
|
||||
return if @params.metric is metric
|
||||
@params.metric = metric
|
||||
@params.metric = metric
|
||||
@params.downloadBackendSelected = undefined
|
||||
App.Event.trigger('ui:report:rerender')
|
||||
@ui.storeParams()
|
||||
|
||||
|
||||
@@ -880,13 +880,17 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||
file.type || file.contentType || file.preferences?['Content-Type'] || file.preferences?['Mime-Type']
|
||||
|
||||
checkBodyEnsureNoCaption: =>
|
||||
@bodyEnsureNoCaption?(@attachments.map (file) => @attachmentContentType(file))
|
||||
return false if !@bodyEnsureNoCaption
|
||||
|
||||
@bodyEnsureNoCaption(@attachments.map (file) => @attachmentContentType(file))
|
||||
|
||||
checkBodyAllowNoCaption: =>
|
||||
@bodyAllowNoCaption?(@attachments)
|
||||
return false if !@bodyAllowNoCaption
|
||||
|
||||
@bodyAllowNoCaption(@attachments)
|
||||
|
||||
checkBodyAllowEmpty: =>
|
||||
!!@checkBodyEnsureNoCaption() || @checkBodyAllowNoCaption()
|
||||
@checkBodyEnsureNoCaption() || @checkBodyAllowNoCaption()
|
||||
|
||||
evaluateAttachmentsList: =>
|
||||
@toggleBodyEnsureNoCaption @checkBodyEnsureNoCaption()
|
||||
|
||||
@@ -14,7 +14,7 @@ class App.TicketZoomArticleView extends App.Controller
|
||||
controllerKey = ticket_article_id.toString()
|
||||
if !@articleController[controllerKey]
|
||||
el = $('<div></div>')
|
||||
@articleController[controllerKey] = new ArticleViewItem(
|
||||
@articleController[controllerKey] = new App.ArticleViewItem(
|
||||
ticket: @ticket
|
||||
object_id: ticket_article_id
|
||||
el: el
|
||||
@@ -65,481 +65,3 @@ class App.TicketZoomArticleView extends App.Controller
|
||||
|
||||
for id, viewItem of @articleController
|
||||
viewItem.updateFormId(newFormId)
|
||||
|
||||
class ArticleViewItem extends App.ControllerObserver
|
||||
model: 'TicketArticle'
|
||||
observe:
|
||||
from: true
|
||||
to: true
|
||||
cc: true
|
||||
subject: true
|
||||
body: true
|
||||
internal: true
|
||||
preferences: true
|
||||
|
||||
elements:
|
||||
'.textBubble-content': 'textBubbleContent'
|
||||
'.textBubble-content img': 'textBubbleImages'
|
||||
'.textBubble-overflowContainer': 'textBubbleOverflowContainer'
|
||||
|
||||
events:
|
||||
'click .article-meta-permanent': 'toggleMetaWithDelay'
|
||||
'click .textBubble': 'toggleMetaWithDelay'
|
||||
'click .textBubble a': 'stopPropagation'
|
||||
'click .js-toggleFold': 'toggleFold'
|
||||
'click .richtext-content img': 'imageView'
|
||||
'click .attachments img': 'imageView'
|
||||
'click .file-calendar .js-preview': 'calendarView'
|
||||
'click .js-securityRetryProcess': 'retrySecurityProcess'
|
||||
'click .js-retryWhatsAppAttachmentDownload': 'retryWhatsAppAttachmentDownload'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@seeMoreOpen = false
|
||||
|
||||
# set expand of text area only once
|
||||
@controllerBind('ui::ticket::shown', (data) =>
|
||||
return if data.ticket_id.toString() isnt @ticket.id.toString()
|
||||
|
||||
# set highlighter
|
||||
@setHighlighter()
|
||||
|
||||
# set see more
|
||||
@setSeeMore()
|
||||
)
|
||||
|
||||
setHighlighter: =>
|
||||
return if @el.is(':hidden')
|
||||
# use delay do no ui blocking
|
||||
#@highlighter.loadHighlights(@object_id)
|
||||
d = =>
|
||||
if @highlighter
|
||||
@highlighter.loadHighlights(@object_id)
|
||||
@delay(d, 200)
|
||||
|
||||
render: (article) =>
|
||||
|
||||
# set @el attributes
|
||||
@el.addClass("ticket-article-item #{article.sender.name.toLowerCase()}")
|
||||
@el.attr('data-id', article.id)
|
||||
@el.attr('id', "article-#{article.id}")
|
||||
if article.internal
|
||||
@el.addClass('is-internal')
|
||||
else
|
||||
@el.removeClass('is-internal')
|
||||
|
||||
# check if email link needs to be updated
|
||||
links = clone(article.preferences.links) || []
|
||||
if article.type.name is 'email'
|
||||
link =
|
||||
name: __('Raw')
|
||||
url: "#{@Config.get('api_path')}/ticket_article_plain/#{article.id}"
|
||||
target: '_blank'
|
||||
links.push link
|
||||
|
||||
# attachments prepare
|
||||
attachments = App.TicketArticle.contentAttachments(article)
|
||||
if article.attachments
|
||||
for attachment in article.attachments
|
||||
|
||||
dispositionParams = ''
|
||||
if attachment?.preferences['Content-Type'] isnt 'text/html'
|
||||
dispositionParams = '?disposition=attachment'
|
||||
|
||||
attachment.url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}#{dispositionParams}"
|
||||
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
|
||||
link =
|
||||
url: "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?disposition=attachment"
|
||||
name: __('Original Formatting')
|
||||
target: '_blank'
|
||||
links.push link
|
||||
|
||||
# prepare html body
|
||||
if article.content_type is 'text/html'
|
||||
body = article.body
|
||||
if article.preferences && article.preferences.signature_detection
|
||||
signatureDetected = '<span class="js-signatureMarker"></span>'
|
||||
body = body.replace(signatureDetected, '')
|
||||
body = body.split('<br>')
|
||||
body.splice(article.preferences.signature_detection, 0, signatureDetected)
|
||||
body = body.join('<br>')
|
||||
else
|
||||
body = App.Utils.signatureIdentifyByHtml(body)
|
||||
article['html'] = body
|
||||
else
|
||||
|
||||
# client signature detection
|
||||
bodyHtml = App.Utils.text2html(article.body)
|
||||
article['html'] = App.Utils.signatureIdentifyByPlaintext(bodyHtml)
|
||||
|
||||
# if no signature detected or within first 25 lines, check if signature got detected in backend
|
||||
if article['html'] is bodyHtml || (article.preferences && article.preferences.signature_detection < 25)
|
||||
signatureDetected = false
|
||||
body = article.body
|
||||
if article.preferences && article.preferences.signature_detection
|
||||
signatureDetected = '########SIGNATURE########'
|
||||
# coffeelint: disable=no_unnecessary_double_quotes
|
||||
body = body.split("\n")
|
||||
body.splice(article.preferences.signature_detection, 0, signatureDetected)
|
||||
body = body.join("\n")
|
||||
# coffeelint: enable=no_unnecessary_double_quotes
|
||||
if signatureDetected
|
||||
body = App.Utils.textCleanup(body)
|
||||
article['html'] = App.Utils.text2html(body)
|
||||
article['html'] = article['html'].replace(signatureDetected, '<span class="js-signatureMarker"></span>')
|
||||
|
||||
if article.preferences.delivery_message
|
||||
@html App.view('ticket_zoom/article_view_delivery_failed')(
|
||||
ticket: @ticket
|
||||
article: article
|
||||
attachments: attachments
|
||||
links: links
|
||||
)
|
||||
return
|
||||
if article.sender.name is 'System' && article.type.name isnt 'note'
|
||||
#if article.sender.name is 'System' && article.preferences.perform_origin is 'trigger'
|
||||
@html App.view('ticket_zoom/article_view_system')(
|
||||
ticket: @ticket
|
||||
article: article
|
||||
attachments: attachments
|
||||
links: links
|
||||
)
|
||||
return
|
||||
|
||||
if article.preferences?.whatsapp
|
||||
icon = null
|
||||
msg = null
|
||||
if article.preferences?.whatsapp?.timestamp_read
|
||||
icon = 'double-checkmark'
|
||||
msg = __('read by the customer')
|
||||
else if article.preferences?.whatsapp?.timestamp_delivered
|
||||
icon = 'double-checkmark-outline'
|
||||
msg = __('delivered to the customer')
|
||||
else if article.preferences?.whatsapp?.timestamp_sent
|
||||
icon = 'checkmark-outline'
|
||||
msg = __('sent to the customer')
|
||||
|
||||
article['delivery_status_icon'] = icon
|
||||
article['delivery_status_message'] = msg
|
||||
|
||||
@html App.view('ticket_zoom/article_view')(
|
||||
ticket: @ticket
|
||||
article: article
|
||||
attachments: App.view('generic/attachments')(attachments: attachments, has_body: !!article.html)
|
||||
links: links
|
||||
)
|
||||
|
||||
new App.WidgetAvatar(
|
||||
el: @$('.js-avatar')
|
||||
object_id: article.origin_by_id || article.created_by_id
|
||||
size: 40
|
||||
)
|
||||
|
||||
@articleActions = new App.TicketZoomArticleActions(
|
||||
el: @$('.js-article-actions')
|
||||
ticket: @ticket
|
||||
article: article
|
||||
lastAttributes: @lastAttributes
|
||||
form_id: @form_id
|
||||
)
|
||||
|
||||
# set see more
|
||||
@shown = false
|
||||
a = =>
|
||||
@setSeeMore()
|
||||
@delay(a, 50)
|
||||
|
||||
# set highlighter
|
||||
@setHighlighter()
|
||||
|
||||
# set see more options
|
||||
setSeeMore: =>
|
||||
return if @el.is(':hidden')
|
||||
return if @shown
|
||||
@shown = true
|
||||
|
||||
@textBubbleImages.each (i, el) =>
|
||||
if !el.complete
|
||||
$(el).one 'load', @measureSeeMore
|
||||
|
||||
@measureSeeMore()
|
||||
|
||||
measureSeeMore: =>
|
||||
maxHeight = 560
|
||||
minHeight = 90
|
||||
bubbleContent = @textBubbleContent
|
||||
bubbleOverflowContainer = @textBubbleOverflowContainer
|
||||
|
||||
# expand if see more is already clicked
|
||||
if @seeMoreOpen
|
||||
bubbleContent.css('height', 'auto')
|
||||
else
|
||||
# reset bubble height and "see more" opacity
|
||||
bubbleContent.css('height', '')
|
||||
bubbleOverflowContainer.css('opacity', '')
|
||||
|
||||
# remember offset of "see more"
|
||||
signatureMarker = bubbleContent.find('.js-signatureMarker')
|
||||
if !signatureMarker.get(0)
|
||||
signatureMarker = bubbleContent.find('div [data-signature=true]')
|
||||
offsetTop = signatureMarker.position()
|
||||
|
||||
# safari - workaround
|
||||
# in safari sometimes the marker is directly on top via .top and inspector but it isn't
|
||||
# in this case use the next element
|
||||
if offsetTop && offsetTop.top is 0
|
||||
offsetTop = signatureMarker.next('div, p, br').position()
|
||||
|
||||
# remember bubble content height
|
||||
bubbleContentHeight = bubbleContent.height()
|
||||
|
||||
# get marker height
|
||||
if offsetTop
|
||||
markerHeight = offsetTop.top
|
||||
|
||||
# if signature marker exists and height is within maxHeight
|
||||
if markerHeight && markerHeight < maxHeight
|
||||
newHeight = markerHeight + 30
|
||||
if newHeight < minHeight
|
||||
newHeight = minHeight
|
||||
|
||||
bubbleContent.attr('data-height', bubbleContentHeight + 30)
|
||||
bubbleContent.attr('data-height-origin', newHeight)
|
||||
bubbleContent.css('height', "#{newHeight}px")
|
||||
bubbleOverflowContainer.removeClass('hide')
|
||||
|
||||
# if height is higher then maxHeight
|
||||
else if bubbleContentHeight > maxHeight
|
||||
bubbleContent.attr('data-height', bubbleContentHeight + 30)
|
||||
bubbleContent.attr('data-height-origin', maxHeight)
|
||||
newHeight = if @seeMoreOpen then 'auto' else "#{maxHeight}px"
|
||||
bubbleContent.css('height', newHeight)
|
||||
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(@label)
|
||||
bubbleOverflowContainer.removeClass('hide')
|
||||
else
|
||||
bubbleOverflowContainer.addClass('hide')
|
||||
|
||||
retrySecurityProcess: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
article_id = $(e.target).closest('.ticket-article-item').data('id')
|
||||
article = App.TicketArticle.find(article_id)
|
||||
|
||||
@ajax(
|
||||
id: 'retrySecurityProcess'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/ticket_articles/#{article_id}/retry_security_process"
|
||||
processData: true
|
||||
success: (encryption_data, status, xhr) =>
|
||||
for data in encryption_data
|
||||
continue if article.preferences.security.type isnt data.type
|
||||
|
||||
if data.sign.success
|
||||
@notify
|
||||
type: 'success'
|
||||
msg: App.i18n.translateContent('The signature was successfully verified.')
|
||||
else if data.sign.comment
|
||||
comment = App.i18n.translateContent('Signature verification failed!') + ' ' + App.i18n.translateContent(data.sign.comment || '', data.sign.commentPlaceholders)
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: comment
|
||||
timeout: 2000
|
||||
|
||||
if data.encryption.success
|
||||
@notify
|
||||
type: 'success'
|
||||
msg: App.i18n.translateContent('Decryption was successful.')
|
||||
else if data.encryption.comment
|
||||
comment = App.i18n.translateContent('Decryption failed!') + ' ' + App.i18n.translateContent(data.encryption.comment || '', data.encryption.commentPlaceholders)
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: comment
|
||||
timeout: 2000
|
||||
|
||||
error: (xhr) =>
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent('The retried security process failed!')
|
||||
)
|
||||
|
||||
retryWhatsAppAttachmentDownload: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
article_id = $(e.target).closest('.ticket-article-item').data('id')
|
||||
|
||||
@ajax(
|
||||
id: 'retryWhatsAppAttachmentDownload'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/ticket_articles/#{article_id}/retry_whatsapp_attachment_download"
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
@notify
|
||||
type: 'success'
|
||||
msg: App.i18n.translateContent('Downloading attachments…')
|
||||
|
||||
error: (data, status, xhr) =>
|
||||
details = data.responseJSON || {}
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent(details.error)
|
||||
)
|
||||
|
||||
stopPropagation: (e) ->
|
||||
e.stopPropagation()
|
||||
|
||||
toggleMetaWithDelay: (e) =>
|
||||
# allow double click select
|
||||
# by adding a delay to the toggle
|
||||
delay = 300
|
||||
|
||||
article = $(e.target).closest('.ticket-article-item')
|
||||
if @elementContainsSelection(article.get(0))
|
||||
@stopPropagation(e)
|
||||
return false
|
||||
|
||||
if @lastClick and +new Date - @lastClick < delay
|
||||
clearTimeout(@toggleMetaTimeout)
|
||||
else
|
||||
@toggleMetaTimeout = setTimeout(@toggleMeta, delay, e)
|
||||
@lastClick = +new Date
|
||||
|
||||
toggleMeta: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
animSpeed = 300
|
||||
article = $(e.target).closest('.ticket-article-item')
|
||||
metaTopClip = article.find('.article-meta-clip.top')
|
||||
metaBottomClip = article.find('.article-meta-clip.bottom')
|
||||
metaTop = article.find('.article-content-meta.top')
|
||||
metaBottom = article.find('.article-content-meta.bottom')
|
||||
|
||||
if @elementContainsSelection(article.get(0))
|
||||
@stopPropagation(e)
|
||||
return false
|
||||
|
||||
if !metaTop.hasClass('hide')
|
||||
article.removeClass('state--folde-out')
|
||||
|
||||
# scroll back up
|
||||
article.velocity 'scroll',
|
||||
container: article.scrollParent()
|
||||
offset: -article.offset().top - metaTop.outerHeight()
|
||||
duration: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
|
||||
metaTop.velocity
|
||||
properties:
|
||||
translateY: 0
|
||||
opacity: [ 0, 1 ]
|
||||
options:
|
||||
speed: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
complete: -> metaTop.addClass('hide')
|
||||
|
||||
metaBottom.velocity
|
||||
properties:
|
||||
translateY: [ -metaBottom.outerHeight(), 0 ]
|
||||
opacity: [ 0, 1 ]
|
||||
options:
|
||||
speed: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
complete: -> metaBottom.addClass('hide')
|
||||
|
||||
metaTopClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
|
||||
metaBottomClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
|
||||
else
|
||||
article.addClass('state--folde-out')
|
||||
metaBottom.removeClass('hide')
|
||||
metaTop.removeClass('hide')
|
||||
|
||||
# balance out the top meta height by scrolling down
|
||||
article.velocity('scroll',
|
||||
container: article.scrollParent()
|
||||
offset: -article.offset().top + metaTop.outerHeight()
|
||||
duration: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
)
|
||||
|
||||
metaTop.velocity
|
||||
properties:
|
||||
translateY: [ 0, metaTop.outerHeight() ]
|
||||
opacity: [ 1, 0 ]
|
||||
options:
|
||||
speed: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
|
||||
metaBottom.velocity
|
||||
properties:
|
||||
translateY: [ 0, -metaBottom.outerHeight() ]
|
||||
opacity: [ 1, 0 ]
|
||||
options:
|
||||
speed: animSpeed
|
||||
easing: 'easeOutQuad'
|
||||
|
||||
metaTopClip.velocity({ height: metaTop.outerHeight() }, animSpeed, 'easeOutQuad')
|
||||
metaBottomClip.velocity({ height: metaBottom.outerHeight() }, animSpeed, 'easeOutQuad')
|
||||
|
||||
toggleFold: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
bubbleContent = @textBubbleContent
|
||||
bubbleOverflowContainer = @textBubbleOverflowContainer
|
||||
|
||||
if @seeMoreOpen
|
||||
@label = App.i18n.translateContent('See more')
|
||||
height = bubbleContent.attr('data-height-origin')
|
||||
@seeMoreOpen = false
|
||||
else
|
||||
@label = App.i18n.translateContent('See less')
|
||||
height = bubbleContent.attr('data-height')
|
||||
@seeMoreOpen = true
|
||||
|
||||
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(@label)
|
||||
|
||||
bubbleContent.velocity
|
||||
properties:
|
||||
height: height
|
||||
options:
|
||||
duration: 300
|
||||
|
||||
isOrContains: (node, container) ->
|
||||
while node
|
||||
if node is container
|
||||
return true
|
||||
node = node.parentNode
|
||||
false
|
||||
|
||||
elementContainsSelection: (el) ->
|
||||
sel = window.getSelection()
|
||||
if sel.rangeCount > 0 && sel.toString()
|
||||
for i in [0..sel.rangeCount-1]
|
||||
if !@isOrContains(sel.getRangeAt(i).commonAncestorContainer, el)
|
||||
return false
|
||||
return true
|
||||
false
|
||||
|
||||
remove: =>
|
||||
@el.remove()
|
||||
|
||||
imageView: (e) ->
|
||||
# take care of images surrounded by a link
|
||||
if e.target && e.target.parentNode && e.target.parentNode.nodeName.toLowerCase() == 'a'
|
||||
return false
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML, parentElement: $(e.currentTarget))
|
||||
|
||||
calendarView: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
parentElement = $(e.target).closest('.attachment.file-calendar')
|
||||
new App.TicketZoomArticleCalendarView(calendar: parentElement.get(0).outerHTML)
|
||||
|
||||
updateFormId: (newFormId) ->
|
||||
@articleActions?.form_id = newFormId
|
||||
|
||||
@@ -27,6 +27,7 @@ class App.TicketZoomChannel
|
||||
# Otherwise, return a warning alert with a "humanized" end time of the window.
|
||||
return {
|
||||
text: __('You have a 24 hour window to send WhatsApp messages in this conversation. The customer service window closes %s.')
|
||||
textPlaceholder: App.PrettyDate.humanTime(timeWindowEnd)
|
||||
textPlaceholder: App.ViewHelpers.humanTime(timeWindowEnd)
|
||||
noQuote: true
|
||||
type: 'warning'
|
||||
}
|
||||
|
||||
@@ -329,18 +329,14 @@ class TranslationModal extends App.ControllerModal
|
||||
url: "#{@apiPath}/translations/upsert",
|
||||
data: JSON.stringify(params),
|
||||
processData: true,
|
||||
success: =>
|
||||
success: (data) =>
|
||||
@close()
|
||||
@successCallback()
|
||||
|
||||
# Show toast if neeeded (means that in string inside the current user language was changed).
|
||||
# Later we should add real subscription handling for this situation in the new tech stack.
|
||||
currentLocale = App.i18n.get()
|
||||
if params.locale == currentLocale
|
||||
@notify(
|
||||
type: 'success'
|
||||
msg: App.i18n.translateContent('To see the updated translation, please reload your browser.')
|
||||
)
|
||||
App.i18n.setMap(data.source, data.target)
|
||||
|
||||
})
|
||||
|
||||
|
||||
@@ -48,32 +48,41 @@ class Index extends App.ControllerGenericIndex
|
||||
newControllerClass: -> New
|
||||
editControllerClass: -> Edit
|
||||
|
||||
ActivatorMixin =
|
||||
ModalContentFormModelMixin =
|
||||
events:
|
||||
'change select[name="activator"]': 'activatorChanged'
|
||||
'change input[name="execution_localization"]': 'executionLocalizationChanged'
|
||||
|
||||
contentFormModel: ->
|
||||
params = @contentFormParams()
|
||||
attrs = App[ @genericObject ].configure_attributes
|
||||
attrs = _.clone(App[ @genericObject ].configure_attributes)
|
||||
|
||||
attrs = @prepareActivatorAttributes(params, attrs)
|
||||
attrs = @prepareExecutionLocalizationAttributes(params, attrs)
|
||||
|
||||
{ configure_attributes: attrs }
|
||||
|
||||
prepareActivatorAttributes: (params, attrs) ->
|
||||
_.findWhere(attrs, { name: 'execution_condition_mode'}).hide = params.activator isnt 'action'
|
||||
_.findWhere(attrs, { name: 'condition'}).hasReached = params.activator is 'time'
|
||||
_.findWhere(attrs, { name: 'condition'}).action = params.activator is 'action'
|
||||
|
||||
{ configure_attributes: attrs }
|
||||
|
||||
contentFormParams: ->
|
||||
@intermediaryParams || @item || { activator: 'action', execution_condition_mode: 'selective' }
|
||||
attrs
|
||||
|
||||
activatorChanged: (e) ->
|
||||
e.preventDefault()
|
||||
@intermediaryParams = App.ControllerForm.params(@el)
|
||||
@update()
|
||||
|
||||
contentFormParams: ->
|
||||
@intermediaryParams || @item || { activator: 'action', execution_condition_mode: 'selective' }
|
||||
|
||||
class Edit extends App.ControllerGenericEdit
|
||||
@include ActivatorMixin
|
||||
@include App.ExecutionLocalizationMixin
|
||||
@include ModalContentFormModelMixin
|
||||
|
||||
class New extends App.ControllerGenericNew
|
||||
@include ActivatorMixin
|
||||
@include App.ExecutionLocalizationMixin
|
||||
@include ModalContentFormModelMixin
|
||||
|
||||
App.Config.set('Trigger', { prio: 3300, name: __('Trigger'), parent: '#manage', target: '#manage/trigger', controller: Trigger, permission: ['admin.trigger'] }, 'NavBarAdmin')
|
||||
|
||||
@@ -880,10 +880,10 @@ class App.Utils
|
||||
dataRef = objects
|
||||
dataRefLast = undefined
|
||||
for level in levels
|
||||
if typeof dataRef is 'object' && level of dataRef
|
||||
if _.isObject(dataRef) && dataRef && level of dataRef
|
||||
dataRefLast = dataRef
|
||||
dataRef = dataRef[level]
|
||||
else if typeof dataRef is 'object' && typeof level is 'string' && matches = level.match(/(?<functionName>\w+)\((?<params>.*?)\)/)
|
||||
else if _.isObject(dataRef) && typeof level is 'string' && matches = level.match(/(?<functionName>\w+)\((?<params>.*?)\)/)
|
||||
dataRefLast = dataRef
|
||||
{ functionName, params } = matches.groups
|
||||
parameters = params.split(',').map((param) -> param.trim())
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Handler for localization of execution changes in scheduler and trigger modals.
|
||||
# Collapses/expands localization and timezone fields depending on the state of the switch.
|
||||
#
|
||||
# Additional code is needed to call the function to manipulate attribute array:
|
||||
#
|
||||
# contentFormModel: =>
|
||||
# params = @contentFormParams() or {}
|
||||
# attrs = _.clone(App[ @genericObject ].configure_attributes)
|
||||
#
|
||||
# attrs = @prepareExecutionLocalizationAttributes(params, attrs)
|
||||
#
|
||||
# { configure_attributes: attrs }
|
||||
#
|
||||
# Make sure to also register the change event handler in your target class, like so:
|
||||
#
|
||||
# events:
|
||||
# 'change input[name="execution_localization"]': 'executionLocalizationChanged'
|
||||
|
||||
App.ExecutionLocalizationMixin =
|
||||
prepareExecutionLocalizationAttributes: (params, attrs) ->
|
||||
# Expand locale and timezone fields in case they are not set to system defaults.
|
||||
if (params.localization and params.localization isnt 'system') or (params.timezone and params.timezone isnt 'system')
|
||||
_.findWhere(attrs, { name: 'execution_localization'}).value = true
|
||||
_.findWhere(attrs, { name: 'localization'}).item_class += ' in'
|
||||
_.findWhere(attrs, { name: 'timezone'}).item_class += ' in'
|
||||
else
|
||||
_.findWhere(attrs, { name: 'execution_localization'}).value = false
|
||||
localization = _.findWhere(attrs, { name: 'localization'})
|
||||
localization.item_class = localization.item_class.replace /\sin$/, ''
|
||||
timezone = _.findWhere(attrs, { name: 'timezone'})
|
||||
timezone.item_class = timezone.item_class.replace /\sin$/, ''
|
||||
|
||||
attrs
|
||||
|
||||
executionLocalizationChanged: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
localizationCollapseWidget = @el.find('[data-attribute-name="localization"]')
|
||||
timezoneCollapseWidget = @el.find('[data-attribute-name="timezone"]')
|
||||
|
||||
# Show or hide the locale and timezone fields depending on the switch value.
|
||||
if $(e.target).is(':checked')
|
||||
localizationCollapseWidget.collapse('show')
|
||||
timezoneCollapseWidget.collapse('show')
|
||||
@el.find('[data-attribute-name="localization"]').css('margin-bottom', '')
|
||||
@el.find('[data-attribute-name="timezone"]').css('margin-bottom', '')
|
||||
else
|
||||
localizationCollapseWidget.collapse('hide')
|
||||
timezoneCollapseWidget.collapse('hide')
|
||||
@el.find('[data-attribute-name="localization"]').css('margin-bottom', '0')
|
||||
@el.find('[data-attribute-name="timezone"]').css('margin-bottom', '0')
|
||||
@@ -9,8 +9,9 @@ class App.Job extends App.Model
|
||||
{ name: 'condition', display: __('Conditions for affected objects'), tag: 'object_selector', null: true, executionTime: true, noCurrentUser: true },
|
||||
{ name: 'perform', display: __('Execute changes on objects'), tag: 'object_perform_action', null: true, notification: true, ticket_delete: true, data_privacy_deletion_task: true },
|
||||
{ name: 'disable_notification', display: __('Disable Notifications'), tag: 'boolean', default: true },
|
||||
{ name: 'localization', display: __('Locale'), tag: 'language', null: false, class: 'input', show_system_default_option: true },
|
||||
{ name: 'timezone', display: __('Timezone'), tag: 'timezone', null: false, class: 'input', show_system_default_option: true },
|
||||
{ name: 'execution_localization', display: __('Localization of execution changes'), tag: 'switch', null: true, label_class: 'hidden', help: __('Customize the default locale and timezone during replacement of template variables.') },
|
||||
{ name: 'localization', display: __('Locale'), tag: 'language', null: true, class: 'input', show_system_default_option: true, item_class: 'collapse formGroup--halfSize' },
|
||||
{ name: 'timezone', display: __('Timezone'), tag: 'timezone', null: true, class: 'input', show_system_default_option: true, item_class: 'collapse formGroup--halfSize' },
|
||||
{ name: 'note', display: __('Note'), tag: 'textarea', note: __('Notes are visible to agents only, never to customers.'), limit: 250, null: true },
|
||||
{ name: 'active', display: __('Active'), tag: 'active', default: true },
|
||||
{ name: 'matching', display: __('Will process'), readonly: 1 },
|
||||
|
||||
@@ -8,8 +8,9 @@ class App.Trigger extends App.Model
|
||||
{ name: 'execution_condition_mode', display: __('Action execution'), tag: 'radio', type: 'text', limit: 50, null: true, options: [ { value: 'selective', name: __('Selective (default)'), note: __('When at least one field from conditions was updated or article was added and conditions match') }, { value: 'always', name: __('Always'), note: __('When conditions match') } ] },
|
||||
{ name: 'condition', display: __('Conditions for affected objects'), tag: 'ticket_selector', null: false, preview: false, action: true, hasChanged: true, executionTime: true, hasReached: true, hasRegexOperators: true },
|
||||
{ name: 'perform', display: __('Execute changes on objects'), tag: 'ticket_perform_action', null: true, notification: true, trigger: true },
|
||||
{ name: 'localization', display: __('Locale'), tag: 'language', null: false, class: 'input', show_system_default_option: true },
|
||||
{ name: 'timezone', display: __('Timezone'), tag: 'timezone', null: false, class: 'input', show_system_default_option: true },
|
||||
{ name: 'execution_localization', display: __('Localization of execution changes'), tag: 'switch', null: true, label_class: 'hidden', help: __('Customize the default locale and timezone during replacement of template variables.') },
|
||||
{ name: 'localization', display: __('Locale'), tag: 'language', null: true, class: 'input', show_system_default_option: true, item_class: 'collapse formGroup--halfSize' },
|
||||
{ name: 'timezone', display: __('Timezone'), tag: 'timezone', null: true, class: 'input', show_system_default_option: true, item_class: 'collapse formGroup--halfSize' },
|
||||
{ name: 'note', display: __('Note'), tag: 'textarea', limit: 250, null: true },
|
||||
{ name: 'active', display: __('Active'), tag: 'active', default: true },
|
||||
{ name: 'updated_at', display: __('Updated'), tag: 'datetime', readonly: 1 },
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
<label for=""><%- @T('Security') %></label>
|
||||
</div>
|
||||
<div class="horizontal">
|
||||
<div class="btn btn--action btn--split--first js-active-toggle-type js-securityType" data-type="PGP"><%= @T('PGP') %></div>
|
||||
<div class="btn btn--action btn--split--last js-active-toggle-type js-securityType" data-type="S/MIME"><%= @T('S/MIME') %></div>
|
||||
<div class="btn btn--action btn--split--first js-active-toggle-type js-securityType" data-type="PGP"><%- @T('PGP') %></div>
|
||||
<div class="btn btn--action btn--split--last js-active-toggle-type js-securityType" data-type="S/MIME"><%- @T('S/MIME') %></div>
|
||||
<div class="spacer js-securityType"></div>
|
||||
<span class="js-securityEncryptComment">
|
||||
<div class="btn btn--action btn--split--first js-securityEncrypt js-active-toggle" disabled><%- @Icon('lock-open', 'btn-inactive-icon') %><%- @Icon('lock', 'btn-active-icon') %><%- @T('Encrypt') %></div>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<div class="base-inbound-settings"></div>
|
||||
</div>
|
||||
@@ -151,7 +151,7 @@
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<div class="base-outbound-type"></div>
|
||||
<div class="base-outbound-settings"></div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<div class="base-outbound-type"></div>
|
||||
<div class="base-outbound-settings"></div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<%- @humanTime(task.created_at) %>
|
||||
<br><br>
|
||||
<div class="label"><%- @T('State') %></div>
|
||||
<% if task.state: %><%= @T(task.state) %><% else: %><%- @T('in process') %><% end %>
|
||||
<% if task.state: %><%- @T(task.state) %><% else: %><%- @T('in process') %><% end %>
|
||||
<% if task.preferences.error: %> (<%= task.preferences.error %>)<% end %>
|
||||
</div>
|
||||
<% if task.preferences.user or task.preferences.ticket: %>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="draggable"><%- @Icon('draggable') %></div>
|
||||
<% end %>
|
||||
<div class="horizontal-filter-body">
|
||||
<div class="controls">
|
||||
<div class="controls horizontal-filter-attribute">
|
||||
<div class="u-positionOrigin js-attributeSelector">
|
||||
<%- @Icon('arrow-down', 'dropdown-arrow') %>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="columnSelect-pool js-pool">
|
||||
<% for option in @attribute.options: %>
|
||||
<% if option.group != undefined: %>
|
||||
<div class="columnSelect-group-label" title="<%= @T(option.label) %>"><%= @T(option.label) %></div>
|
||||
<div class="columnSelect-group-label" title="<%- @T(option.label) %>"><%- @T(option.label) %></div>
|
||||
<% for o in option.group: %>
|
||||
<div class="columnSelect-option js-select js-option<%= ' is-hidden' if o.selected %>" data-value="<%= o.value %>" title="<%= o.title || o.name %>"><%= o.name %></div>
|
||||
<% end %>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<% for source_user in source.users: %>
|
||||
<span<% if !source.sourceable_name && source_user.id != 1: %> class="user-popover" data-id="<%= source_user.id %>"<% end %>>
|
||||
<% if source.sourceable_name: %>
|
||||
<%= @T(source.sourceable_type) %>: <%= @T(source.sourceable_name) %>
|
||||
<%- @T(source.sourceable_type) %>: <%- @T(source.sourceable_name) %>
|
||||
<% else if source_user.id == 1: %>
|
||||
<%- @T('System') %>
|
||||
<% else: %>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<span class="searchableSelect-autocomplete-visible js-autocomplete-visible"></span>
|
||||
</div>
|
||||
<% if @attribute.ajax and not @attribute.multiple and not @attribute.disabled: %>
|
||||
<div role="button" title="<%= @T('Clear Selection') %>" tabindex="0" class="dropdown-clear <% if @attribute.showArrowIcon: %>dropdown-clear--with-arrow-icon<% end %> js-clear <% if not @attribute.value or _.isEmpty(@attribute.value): %>hide<% end %>">
|
||||
<div role="button" title="<%- @T('Clear Selection') %>" tabindex="0" class="dropdown-clear <% if @attribute.showArrowIcon: %>dropdown-clear--with-arrow-icon<% end %> js-clear <% if not @attribute.value or _.isEmpty(@attribute.value): %>hide<% end %>">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<%= @option.name %><% if @detail: %><span class="dropdown-detail"><%= @detail %></span><% end %>
|
||||
</span>
|
||||
<% if @option.children: %>
|
||||
<span role="button" class="searchableSelect-option-arrow" title="<%= @T('Navigate to %s', @option.name) %>">
|
||||
<span role="button" class="searchableSelect-option-arrow" title="<%- @T('Navigate to %s', @option.name) %>">
|
||||
<%- @Icon('arrow-right', 'recipientList-arrow') %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ul class="dropdown-submenu js-optionsSubmenu" role="menu" data-parent-value="<%= @parentValue %>" hidden>
|
||||
<% if @title: %>
|
||||
<li role="button" class="dropdown-controls js-back" title="<%= @T('Go Back') %>">
|
||||
<li role="button" class="dropdown-controls js-back" title="<%- @T('Go Back') %>">
|
||||
<%- @Icon('arrow-left') %>
|
||||
<div class="dropdown-title">
|
||||
<%= @title %>
|
||||
|
||||
@@ -75,13 +75,13 @@
|
||||
<%- value %>
|
||||
<% if @object.constructor.configure_set_as_default && header.name == @object.constructor.configure_set_as_default_marker_attribute: %>
|
||||
<% if @object.constructor.is_default(@object): %>
|
||||
<span class="badge badge--primary"><%= @T('Default') %></span>
|
||||
<span class="badge badge--primary"><%- @T('Default') %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @object.constructor.badges && @object.constructor.badges.length > 0: %>
|
||||
<% for badge in @object.constructor.badges: %>
|
||||
<% if badge.attribute == header.name && badge.active(@object): %>
|
||||
<span class="badge badge--<%= badge.class %>"><%= @T(badge.display) %></span>
|
||||
<span class="badge badge--<%= badge.class %>"><%- @T(badge.display) %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<button type="button" class="close js-close" aria-label="<%= @T('Close') %>">
|
||||
<button type="button" class="close js-close" aria-label="<%- @T('Close') %>">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</button>
|
||||
<h4><%= @T(@C('ticket_duplicate_detection_title')) %></h4>
|
||||
<p><%= @T(@C('ticket_duplicate_detection_body')) %></p>
|
||||
<h4><%- @T(@C('ticket_duplicate_detection_title')) %></h4>
|
||||
<p><%- @T(@C('ticket_duplicate_detection_body')) %></p>
|
||||
<% if @items.length: %>
|
||||
<ul>
|
||||
<% for [id, number, title] in @items.slice(0, 2): %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="horizontal-filter js-filterElement">
|
||||
<div class="horizontal-filter-body">
|
||||
<div class="controls flex-no-shrink">
|
||||
<div class="controls flex-no-shrink horizontal-filter-attribute">
|
||||
<div class="u-positionOrigin js-attributeSelector">
|
||||
<%- @Icon('arrow-down', 'dropdown-arrow') %>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<td class="js-groupListItemAction settings-list-middle">
|
||||
<a class="btn btn--text btn--primary js-remove">
|
||||
<%- @Icon('trash') %>
|
||||
<%= @T('Remove') %>
|
||||
<%- @T('Remove') %>
|
||||
</a>
|
||||
</tr>
|
||||
<% end %>
|
||||
@@ -39,7 +39,7 @@
|
||||
<td class="js-groupListItemAction settings-list-middle">
|
||||
<a class="btn btn--text btn--primary js-remove">
|
||||
<%- @Icon('trash') %>
|
||||
<%= @T('Remove') %>
|
||||
<%- @T('Remove') %>
|
||||
</a>
|
||||
</tr>
|
||||
<tr class="js-groupListNewItemRow">
|
||||
@@ -55,7 +55,7 @@
|
||||
<td class="js-groupListItemAction settings-list-middle">
|
||||
<a class="btn btn--text btn--create js-add">
|
||||
<%- @Icon('plus-small') %>
|
||||
<%= @T('Add') %>
|
||||
<%- @T('Add') %>
|
||||
</a>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<div class="base-inbound-settings"></div>
|
||||
</div>
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<p class="js-messageFound"><%- @T('%s email(s) were found in your mailbox. They will all be moved from your mailbox into Zammad.', 'x') %></p>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<div class="base-outbound-type"></div>
|
||||
<div class="base-outbound-settings"></div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<div class="base-outbound-type"></div>
|
||||
<div class="base-outbound-settings"></div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<h2><%- @T('Settings') %></h2>
|
||||
<% if @config.disable_ssl_verify: %>
|
||||
<div class="alert alert--warning" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<% end %>
|
||||
<table class="settings-list" style="width: 100%;">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2><%- @T('Settings') %></h2>
|
||||
<div class="settings-entry">
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<table class="settings-list" style="width: 100%;">
|
||||
<thead>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2><%- @T('Settings') %></h2>
|
||||
<div class="settings-entry">
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<table class="settings-list" style="width: 100%;">
|
||||
<thead>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h2><%- @T('Settings %s', @item.name) %></h2>
|
||||
<% if (@config.ssl is 'ssl' or @config.ssl is 'starttls') and not @config.ssl_verify: %>
|
||||
<div class="alert alert--warning" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<% end %>
|
||||
<table class="settings-list" style="width: 100%;">
|
||||
@@ -25,11 +25,11 @@
|
||||
<td class="settings-list-row-control"><%= @config.host %>
|
||||
<tr>
|
||||
<td class="settings-list-row-control"><%- @T('SSL/STARTTLS') %>
|
||||
<td class="settings-list-row-control"><%= @T(@config.ssl_humanized) %>
|
||||
<td class="settings-list-row-control"><%- @T(@config.ssl_humanized) %>
|
||||
<% if @config.ssl is 'ssl' or @config.ssl is 'starttls': %>
|
||||
<tr>
|
||||
<td class="settings-list-row-control"><%- @T('SSL verification') %>
|
||||
<td class="settings-list-row-control"><% if @config.ssl_verify: %><%= @T('yes') %><% else: %><%= @T('no') %><% end %>
|
||||
<td class="settings-list-row-control"><% if @config.ssl_verify: %><%- @T('yes') %><% else: %><%- @T('no') %><% end %>
|
||||
<% end %>
|
||||
<tr>
|
||||
<td class="settings-list-row-control"><%- @T('Base DN') %>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<table class="settings-list" style="width: 100%;">
|
||||
<thead>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<%- @iconWithModifier(@item, className: 'section-inner-icon-holder', iconset: @iconset, addStateClass: true) %>
|
||||
<span class="title"><%= @item.title %></span>
|
||||
<% if @item.type is 'category': %>
|
||||
<span class="section-detail"><%= @T('Answers: %s', @item.count) %></span>
|
||||
<span class="section-detail"><%- @T('Answers: %s', @item.count) %></span>
|
||||
<% end %>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<% for location in @locations: %>
|
||||
<div class="settings-entry kb-menu-settings-entry">
|
||||
<h3><%= @T(location.headline) %></h3>
|
||||
<h3><%- @T(location.headline) %></h3>
|
||||
|
||||
<% for kb_locale in @locales: %>
|
||||
<div class="kb-menu-preview">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="js-dataOption">
|
||||
<div class="alert alert--warning js-sslVerifyAlert hide" role="alert">
|
||||
<%= @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
<%- @T('Turning off SSL verification is a security risk and should be used only temporary. Use this option at your own risk!') %>
|
||||
</div>
|
||||
<div class="js-inputSearchURL"></div>
|
||||
<div class="js-inputSSLVerify"></div>
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
<td>
|
||||
<div class="settings-list-wrapping-cell">
|
||||
<%- @Icon(elem.details.icon, 'half-space-right') %>
|
||||
<span class="half-space-right"><%= @T(elem.details.label) %></span>
|
||||
<span class="half-space-right"><%- @T(elem.details.label) %></span>
|
||||
<% if elem.configured and elem.default: %>
|
||||
<span class="badge badge--primary"><%= @T('Default') %></span>
|
||||
<span class="badge badge--primary"><%- @T('Default') %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
@@ -59,7 +59,7 @@
|
||||
<span class="dropdown-iconSpacer">
|
||||
<%- @Icon('reload') %>
|
||||
</span>
|
||||
<%= @T('Set as default') %>
|
||||
<%- @T('Set as default') %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
@@ -69,7 +69,7 @@
|
||||
<span class="dropdown-iconSpacer">
|
||||
<%- @Icon('pen') %>
|
||||
</span>
|
||||
<%= @T('Edit') %>
|
||||
<%- @T('Edit') %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
@@ -78,7 +78,7 @@
|
||||
<span class="dropdown-iconSpacer">
|
||||
<%- @Icon('trash') %>
|
||||
</span>
|
||||
<%= @T('Remove') %>
|
||||
<%- @T('Remove') %>
|
||||
</a>
|
||||
</li>
|
||||
<% else: %>
|
||||
@@ -87,7 +87,7 @@
|
||||
<span class="dropdown-iconSpacer">
|
||||
<%- @Icon('checkmark') %>
|
||||
</span>
|
||||
<%= @T('Set Up') %>
|
||||
<%- @T('Set Up') %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<% if @params.timeRange is 'day' || @params.timeRange is 'month': %>
|
||||
<div class="btn-group btn-group--full" role="group" aria-label="">
|
||||
<% for item in @timeRangeMonth: %>
|
||||
<div class="btn btn--text btn--large js-timePickerMonth<%- ' is-selected' if @params.month is item.value %>" data-id="<%= @params.timeRange %>" data-type="<%= item.value %>"><%= @T(item.display) %></div>
|
||||
<div class="btn btn--text btn--large js-timePickerMonth<%- ' is-selected' if @params.month is item.value %>" data-id="<%= @params.timeRange %>" data-type="<%= item.value %>"><%- @T(item.display) %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="batch-overlay-box-inner">
|
||||
<% if _.isEmpty(@macros): %>
|
||||
<div class="batch-overlay-macro-error-message">
|
||||
<%= @T('No macros available.') %>
|
||||
<%- @T('No macros available.') %>
|
||||
<% if @errorMessage: %>
|
||||
<br>
|
||||
<%= @T(@errorMessage) %>
|
||||
<%- @T(@errorMessage) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else: %>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<div class="alert alert--<%= @alert.type %>" role="alert">
|
||||
<%- @T(@alert.text, @alert.textPlaceholder) %>
|
||||
<% if @alert.noQuote: %>
|
||||
<%- @Tp(@alert.text, @alert.textPlaceholder) %>
|
||||
<% else: %>
|
||||
<%- @T(@alert.text, @alert.textPlaceholder) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -70,8 +70,8 @@
|
||||
<label for=""><%- @T('Security') %></label>
|
||||
</div>
|
||||
<div class="horizontal">
|
||||
<div class="btn btn--action btn--split--first js-active-toggle-type js-securityType" data-type="PGP"><%= @T('PGP') %></div>
|
||||
<div class="btn btn--action btn--split--last js-active-toggle-type js-securityType" data-type="S/MIME"><%= @T('S/MIME') %></div>
|
||||
<div class="btn btn--action btn--split--first js-active-toggle-type js-securityType" data-type="PGP"><%- @T('PGP') %></div>
|
||||
<div class="btn btn--action btn--split--last js-active-toggle-type js-securityType" data-type="S/MIME"><%- @T('S/MIME') %></div>
|
||||
<div class="spacer js-securityType"></div>
|
||||
<span class="js-securityEncryptComment">
|
||||
<div class="btn btn--action btn--split--first js-securityEncrypt js-active-toggle" disabled><%- @Icon('lock-open', 'btn-inactive-icon') %><%- @Icon('lock', 'btn-active-icon') %><%- @T('Encrypt') %></div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="sidebar-git-issue-content">
|
||||
<div class="sidebar-block">
|
||||
<a href="<%- issue.url %>" target="_blank">
|
||||
<span class="icon-holder" title="<%= @T(issue.icon_state) %>">
|
||||
<span class="icon-holder" title="<%- @T(issue.icon_state) %>">
|
||||
<%- @Icon('task-state', issue.icon_state) %>
|
||||
</span>
|
||||
#<%= issue.id %> <%= issue.title %>
|
||||
@@ -16,7 +16,7 @@
|
||||
<% if issue.milestone: %>
|
||||
<div class="sidebar-block">
|
||||
<label><%- @T('Milestone') %></label>
|
||||
<%= @T(issue.milestone) %>
|
||||
<%- @T(issue.milestone) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if issue.assignees.length > 0: %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<% for entry in @list: %>
|
||||
<div class="accounted-time-value-row">
|
||||
<div class="accounted-time-value-item" title="<%= @T(entry[0]) %>"><%= @T(entry[0]) %></div>
|
||||
<div class="accounted-time-value-item" title="<%- @T(entry[0]) %>"><%- @T(entry[0]) %></div>
|
||||
<div class="accounted-time-value-item"><%= parseFloat(entry[1]).toFixed(1) %></div>
|
||||
<% if @displayUnit: %>
|
||||
<div class="accounted-time-value-item">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="btn-group btn-group--full" role="group" aria-label="">
|
||||
<% for month, i in @timeRangeMonth: %>
|
||||
<div class="btn btn--text btn--large js-timePickerMonth<%- ' is-selected' if @month is i+1 %>" data-type="<%= i+1 %>"><%= @T(month) %></div>
|
||||
<div class="btn btn--text btn--large js-timePickerMonth<%- ' is-selected' if @month is i+1 %>" data-type="<%= i+1 %>"><%- @T(month) %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<select id="timeAccountingUnit" class="form-control">
|
||||
<% if @timeUnits: %>
|
||||
<% for row in @timeUnits: %>
|
||||
<option value="<%= row.value %>" <% if row.value is @timeAccountingUnit: %>selected<%end %>><%= @T(row.display) %></option>
|
||||
<option value="<%= row.value %>" <% if row.value is @timeAccountingUnit: %>selected<%end %>><%- @T(row.display) %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</select>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="js-suggestions">
|
||||
<h3><%- @T('Find translation suggestion') %><span class="subtitle js-suggestionsCounterContainer hide"> (<span class="js-suggestionsCounter">?</span> <%- @T('matches found') %>)</span> <span class="tiny loading icon js-suggestionsLoader hide" style="margin-left: 3px;"></span></h3>
|
||||
<div class="form-group">
|
||||
<input class="form-control js-suggestionsSearchInput" type="text" placeholder="<%= @T('Search…') %>">
|
||||
<input class="form-control js-suggestionsSearchInput" type="text" placeholder="<%- @T('Search…') %>">
|
||||
</div>
|
||||
<div class="scroll-table-container js-suggestionsTable"></div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<div class="controls controls--button ignore-readonly">
|
||||
<input readonly id="callback_url" name="callback_url" name="callback_url" type="text" value="<%= @callback_url %>" class="form-control">
|
||||
<div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%= @T('Copy to clipboard') %>">
|
||||
<div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%- @T('Copy to clipboard') %>">
|
||||
<span class="controls-button-inner">
|
||||
<%- @Icon('clipboard') %>
|
||||
</span>
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div class="controls controls--button ignore-readonly">
|
||||
<input readonly id="verify_token" type="text" name="verify_token" value="<%= @channel?.options?.verify_token %>" class="form-control">
|
||||
<div class="controls-button u-clickable js-copy" role="button" data-target-field="verify_token" aria-label="<%= @T('Copy to clipboard') %>">
|
||||
<div class="controls-button u-clickable js-copy" role="button" data-target-field="verify_token" aria-label="<%- @T('Copy to clipboard') %>">
|
||||
<span class="controls-button-inner">
|
||||
<%- @Icon('clipboard') %>
|
||||
</span>
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<div class="two-factor-auth flex">
|
||||
<p><%= @T('To set up Authenticator App for your account, follow the steps below:') %></p>
|
||||
<p><%- @T('To set up Authenticator App for your account, follow the steps below:') %></p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><%= @T('Unless you already have it, install one of the following authenticator apps on your mobile device:') %></p>
|
||||
<p><%- @T('Unless you already have it, install one of the following authenticator apps on your mobile device:') %></p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://support.google.com/accounts/answer/1066447" target="_blank"><%= @T('Google Authenticator') %></a>
|
||||
<a href="https://support.google.com/accounts/answer/1066447" target="_blank"><%- @T('Google Authenticator') %></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://support.authy.com/hc/en-us/articles/115001945848-Installing-Authy-apps/" target="_blank"><%= @T('Authy') %></a>
|
||||
<a href="https://support.authy.com/hc/en-us/articles/115001945848-Installing-Authy-apps/" target="_blank"><%- @T('Authy') %></a>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<a href="https://support.microsoft.com/en-us/account-billing/download-and-install-the-microsoft-authenticator-app-351498fc-850a-45da-b7b6-27e523b8702a" target="_blank"><%= @T('Microsoft Authenticator') %></a>
|
||||
<a href="https://support.microsoft.com/en-us/account-billing/download-and-install-the-microsoft-authenticator-app-351498fc-850a-45da-b7b6-27e523b8702a" target="_blank"><%- @T('Microsoft Authenticator') %></a>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><%= @T('Open your authenticator app and scan the QR code below:') %></p>
|
||||
<p><%- @T('Open your authenticator app and scan the QR code below:') %></p>
|
||||
<div class="authenticator-app">
|
||||
<canvas class="qr-code-canvas js-qr-code-canvas" title="<%= @T('Show authenticator app secret') %>"></canvas>
|
||||
<div class="secret js-secret" title="<%= @T('Hide authenticator app secret') %>" style="display: none;" aria-label="<%= @T('Authenticator app secret') %>"><%= @config.secret %></div>
|
||||
<canvas class="qr-code-canvas js-qr-code-canvas" title="<%- @T('Show authenticator app secret') %>"></canvas>
|
||||
<div class="secret js-secret" title="<%- @T('Hide authenticator app secret') %>" style="display: none;" aria-label="<%- @T('Authenticator app secret') %>"><%= @config.secret %></div>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><%= @T('Enter the security code generated by the authenticator app:') %></p>
|
||||
<p><%- @T('Enter the security code generated by the authenticator app:') %></p>
|
||||
<p>
|
||||
<div class="method-form js-payload-form"></div>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><%= @T('Press the button below to finish the setup.') %></p>
|
||||
<p><%- @T('Press the button below to finish the setup.') %></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="two-factor-auth flex">
|
||||
<p><%= @T('Please save your recovery codes listed below somewhere safe. You can use them to sign in if you lose access to another two-factor method:') %></p>
|
||||
<p><%- @T('Please save your recovery codes listed below somewhere safe. You can use them to sign in if you lose access to another two-factor method:') %></p>
|
||||
|
||||
<pre><code readonly><% for elem, i in @recovery_codes: %><%= elem %><% if i == @recovery_codes.length - 1: %><% else if i % 2: %>
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<p><%= @T('Security keys are hardware or software credentials that can be used as your two-factor authentication method.') %></p>
|
||||
<p><%- @T('Security keys are hardware or software credentials that can be used as your two-factor authentication method.') %></p>
|
||||
<div class="js-table"></div>
|
||||
<p><%= @T('To register a new security key with your account, press the button below.') %></p>
|
||||
<p><%- @T('To register a new security key with your account, press the button below.') %></p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="main two-factor-auth flex">
|
||||
<p class="text-center"><%= @T('Getting key information from the browser…') %></p>
|
||||
<p class="text-center"><%- @T('Getting key information from the browser…') %></p>
|
||||
<div class="loading icon"></div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="form-controls">
|
||||
|
||||
<% if @errorMessage: %>
|
||||
<p class="text-center"><%= @T('Try using your security key again.') %></p>
|
||||
<p class="text-center"><%- @T('Try using your security key again.') %></p>
|
||||
<button class="btn btn--primary js-retry" type="button"><%- @T('Retry') %></button>
|
||||
<% else: %>
|
||||
<input name="username" type="hidden" value="<%= @formPayload.username %>" />
|
||||
@@ -21,7 +21,7 @@
|
||||
<input name="two_factor_method" type="hidden" value="<%= @twoFactorMethodDetails.key %>" />
|
||||
|
||||
<div class="double-spacer"></div>
|
||||
<p class="text-center"><%= @T('Verifying key information…') %></p>
|
||||
<p class="text-center"><%- @T('Verifying key information…') %></p>
|
||||
<div class="double-spacer"></div>
|
||||
<div class="centered">
|
||||
<div class="loading icon"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h3 class="text-center">
|
||||
<%= @T('Try Another Method') %>
|
||||
<%- @T('Try Another Method') %>
|
||||
</h3>
|
||||
<div class="two-factor-auth">
|
||||
<div class="two-factor-auth-method-buttons">
|
||||
@@ -9,7 +9,7 @@
|
||||
<%- @Icon(method.icon) %>
|
||||
<%- @T(method.label) %>
|
||||
</button>
|
||||
<p class="help-block"><%= @T(method.description) %></p>
|
||||
<p class="help-block"><%- @T(method.description) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -9237,6 +9237,10 @@ li.list-item-none {
|
||||
background: var(--background-secondary-hover);
|
||||
}
|
||||
|
||||
.user-menu li.add .dropdown-menu.dropdown-menu--light li > a:hover {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.dropdown-menu.dropdown-menu--light li:hover {
|
||||
background: none;
|
||||
}
|
||||
@@ -10777,6 +10781,14 @@ label + .wizard-buttonList {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&-attribute {
|
||||
max-width: 250px;
|
||||
|
||||
.modal--veryLarge & {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
input {
|
||||
border-color: var(--border-alt);
|
||||
|
||||
@@ -31,6 +31,7 @@ class ReportsController < ApplicationController
|
||||
backend[:condition] = condition
|
||||
end
|
||||
next if !backend[:adapter]
|
||||
next if params['backends'][backend[:name]].blank?
|
||||
|
||||
result[backend[:name]] = backend[:adapter].aggs(
|
||||
range_start: get_params[:start],
|
||||
|
||||
@@ -45,8 +45,6 @@ const props = defineProps<Props>()
|
||||
const reactiveContext = toRef(props, 'context')
|
||||
const { currentValue } = useValue(reactiveContext)
|
||||
|
||||
// TODO: add maybe something to extract the props from the context, instead of using context.XYZ (or props.context.XYZ)
|
||||
|
||||
const disabledPlugins = Object.entries(props.context.meta || {})
|
||||
.filter(([, value]) => value.disabled)
|
||||
.map(([key]) => key as EditorCustomPlugins)
|
||||
|
||||
@@ -10,6 +10,8 @@ import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
|
||||
import testFlags from '#shared/utils/testFlags.ts'
|
||||
|
||||
const props = defineProps(nodeViewProps)
|
||||
const initialHeight = props.node.attrs.height
|
||||
const initialWidth = props.node.attrs.width
|
||||
|
||||
const needBase64Convert = (src: string) => {
|
||||
return !(src.startsWith('data:') || src.startsWith('cid:'))
|
||||
@@ -59,8 +61,9 @@ const onLoadImage = (e: Event) => {
|
||||
|
||||
const img = e.target as HTMLImageElement
|
||||
const { naturalWidth, naturalHeight } = img
|
||||
dimensions.width = naturalWidth
|
||||
dimensions.height = naturalHeight
|
||||
|
||||
dimensions.width = initialWidth !== '100%' ? initialWidth : naturalWidth
|
||||
dimensions.height = initialHeight !== 'auto' ? initialHeight : naturalWidth
|
||||
dimensions.maxHeight = naturalHeight
|
||||
dimensions.maxWidth = naturalWidth
|
||||
imageLoaded.value = true
|
||||
@@ -100,6 +103,8 @@ const wrapperStyle = computed(() => {
|
||||
:style="style"
|
||||
:src="src"
|
||||
:alt="node.attrs.alt"
|
||||
:width="node.attrs.width"
|
||||
:height="node.attrs.height"
|
||||
:title="node.attrs.title"
|
||||
:draggable="isDraggable"
|
||||
@load="onLoadImage"
|
||||
|
||||
@@ -32,7 +32,7 @@ module KnowledgeBaseBreadcrumbHelper
|
||||
def calculate_breadcrumb_to_category(category)
|
||||
return [] if category.blank?
|
||||
|
||||
output = [category]
|
||||
output = [find_category(category)]
|
||||
|
||||
parent = category
|
||||
while (parent = find_category(parent&.parent_id))
|
||||
|
||||
@@ -12,7 +12,7 @@ class Channel < ApplicationModel
|
||||
scope :active, -> { where(active: true) }
|
||||
scope :in_area, ->(area) { where(area: area) }
|
||||
|
||||
validates_with Validations::EmailAccountUniquenessValidator
|
||||
validates_with Validations::ChannelEmailAccountUniquenessValidator
|
||||
|
||||
after_create :email_address_check
|
||||
after_update :email_address_check
|
||||
|
||||
@@ -120,7 +120,7 @@ returns
|
||||
def process(channel, msg, exception = true)
|
||||
process_with_timeout(channel, msg)
|
||||
rescue => e
|
||||
failed_email = ::FailedEmail.create(data: msg, parsing_error: e)
|
||||
failed_email = ::FailedEmail.create!(data: msg, parsing_error: e)
|
||||
|
||||
message = <<~MESSAGE.chomp
|
||||
Can't process email. Run the following command to get the message for issue report at https://github.com/zammad/zammad/issues:
|
||||
|
||||
@@ -283,7 +283,7 @@ returns
|
||||
preferences_maybe = {}
|
||||
preferences_maybe[direction] = []
|
||||
|
||||
lookup(extract_numbers(caller_id)).each do |record|
|
||||
lookup(extract_numbers(caller_id)).each do |record| # rubocop:disable Metrics/BlockLength
|
||||
if record.level == 'known'
|
||||
preferences_known[direction].push record.attributes
|
||||
else
|
||||
@@ -294,6 +294,16 @@ returns
|
||||
user = User.lookup(id: record.user_id)
|
||||
if user
|
||||
comment += user.fullname
|
||||
|
||||
if comment.blank?
|
||||
%w[phone mobile].each do |item| # rubocop:disable Performance/CollectionLiteralInLoop
|
||||
next if user[item].blank? # rubocop:disable Metrics/BlockNesting
|
||||
|
||||
comment += user[item]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
elsif record.comment.present?
|
||||
comment += record.comment
|
||||
|
||||
@@ -10,6 +10,7 @@ class LdapSource < ApplicationModel
|
||||
store :preferences
|
||||
|
||||
def self.by_user(user)
|
||||
return if user.blank? || user.source.blank?
|
||||
return if !%r{^Ldap::(\d+)$}.match?(user.source)
|
||||
|
||||
LdapSource.find(user.source.split('::')[1])
|
||||
|
||||
@@ -261,7 +261,7 @@ returns
|
||||
if !has_editor && !has_reader
|
||||
elem.destroy!
|
||||
elsif !has_editor && has_reader
|
||||
elem.update!(access: 'reader') if permission.access == 'editor'
|
||||
elem.update!(access: 'reader') if elem.access == 'editor'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -74,7 +74,7 @@ translate strings in Ruby context, e. g. for notifications
|
||||
=end
|
||||
|
||||
def self.translate(locale, string, *args)
|
||||
translated = find_source(locale, string)&.target || string
|
||||
translated = find_source(locale, string)&.target.presence || string
|
||||
|
||||
translated %= args if args.any?
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ class Service::Translation::Upsert < Service::Base
|
||||
translation = Translation.find_source(locale, source)
|
||||
|
||||
if translation
|
||||
return translation.update!(target: target)
|
||||
translation.update!(target: target)
|
||||
return translation
|
||||
end
|
||||
|
||||
Translation.create!(locale: locale, source: source, target: target, is_synchronized_from_codebase: false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Inicio de sesión como administrador en #{config.product_name}
|
||||
Inicio de sesión como administrador a #{config.product_name}
|
||||
|
||||
<div>Hola, #{user.firstname}:</div>
|
||||
<br>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#{config.product_name} için yönetim girişi
|
||||
#{config.product_name} için yönetici girişi
|
||||
|
||||
<div>Merhaba #{user.firstname},</div>
|
||||
<br>
|
||||
<div>#{config.product_name} hesabınız için bir yönetim girişi talebi aldık <b>#{user.login}</b>.</div>
|
||||
<div>#{config.product_name} hesabınız için bir yönetici girişi talebi aldık <b>#{user.login}</b>.</div>
|
||||
<br>
|
||||
<div>Devam etmek için lütfen aşağıdaki bağlantıya tıklayın (veya URL'yi kopyalayıp tarayıcınıza yapıştırın):</div>
|
||||
<br>
|
||||
@@ -10,6 +10,6 @@
|
||||
<br>
|
||||
<div>Bu bağlantı sizi giriş yapabileceğiniz bir sayfaya yönlendirir.</div>
|
||||
<br>
|
||||
<div>Giriş yapmak istemiyorsanız, lütfen bu mesajı dikkate almayın.</div>
|
||||
<div>Giriş yapmak istemiyorsanız, lütfen bu mesajı dikkate almayın ve tamamen sildiğinizden emin olun.</div>
|
||||
<br>
|
||||
<div>#{config.product_name} Ekibiniz</div>
|
||||
|
||||
10
app/views/mailer/email_oversized/tr.txt.erb
Normal file
10
app/views/mailer/email_oversized/tr.txt.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
[undeliverable] Mesaj çok büyük
|
||||
Sayın #{mail.from_display_name},
|
||||
|
||||
Maalesef "#{mail.subject}" başlıklı e-postanız bir veya daha fazla alıcıya teslim edilemedi.
|
||||
|
||||
Mesajınızın boyutu #{mail.msg_size} MB, ancak yalnızca #{config.postmaster_max_size} MB'a kadar olan mesajları kabul ediyoruz.
|
||||
|
||||
Lütfen mesaj boyutunu küçültüp tekrar deneyin. Anlayışınız için teşekkür ederiz.
|
||||
|
||||
#{config.fqdn} E-Posta Yöneticisi
|
||||
@@ -12,4 +12,4 @@ Restablecer contraseña en #{config.product_name}
|
||||
<br>
|
||||
<div>Si no desea restablecer su contraseña puede ignorar este mensaje. La contraseña no se cambiará.</div>
|
||||
<br>
|
||||
<div>El equipo de #{config.product_name}</div>
|
||||
<div>Su equipo #{config.product_name}</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ Ponovno postavite svoju #{config.product_name} lozinku
|
||||
<br>
|
||||
<div><a href="#{url}">#{url}</a></div>
|
||||
<br>
|
||||
<div>Ovaj link vas vodi na stranicu gdje možete resetirati Vašu lozinku.</div>
|
||||
<div>Ovaj link Vas vodi na stranicu gdje možete promijeniti Vašu lozinku.</div>
|
||||
<br>
|
||||
<div>Ukoliko to ne želite, molimo Vas da ignorirate ovu poruku. Vaša lozinka neće biti resetirana.</div>
|
||||
<br>
|
||||
|
||||
@@ -10,6 +10,6 @@ Odzyskiwanie hasła #{config.product_name}
|
||||
<br>
|
||||
<div>Link przeniesie Cię na witrynę, na której możliwa jest zmiana hasła.</div>
|
||||
<br>
|
||||
<div>Jeżeli nie chcesz zmieniać swego hasła, zignoruj tego maila. Twoje hasło nie zostanie zmienione.</div>
|
||||
<div>Jeżeli nie chcesz zmieniać swego hasła, zignoruj ten e-mail. Twoje hasło nie zostanie zmienione.</div>
|
||||
<br>
|
||||
<div>Zespół #{config.product_name}</div>
|
||||
|
||||
15
app/views/mailer/password_reset/tr.html.erb
Normal file
15
app/views/mailer/password_reset/tr.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
#{config.product_name} şifreni sıfırla.
|
||||
|
||||
<div>Merhaba #{user.firstname},</div>
|
||||
<br>
|
||||
<div>#{config.product_name} Şifrenizin sıfırlanması için bir talep aldık. Hesap: <b>#{user.login}</b>.</div>
|
||||
<br>
|
||||
<div>Şifrenizi sıfırlamak istiyorsanız aşağıdaki bağlantıya tıklayın (veya URL'yi kopyalayıp tarayıcınıza yapıştırın):</div>
|
||||
<br>
|
||||
<div><a href="#{url}">#{url}</a></div>
|
||||
<br>
|
||||
<div>Bu bağlantı sizi şifrenizi değiştirebileceğiniz bir sayfaya yönlendirir.</div>
|
||||
<br>
|
||||
<div>Şifrenizi sıfırlamak istemiyorsanız lütfen bu mesajı dikkate almayın ve tamamen sildiğinizden emin olun. Şifreniz sıfırlanmayacaktır.</div>
|
||||
<br>
|
||||
<div>#{config.product_name} Ekibi</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
Confirme la cuenta de #{config.product_name}, #{user.firstname} #{user.lastname}
|
||||
Confirma tu cuenta #{config.product_name}, #{user.firstname} #{user.lastname}
|
||||
|
||||
<div>Hola, #{user.firstname}:</div>
|
||||
<br>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#{user.firstname} #{user.lastname}, potvrdite svoj #{config.product_name} račun
|
||||
Potvrdite svoj #{config.product_name} račun, #{user.firstname} #{user.lastname}
|
||||
|
||||
<div>Bok #{user.firstname},</div>
|
||||
<br>
|
||||
<div>potvrdite svoju mail adresu za otvaranje #{config.product_name}računa. Vrlo je jednostavno, samo kliknite na link ispod.</div>
|
||||
<div>potvrdite svoju mail adresu za dovršavanje #{config.product_name} računa. Vrlo je jednostavno, samo kliknite na link ispod.</div>
|
||||
<br>
|
||||
<div><a href="#{url}">#{url}</a></div>
|
||||
<br>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
Potwierdź swoje konto na #{config.product_name}, #{user.firstname} #{user.lastname}
|
||||
|
||||
<div>Witaj #{user.firstname},</div>
|
||||
<div>Cześć #{user.firstname},</div>
|
||||
<br>
|
||||
<div>Potwierdź swój adres e-mail, aby aktywować swoje konto #{config.product_name}. To proste, użyj linku poniżej.</div>
|
||||
<div>Potwierdź swój adres e-mail, aby ukończyć tworzenie konta #{config.product_name}. To jest łatwe, po prostu kliknij w link poniżej.</div>
|
||||
<br>
|
||||
<div><a href="#{url}">#{url}</a></div>
|
||||
<br>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
<div>Merhaba #{user.firstname},</div>
|
||||
<br>
|
||||
<div>#{config.product_name} hesabınızı tamamlamak için e-posta adresinizi onaylayın. Çok kolay, aşağıdaki bağlantıya tıklamanız yeterli.</div>
|
||||
<div>#{config.product_name} kaydınızı tamamlamak için e-posta adresinizi onaylayın. Aşağıdaki bağlantıya tıklamanız yeterli.</div>
|
||||
<br>
|
||||
<div><a href="#{url}">#{url}</a></div>
|
||||
<div><a href="#{config.http_type}://#{config.fqdn}/#email_verify/#{token.token}">#{config.http_type}://#{config.fqdn}/#email_verify/#{token.token}</a></div>
|
||||
<br>
|
||||
<div>#{config.product_name} Ekibiniz</div>
|
||||
|
||||
@@ -12,4 +12,4 @@ Restablecer su contraseña de #{config.product_name}
|
||||
<br>
|
||||
<div>Si no desea cambiar su contraseña, ignore este mensaje. Su contraseña no será restablecida.</div>
|
||||
<br>
|
||||
<div>El equipo de #{config.product_name}</div>
|
||||
<div>Su equipo #{config.product_name}</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ Ponovno postavite svoju #{config.product_name} lozinku
|
||||
<br>
|
||||
<div><a href="#{url}">#{url}</a></div>
|
||||
<br>
|
||||
<div>Ovaj link vas vodi na stranicu gdje možete resetirati Vašu lozinku.</div>
|
||||
<div>Ovaj link Vas vodi na stranicu gdje možete resetirati Vašu lozinku.</div>
|
||||
<br>
|
||||
<div>Ukoliko to ne želite, molimo, ignorirajte ovu poruku. Vaša lozinka neće biti resetirana.</div>
|
||||
<br>
|
||||
|
||||
15
app/views/mailer/signup_taken_reset/tr.html.erb
Normal file
15
app/views/mailer/signup_taken_reset/tr.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
#{config.product_name} şifrenizi sıfırlayın
|
||||
|
||||
<div>Merhaba #{user.firstname},</div>
|
||||
<br>
|
||||
<div>Siz veya bir başkası bu e-posta adresiyle kaydolmayı denediniz. Ancak bu e-posta zaten kullanılıyor.</div>
|
||||
<br>
|
||||
<div>Şifrenizi sıfırlamak istiyorsanız aşağıdaki bağlantıya tıklayın (veya URL'yi kopyalayıp tarayıcınıza yapıştırın):</div>
|
||||
<br>
|
||||
<div><a href="#{url}">#{url</a></div>
|
||||
<br>
|
||||
<div>Bu bağlantı sizi şifrenizi değiştirebileceğiniz bir sayfaya yönlendirir.</div>
|
||||
<br>
|
||||
<div>Şifrenizi sıfırlamak istemiyorsanız lütfen bu mesajı dikkate almayın ve tamamen sildiğinizden emin olun. Şifreniz sıfırlanmayacak.</div>
|
||||
<br>
|
||||
<div>#{config.product_name} Ekibiniz</div>
|
||||
9
app/views/mailer/test_ticket/tr.html.erb
Normal file
9
app/views/mailer/test_ticket/tr.html.erb
Normal file
@@ -0,0 +1,9 @@
|
||||
Deneme Bileti!
|
||||
|
||||
<div>Sayın #{agent.firstname},</div>
|
||||
<br>
|
||||
<div>Bu bir <b>test biletidir</b>. Ben bir müşteriyim ve yardıma ihtiyacım var! :)</div>
|
||||
<br>
|
||||
<div>#{customer.fullname}</div>
|
||||
<br>
|
||||
<div>#{config.product_name} Ekibiniz</div>
|
||||
24
app/views/mailer/ticket_create/tr.html.erb
Normal file
24
app/views/mailer/ticket_create/tr.html.erb
Normal file
@@ -0,0 +1,24 @@
|
||||
Yeni bilet (#{ticket.title})
|
||||
|
||||
<div>Merhaba #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Yeni bilet (#{ticket.title}), "<b>#{current_user.longname}</b>" tarafından oluşturuldu.</div>
|
||||
<br>
|
||||
<div>
|
||||
Grup: #{ticket.group.name}<br>
|
||||
Sahip: #{ticket.owner.fullname}<br>
|
||||
Konum: #{t(ticket.state.name)}<br>
|
||||
</div>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
Bilgi:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">Görüntüle #{config.product_name}</a>
|
||||
</div>
|
||||
18
app/views/mailer/ticket_escalation/tr.html.erb
Normal file
18
app/views/mailer/ticket_escalation/tr.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
Bilet eskale edildi (#{ticket.title})
|
||||
|
||||
<div>Merhaba #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>(#{ticket.title}) bileti "<b>#{ticket.customer.longname}</b>" tarafından, "#{ticket.escalation_at}" eskale edildi.</div>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
Bilgi:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{config.product_name} üzerinde görüntüle</a>
|
||||
</div>
|
||||
19
app/views/mailer/user_device_new/he-il.html.erb
Normal file
19
app/views/mailer/user_device_new/he-il.html.erb
Normal file
@@ -0,0 +1,19 @@
|
||||
#{config.product_name} התחברות ממכשיר חדש
|
||||
|
||||
<div>שלום #{user.firstname},</div>
|
||||
<br>
|
||||
<div>נראה שנכנסת לחשבון שלך <b>באמצעות מכשיר חדש</b> ב-"#{user_device.created_at}":</div>
|
||||
<br>
|
||||
<div>
|
||||
המכשיר שלך: #{user_device.name}<br>
|
||||
המיקום שלך (יחסי): #{user_device.location}<br>
|
||||
ה-IP שלך: #{user_device.ip}<br>
|
||||
</div>
|
||||
<br>
|
||||
<div>המכשיר שלך נוסף לרשימת המכשירים המוכרים שלך, שאותה תוכל להציג כאן:</div>
|
||||
<br>
|
||||
<div><a href="#{config.http_type}://#{config.fqdn}/#profile/devices">#{config.http_type}://#{config.fqdn}/#profile/devices </a></div>
|
||||
<br>
|
||||
<div>אם זה לא היית אתה, אנא הסר את המכשיר, שנה את סיסמת החשבון שלך ופנה למנהל המערכת שלך. ייתכן שמישהו קיבל גישה לא מורשית לחשבון שלך.</div>
|
||||
<br>
|
||||
<div>צוות #{config.product_name} שלך</div>
|
||||
13
app/views/mailer/user_invite/tr.html.erb
Normal file
13
app/views/mailer/user_invite/tr.html.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
#{config.fqdn} adresindeki #{config.product_name} için davetiye
|
||||
|
||||
<div>Merhaba #{user.firstname},</div>
|
||||
<br>
|
||||
<div>Ben (#{current_user.firstname} #{current_user.lastname}) sizi müşteri destek platformumuz #{config.product_name}'e davet ediyorum.</div>
|
||||
<br>
|
||||
<div><a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.token}">burayı</a> tıklayın ve şifrenizi ayarlayın.</div>
|
||||
<br>
|
||||
<div>İyi günler...</div>
|
||||
<br>
|
||||
<div>#{current_user.firstname} #{current_user.lastname}</div>
|
||||
<br>
|
||||
<div>#{config.product_name} Ekibiniz</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user