Compare commits
257 Commits
develop
...
stable-6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26dc27b46e | ||
|
|
5c5b5fba3c | ||
|
|
09681ceab3 | ||
|
|
694000981d | ||
|
|
6ec2b8d253 | ||
|
|
67b236f68e | ||
|
|
7455f2ac7e | ||
|
|
00dde80348 | ||
|
|
d82ac357f6 | ||
|
|
2395fd0d67 | ||
|
|
46d75550cb | ||
|
|
fe37d1408a | ||
|
|
0decfb6119 | ||
|
|
60f14b59de | ||
|
|
fe2c9d9abd | ||
|
|
43616d3482 | ||
|
|
d59270798d | ||
|
|
7f65c0632e | ||
|
|
f6b780c1b8 | ||
|
|
0b631ebdb0 | ||
|
|
10715eca05 | ||
|
|
37516af574 | ||
|
|
043120cb38 | ||
|
|
012f549d7f | ||
|
|
d68e524017 | ||
|
|
32846b768c | ||
|
|
81841e44df | ||
|
|
72eb74dc53 | ||
|
|
30e843fa41 | ||
|
|
3ab8f7ff38 | ||
|
|
64f44c1a5c | ||
|
|
7d37bb213d | ||
|
|
817498f672 | ||
|
|
106af4c8f1 | ||
|
|
d8cdee1068 | ||
|
|
3af12932c0 | ||
|
|
e8cc0366bd | ||
|
|
79d6f9f359 | ||
|
|
b8b340b94f | ||
|
|
8551eeb846 | ||
|
|
dd340a1c1f | ||
|
|
54a5ebb988 | ||
|
|
5dc1863344 | ||
|
|
4de147c209 | ||
|
|
647b4fc419 | ||
|
|
5910c2d30b | ||
|
|
c81e7ad5a9 | ||
|
|
082791882e | ||
|
|
e18fb1f7d9 | ||
|
|
f95b780e23 | ||
|
|
b36304a200 | ||
|
|
f17c13bc29 | ||
|
|
d3b98be5d0 | ||
|
|
c22e585a8b | ||
|
|
3efbdc41d9 | ||
|
|
2fa5681c5e | ||
|
|
5289175836 | ||
|
|
0ffcde3420 | ||
|
|
724dac2ba4 | ||
|
|
c6220d2067 | ||
|
|
6840299f91 | ||
|
|
4ec564ca43 | ||
|
|
91ea166591 | ||
|
|
1aa6b7d19b | ||
|
|
d42b8d6e92 | ||
|
|
950693039d | ||
|
|
13d89f9d23 | ||
|
|
fe9acc2256 | ||
|
|
133fd2a530 | ||
|
|
857662ab01 | ||
|
|
11b8cf8508 | ||
|
|
dd31749656 | ||
|
|
4dc4436684 | ||
|
|
41b98ccb43 | ||
|
|
8533f292ab | ||
|
|
c29de462fd | ||
|
|
6a172def9e | ||
|
|
187a102179 | ||
|
|
d0f6e5b923 | ||
|
|
cfbdf6a2b9 | ||
|
|
d7df27630c | ||
|
|
ea95e368d1 | ||
|
|
0db679bc07 | ||
|
|
5c71daf26d | ||
|
|
42c433270a | ||
|
|
6ae8bc5c5a | ||
|
|
9d3d0498a9 | ||
|
|
e20def4b4e | ||
|
|
67119f8e2c | ||
|
|
c190c5de12 | ||
|
|
d239b0061d | ||
|
|
8510c4d51c | ||
|
|
6c6b557182 | ||
|
|
7b187dbf8e | ||
|
|
e504ff79cd | ||
|
|
ef7af6b5ee | ||
|
|
dafad93820 | ||
|
|
8b8eececee | ||
|
|
26aefbf2b1 | ||
|
|
2dd8229cce | ||
|
|
03843b6f2a | ||
|
|
e99450cdcc | ||
|
|
fc8bee392d | ||
|
|
43d58aa518 | ||
|
|
ce363b965b | ||
|
|
be83838240 | ||
|
|
f34779e2f8 | ||
|
|
f7062be223 | ||
|
|
0169f773a5 | ||
|
|
877871ebc3 | ||
|
|
b6ba68bf7e | ||
|
|
efd003fa5a | ||
|
|
e7da6df993 | ||
|
|
b0190017e6 | ||
|
|
75bed74dd0 | ||
|
|
4aa8d7483e | ||
|
|
85b53b8108 | ||
|
|
9d09987e58 | ||
|
|
92fb5d7c5e | ||
|
|
45c9b70f98 | ||
|
|
f6e023397b | ||
|
|
e64193201f | ||
|
|
300297cee8 | ||
|
|
7c6b5ccb18 | ||
|
|
17f20fad89 | ||
|
|
155ae5199f | ||
|
|
9613090798 | ||
|
|
9449805d3d | ||
|
|
58ae1e031b | ||
|
|
4363a06568 | ||
|
|
80954e2ca9 | ||
|
|
acd6aa5ff9 | ||
|
|
53e2ff71f5 | ||
|
|
d9607d6626 | ||
|
|
9282855701 | ||
|
|
0d519f4847 | ||
|
|
31d0e8dd1f | ||
|
|
aa99d4f58d | ||
|
|
4b927c6d65 | ||
|
|
72a2064fa5 | ||
|
|
83cd208611 | ||
|
|
337f0f5d4f | ||
|
|
d71f3893ec | ||
|
|
70d437769d | ||
|
|
8ca3e48a67 | ||
|
|
f432a000c2 | ||
|
|
0f341764a7 | ||
|
|
f8203bdb41 | ||
|
|
1a72fc963d | ||
|
|
27f6f1299b | ||
|
|
155dfc8a41 | ||
|
|
dee21b8044 | ||
|
|
9d65e9875b | ||
|
|
7cdd07224f | ||
|
|
d1b7094cc8 | ||
|
|
9a09fca169 | ||
|
|
f61b210ff4 | ||
|
|
c86ed47083 | ||
|
|
cb2d32bea8 | ||
|
|
13cd2191ec | ||
|
|
85c573f427 | ||
|
|
ec170d6c90 | ||
|
|
2cd741c2b2 | ||
|
|
5e01876187 | ||
|
|
bfe18b8e60 | ||
|
|
9cec2fc1bf | ||
|
|
57a875ca37 | ||
|
|
bc15b8038f | ||
|
|
c0ac58de29 | ||
|
|
632ebdc4bb | ||
|
|
553c120284 | ||
|
|
af3037135a | ||
|
|
3093227604 | ||
|
|
fd8871d473 | ||
|
|
f8bb3f3d4a | ||
|
|
9724fedc8c | ||
|
|
ae2be7a4fe | ||
|
|
3f2635b9c9 | ||
|
|
44d0e11d97 | ||
|
|
5c287e0f0f | ||
|
|
3b75df1ea6 | ||
|
|
b22e3e99de | ||
|
|
466794ba9d | ||
|
|
358d268c43 | ||
|
|
929318d101 | ||
|
|
ea16c72751 | ||
|
|
029607ccec | ||
|
|
591d7063a8 | ||
|
|
446df150f0 | ||
|
|
472976049b | ||
|
|
d85e5735d4 | ||
|
|
bf20565468 | ||
|
|
125152c2e2 | ||
|
|
a262c56ff7 | ||
|
|
c7fd5da9d8 | ||
|
|
f957db99ef | ||
|
|
3ff82009ea | ||
|
|
5cc8417554 | ||
|
|
48881ba10e | ||
|
|
e8508ec1e5 | ||
|
|
4b860ebe59 | ||
|
|
acd8826bb5 | ||
|
|
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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,6 +16,9 @@
|
||||
# storage (copy from config/zammad/storage.yml.dist)
|
||||
/config/zammad/storage.yml
|
||||
|
||||
# Personal initializers
|
||||
/config/initializers/*.local.rb
|
||||
|
||||
# local backup config file
|
||||
/contrib/backup/config
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
#
|
||||
# Test the migration from the previous `stable` branch to `develop`.
|
||||
# Test the migration from the `stable-6.0` branch to `develop`.
|
||||
#
|
||||
.template_migration:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME =~ /^stable/
|
||||
when: never
|
||||
- if: $CI_COMMIT_REF_NAME =~ /^(private|cherry-pick-)/
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: on_success
|
||||
before_script: []
|
||||
script:
|
||||
# First, checkout stable and set it up.
|
||||
- git fetch --unshallow
|
||||
- git checkout stable
|
||||
- git checkout stable-6.0
|
||||
- !reference [.scripts, source_rvm]
|
||||
- rvm use 3.1.3
|
||||
- !reference [.scripts, bundle_install]
|
||||
@@ -22,7 +15,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
|
||||
|
||||
@@ -14,7 +14,7 @@ paths = [
|
||||
'''^spec/fixtures/files/ldap/.*\.crt''',
|
||||
'''^spec/fixtures/files/pgp/.*\.asc''',
|
||||
'''^spec/fixtures/files/smime/.*\.key''',
|
||||
'''^test/data/mail/mail015\.(yml|box)''',
|
||||
'''^test/data/mail/mail\d+\.(yml|box)''',
|
||||
'''^test/data/saml/zammad-client.json''',
|
||||
'''^test/data/telegram/private/sticker.json''',
|
||||
'''^test/data/vcr_cassettes/''',
|
||||
|
||||
19
.pkgr.yml
19
.pkgr.yml
@@ -86,6 +86,19 @@ targets:
|
||||
- libimlib2
|
||||
- libimlib2-dev
|
||||
- shared-mime-info
|
||||
ubuntu-24.04:
|
||||
dependencies:
|
||||
- curl
|
||||
- elasticsearch|elasticsearch-oss
|
||||
- nginx|apache2
|
||||
- postgresql|mysql-server|mariadb-server
|
||||
- libimlib2
|
||||
- shared-mime-info
|
||||
- redis-server
|
||||
build_dependencies:
|
||||
- libimlib2
|
||||
- libimlib2-dev
|
||||
- shared-mime-info
|
||||
sles-15:
|
||||
dependencies:
|
||||
- curl
|
||||
@@ -95,6 +108,10 @@ targets:
|
||||
- libImlib2-1
|
||||
- imlib2
|
||||
- shared-mime-info
|
||||
# We need the fuser binary from the psmisc package to detect the running redis systemd service.
|
||||
# Because on SUSE systems, this service is arbitrarily named (e.g.) redis@foobar.service
|
||||
# So we cannot hardcode the service name as we do for other supported distros.
|
||||
- psmisc
|
||||
build_dependencies:
|
||||
# Add packages required for build that are not in the official SLES repo.
|
||||
# Direct URLs must be used since we cannot add repos on packager.io
|
||||
@@ -119,4 +136,4 @@ services:
|
||||
- redis
|
||||
before_install: contrib/packager.io/preinstall.sh
|
||||
after_install: contrib/packager.io/postinstall.sh
|
||||
buildpack: https://github.com/pkgr/heroku-buildpack-ruby.git#v267-1
|
||||
buildpack: https://github.com/pkgr/heroku-buildpack-ruby.git#v267-1
|
||||
|
||||
@@ -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
|
||||
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,9 +1,28 @@
|
||||
# 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
|
||||
|
||||
8
Gemfile
8
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
|
||||
@@ -197,13 +197,13 @@ gem 'pry-rescue'
|
||||
gem 'pry-stack_explorer'
|
||||
gem 'pry-theme'
|
||||
|
||||
# watch file changes (also relevant for graphql generation in context of CDs)
|
||||
gem 'listen'
|
||||
|
||||
# Gems used only for develop/test and not required
|
||||
# in production environments by default.
|
||||
group :development, :test do
|
||||
|
||||
# watch file changes
|
||||
gem 'listen'
|
||||
|
||||
# test frameworks
|
||||
gem 'minitest-profile', require: false
|
||||
gem 'rails-controller-testing'
|
||||
|
||||
140
Gemfile.lock
140
Gemfile.lock
@@ -32,59 +32,59 @@ GEM
|
||||
simple_po_parser (~> 1.1.6)
|
||||
aasm (5.5.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
actioncable (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
actioncable (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activejob (= 7.0.8.1)
|
||||
activerecord (= 7.0.8.1)
|
||||
activestorage (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
actionmailbox (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
actionview (= 7.0.8.1)
|
||||
activejob (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
actionmailer (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8.1)
|
||||
actionview (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
actionpack (7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activerecord (= 7.0.8.1)
|
||||
activestorage (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
actiontext (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
actionview (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
activejob (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
activerecord (7.0.8.1)
|
||||
activemodel (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
activemodel (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activerecord (7.0.8.5)
|
||||
activemodel (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activerecord-import (1.6.0)
|
||||
activerecord (>= 4.2)
|
||||
activerecord-session_store (2.1.0)
|
||||
@@ -94,14 +94,14 @@ GEM
|
||||
multi_json (~> 1.11, >= 1.11.2)
|
||||
rack (>= 2.0.8, < 4)
|
||||
railties (>= 6.1)
|
||||
activestorage (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activejob (= 7.0.8.1)
|
||||
activerecord (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
activestorage (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.8.1)
|
||||
activesupport (7.0.8.5)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -284,7 +284,7 @@ GEM
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.4)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
icalendar (2.10.1)
|
||||
ice_cube (~> 0.16)
|
||||
@@ -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.7)
|
||||
minitest (5.22.3)
|
||||
minitest-profile (0.0.2)
|
||||
msgpack (1.7.2)
|
||||
@@ -349,7 +349,7 @@ GEM
|
||||
time
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-imap (0.4.10)
|
||||
net-imap (0.4.17)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.19.0)
|
||||
@@ -359,9 +359,9 @@ GEM
|
||||
timeout
|
||||
net-smtp (0.4.0.1)
|
||||
net-protocol
|
||||
nio4r (2.7.0)
|
||||
nio4r (2.7.3)
|
||||
nkf (0.2.0)
|
||||
nokogiri (1.16.3)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nori (2.7.0)
|
||||
@@ -410,9 +410,9 @@ GEM
|
||||
omniauth-rails_csrf_protection (1.0.1)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-saml (2.1.0)
|
||||
omniauth (~> 2.0)
|
||||
ruby-saml (~> 1.12)
|
||||
omniauth-saml (2.2.1)
|
||||
omniauth (~> 2.1)
|
||||
ruby-saml (~> 1.17)
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
@@ -453,7 +453,7 @@ GEM
|
||||
pry-theme (1.3.1)
|
||||
coderay (~> 1.1)
|
||||
public_suffix (5.0.4)
|
||||
puma (6.4.2)
|
||||
puma (6.4.3)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.1)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -462,8 +462,8 @@ GEM
|
||||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
racc (1.7.3)
|
||||
rack (2.2.8.1)
|
||||
racc (1.8.1)
|
||||
rack (2.2.10)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-protection (3.2.0)
|
||||
@@ -473,20 +473,20 @@ GEM
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (7.0.8.1)
|
||||
actioncable (= 7.0.8.1)
|
||||
actionmailbox (= 7.0.8.1)
|
||||
actionmailer (= 7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
actiontext (= 7.0.8.1)
|
||||
actionview (= 7.0.8.1)
|
||||
activejob (= 7.0.8.1)
|
||||
activemodel (= 7.0.8.1)
|
||||
activerecord (= 7.0.8.1)
|
||||
activestorage (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
rails (7.0.8.5)
|
||||
actioncable (= 7.0.8.5)
|
||||
actionmailbox (= 7.0.8.5)
|
||||
actionmailer (= 7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
actiontext (= 7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activemodel (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.8.1)
|
||||
railties (= 7.0.8.5)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
@@ -498,9 +498,9 @@ GEM
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
railties (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
@@ -513,7 +513,7 @@ GEM
|
||||
rchardet (1.8.0)
|
||||
redis (4.8.1)
|
||||
regexp_parser (2.9.0)
|
||||
rexml (3.2.6)
|
||||
rexml (3.3.9)
|
||||
rotp (6.3.0)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
@@ -574,7 +574,7 @@ GEM
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-saml (1.16.0)
|
||||
ruby-saml (1.17.0)
|
||||
nokogiri (>= 1.13.10)
|
||||
rexml
|
||||
rubyntlm (0.6.3)
|
||||
@@ -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)
|
||||
@@ -633,7 +633,7 @@ GEM
|
||||
execjs (>= 0.3.0, < 3)
|
||||
test-unit (3.6.2)
|
||||
power_assert
|
||||
thor (1.3.1)
|
||||
thor (1.3.2)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.3.0)
|
||||
time (0.3.0)
|
||||
@@ -709,7 +709,7 @@ GEM
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yard (0.9.36)
|
||||
zeitwerk (2.6.13)
|
||||
zeitwerk (2.6.18)
|
||||
zendesk_api (2.0.1)
|
||||
faraday (> 2.0.0)
|
||||
faraday-multipart
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class App.ControllerModal extends App.Controller
|
||||
|
||||
update: =>
|
||||
if @message
|
||||
content = App.i18n.translateContent(@message)
|
||||
content = App.Utils.htmlEscape( App.i18n.translatePlain(@message) )
|
||||
else if @contentInline
|
||||
content = @contentInline
|
||||
else
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -479,10 +479,12 @@ class App.ControllerForm extends App.Controller
|
||||
|
||||
if !@constructor.fieldIsReadonly(field_by_name)
|
||||
field_by_name.closest('.form-group').find('input, select, textarea, .form-control').attr('readonly', true)
|
||||
field_by_name.closest('.form-group').find('input, select, textarea, .form-control').attr('tabindex', '-1')
|
||||
field_by_name.closest('.form-group').find('input[type=file]').attr('disabled', true)
|
||||
field_by_name.closest('.form-group').addClass('is-readonly')
|
||||
if !@constructor.fieldIsReadonly(field_by_data)
|
||||
field_by_data.closest('.form-group').find('input, select, textarea, .form-control').attr('readonly', true)
|
||||
field_by_data.closest('.form-group').find('input, select, textarea, .form-control').attr('tabindex', '-1')
|
||||
field_by_data.closest('.form-group').find('input[type=file]').attr('disabled', true)
|
||||
field_by_data.closest('.form-group').addClass('is-readonly')
|
||||
|
||||
@@ -495,10 +497,12 @@ class App.ControllerForm extends App.Controller
|
||||
|
||||
if @constructor.fieldIsReadonly(field_by_name)
|
||||
field_by_name.closest('.form-group').find('input, select, textarea, .form-control').attr('readonly', false)
|
||||
field_by_name.closest('.form-group').find('input, select, textarea, .form-control').removeAttr('tabindex')
|
||||
field_by_name.closest('.form-group').find('input[type=file]').attr('disabled', false)
|
||||
field_by_name.closest('.form-group').removeClass('is-readonly')
|
||||
if @constructor.fieldIsReadonly(field_by_data)
|
||||
field_by_data.closest('.form-group').find('input, select, textarea, .form-control').attr('readonly', false)
|
||||
field_by_data.closest('.form-group').find('input, select, textarea, .form-control').removeAttr('tabindex')
|
||||
field_by_data.closest('.form-group').find('input[type=file]').attr('disabled', false)
|
||||
field_by_data.closest('.form-group').removeClass('is-readonly')
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class ChannelChat extends App.ControllerSubContent
|
||||
'click .js-toggle-chat': 'toggleChat'
|
||||
'change .js-chatSetting input': 'toggleChatSetting'
|
||||
'click .js-eyedropper': 'pickColor'
|
||||
'change .js-chat-jquery-widget': 'switchScriptPreview'
|
||||
|
||||
elements:
|
||||
'.js-browser': 'browser'
|
||||
@@ -303,6 +304,10 @@ class ChannelChat extends App.ControllerSubContent
|
||||
|
||||
@colorField.val("rgb(#{pixels.slice(0,3).join(',')})").trigger('change')
|
||||
|
||||
switchScriptPreview: (e) ->
|
||||
@$('.js-chat-jquery-widget-jquery').toggleClass('hide', !e.currentTarget.checked)
|
||||
@$('.js-chat-jquery-widget-vanilla').toggleClass('hide', e.currentTarget.checked)
|
||||
|
||||
toggleChat: =>
|
||||
@chat.toggleClass('is-open')
|
||||
@isOpen = @chat.hasClass('is-open')
|
||||
|
||||
@@ -51,6 +51,7 @@ class App.Theme extends App.Controller
|
||||
App.Event.trigger('ui:theme:saved', data)
|
||||
|
||||
document.documentElement.dataset.theme = @currentTheme(data.theme)
|
||||
document.documentElement.style.colorScheme = @currentTheme(data.theme)
|
||||
App.Event.trigger('ui:theme:changed', data)
|
||||
|
||||
App.Config.set('theme', App.Theme, 'Plugins')
|
||||
|
||||
@@ -62,11 +62,13 @@ class App.UiElement.ApplicationSelector
|
||||
operators_name =
|
||||
'_id$': [__('is'), __('is not')]
|
||||
'_ids$': [__('is'), __('is not')]
|
||||
'active$': [__('is'), __('is not')]
|
||||
|
||||
if attribute.hasChanged
|
||||
operators_name =
|
||||
'_id$': [__('is'), __('is not'), __('has changed')]
|
||||
'_ids$': [__('is'), __('is not'), __('has changed')]
|
||||
'active$': [__('is'), __('is not'), __('has changed')]
|
||||
|
||||
# merge config
|
||||
elements = {}
|
||||
@@ -390,7 +392,7 @@ class App.UiElement.ApplicationSelector
|
||||
selection = $('<select class="form-control"></select>')
|
||||
for groupKey, groupMeta of groups
|
||||
groupKeyClass = groupKey.replace('.', '-')
|
||||
displayName = App.i18n.translateInline(groupMeta.name)
|
||||
displayName = App.i18n.translatePlain(groupMeta.name)
|
||||
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKeyClass}\"></optgroup>")
|
||||
optgroup = selection.find("optgroup.js-#{groupKeyClass}")
|
||||
for elementKey, elementGroup of elements
|
||||
@@ -398,7 +400,7 @@ class App.UiElement.ApplicationSelector
|
||||
if spacer is groupKey
|
||||
attributeConfig = elements[elementKey]
|
||||
if attributeConfig.operator
|
||||
displayName = App.i18n.translateInline(attributeConfig.display)
|
||||
displayName = App.i18n.translatePlain(attributeConfig.display)
|
||||
optgroup.append("<option value=\"#{elementKey}\">#{displayName}</option>")
|
||||
selection
|
||||
|
||||
@@ -471,7 +473,7 @@ class App.UiElement.ApplicationSelector
|
||||
break
|
||||
|
||||
for operator in attributeConfig.operator
|
||||
operatorName = App.i18n.translateInline(@mapOperatorDisplayName(operator))
|
||||
operatorName = App.i18n.translatePlain(@mapOperatorDisplayName(operator))
|
||||
selected = ''
|
||||
if !groupAndAttribute.match(/^ticket/) && operator is 'has changed'
|
||||
# do nothing, only show "has changed" in ticket attributes
|
||||
@@ -534,22 +536,22 @@ class App.UiElement.ApplicationSelector
|
||||
options = {}
|
||||
if preCondition is 'user'
|
||||
if attributeConfig.noCurrentUser isnt true
|
||||
options['current_user.id'] = App.i18n.translateInline('current user')
|
||||
options['specific'] = App.i18n.translateInline('specific user')
|
||||
options['current_user.id'] = App.i18n.translatePlain('current user')
|
||||
options['specific'] = App.i18n.translatePlain('specific user')
|
||||
if attributeConfig.noNotSet isnt true
|
||||
options['not_set'] = App.i18n.translateInline('not set (not defined)')
|
||||
options['not_set'] = App.i18n.translatePlain('not set (not defined)')
|
||||
else if preCondition is 'org'
|
||||
if attributeConfig.noCurrentUser isnt true
|
||||
options['current_user.organization_id'] = App.i18n.translateInline('current user organization')
|
||||
options['specific'] = App.i18n.translateInline('specific organization')
|
||||
options['current_user.organization_id'] = App.i18n.translatePlain('current user organization')
|
||||
options['specific'] = App.i18n.translatePlain('specific organization')
|
||||
if attributeConfig.noNotSet isnt true
|
||||
options['not_set'] = App.i18n.translateInline('not set (not defined)')
|
||||
options['not_set'] = App.i18n.translatePlain('not set (not defined)')
|
||||
|
||||
for key, value of options
|
||||
selected = ''
|
||||
if key is meta.pre_condition
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
|
||||
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translatePlain(value)}</option>")
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
elementRow.find('.js-preCondition select').replaceWith(selection)
|
||||
|
||||
|
||||
@@ -749,17 +749,18 @@ class App.UiElement.ApplicationSelectorExpert extends App.UiElement.ApplicationS
|
||||
value.value = element.find('select.js-value').val()
|
||||
else if element.find('.js-value .js-objectId')?.val()
|
||||
value.value = element.find('.js-value .js-objectId').val()
|
||||
else if element.find('.js-value .js-shadow')?.val()
|
||||
else if element.find('.js-value .js-shadow:not([data-value-type])')?.val()
|
||||
value.value = element.find('.js-value .js-shadow').val()
|
||||
else if element.find('[data-value]').length
|
||||
valueField = element.find('[data-value]')
|
||||
else if element.find('[data-value]').length or element.find('[data-value-type]').length
|
||||
valueField = element.find('[data-value],[data-value-type]')
|
||||
dataValue = if valueField.data('value') then valueField.data('value') else valueField.val()
|
||||
if valueField.data('valueType') is 'json'
|
||||
try
|
||||
value.value = JSON.parse(valueField.data('value'))
|
||||
value.value = JSON.parse(dataValue)
|
||||
catch
|
||||
App.Log.error 'App.UiElement.ticket_selector', 'Invalid JSON value for a subfield', valueField
|
||||
else
|
||||
value.value = valueField.data('value')
|
||||
value.value = dataValue
|
||||
else if element.find('.js-value input.form-control')?.val()
|
||||
value.value = element.find('.js-value input.form-control').val()
|
||||
else if element.find('.js-value .form-control')?.val()
|
||||
|
||||
@@ -80,6 +80,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
|
||||
operatorsName =
|
||||
'_id$': [__('is'), __('is not'), __('is set'), __('not set'), __('has changed'), __('changed to')]
|
||||
'_ids$': [__('is'), __('is not'), __('is set'), __('not set'), __('has changed'), __('changed to')]
|
||||
'active$': [__('is'), __('is not'), __('is set'), __('not set'), __('has changed'), __('changed to')]
|
||||
|
||||
if attribute.disable_operators
|
||||
for key, value of operatorsType
|
||||
|
||||
@@ -72,9 +72,10 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
||||
configureAttributes.splice(_.findIndex(configureAttributes, (e) -> e.name is 'title') + 1, 0, { name: 'body', display: __('Text'), data_type: 'richtext', tag: 'richtext', rows: 5, limit: 100, null: false })
|
||||
|
||||
for config in configureAttributes
|
||||
continue if config.readonly
|
||||
continue if !_.contains(['input', 'textarea', 'richtext', 'select', 'multiselect', 'integer', 'boolean', 'multi_tree_select', 'tree_select', 'autocompletion_ajax_external_data_source', 'date', 'datetime'], config.tag)
|
||||
continue if _.contains(['created_at', 'updated_at'], config.name)
|
||||
continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'escalation_at', 'first_response_escalation_at', 'update_escalation_at', 'close_escalation_at', 'last_contact_at', 'last_contact_agent_at', 'last_contact_customer_at', 'first_response_at', 'close_at'], config.name)
|
||||
continue if groupKey is 'ticket' && _.contains(['organization_id'], config.name)
|
||||
continue if groupKey is 'group' && _.contains(['name'], config.name)
|
||||
|
||||
# ignore passwords and relations
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -89,11 +89,13 @@ class App.UiElement.object_selector extends App.UiElement.ApplicationSelectorExp
|
||||
operators_name =
|
||||
'_id$': [__('is'), __('is not')]
|
||||
'_ids$': [__('is'), __('is not')]
|
||||
'active$': [__('is'), __('is not')]
|
||||
|
||||
if attribute.hasChanged
|
||||
operators_name =
|
||||
'_id$': [__('is'), __('is not'), __('has changed')]
|
||||
'_ids$': [__('is'), __('is not'), __('has changed')]
|
||||
'active$': [__('is'), __('is not'), __('has changed')]
|
||||
|
||||
# merge config
|
||||
elements = {}
|
||||
|
||||
@@ -11,9 +11,9 @@ class App.UiElement.tag
|
||||
a = ->
|
||||
$('#' + attribute.id ).tokenfield(
|
||||
createTokensOnBlur: true
|
||||
showAutocompleteOnFocus: true
|
||||
autocomplete: {
|
||||
source: source
|
||||
minLength: 1
|
||||
response: (e, ui) ->
|
||||
return if !ui
|
||||
return if !ui.content
|
||||
|
||||
@@ -116,7 +116,7 @@ class App.UiElement.timer
|
||||
hours = @joinItems hours
|
||||
timezone = App.Config.get('timezone_default')
|
||||
|
||||
formGroup.find('.js-timerResult').text(App.i18n.translateInline('Run every %s at %s in %s time', days, hours, timezone))
|
||||
formGroup.find('.js-timerResult').html(App.i18n.translateInline('Run every %s at %s in %s time', days, hours, timezone))
|
||||
|
||||
@injectMinutes: (hours, minutes) ->
|
||||
newHours = [] # hours.length x minutes.length long
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -144,7 +144,7 @@ class App.TicketCreate extends App.Controller
|
||||
@updateSecurityOptions()
|
||||
)
|
||||
|
||||
@listenTo(App.Group, 'refresh', =>
|
||||
@controllerBind('Group:create Group:update Group:touch Group:destroy', =>
|
||||
@sidebarWidget.render(@params())
|
||||
)
|
||||
|
||||
@@ -616,6 +616,19 @@ class App.TicketCreate extends App.Controller
|
||||
# to replace in text modules properly
|
||||
params.customer = App.User.find(params.customer_id) || {}
|
||||
|
||||
# if customer is given in params (e. g. from CTI integration) show selected customer
|
||||
# display name with email address (if exists) in customer attribute in UI
|
||||
fillCompletionIfRequiredCallback = =>
|
||||
return if !_.isEmpty(@el.find('input[name=customer_id_completion]').val())
|
||||
return if !params.customer
|
||||
return if !params.customer.displayName
|
||||
completion = params.customer.displayName()
|
||||
if params.customer.email
|
||||
completion = App.Utils.buildEmailAddress(params.customer.displayName(), params.customer.email)
|
||||
return if !completion
|
||||
@el.find('input[name=customer_id_completion]').val(completion)
|
||||
@delay(fillCompletionIfRequiredCallback, 10)
|
||||
|
||||
@sidebarWidget.render(params)
|
||||
@textModule.reload(
|
||||
config: App.Config.all()
|
||||
|
||||
494
app/assets/javascripts/app/controllers/article_view/item.coffee
Normal file
494
app/assets/javascripts/app/controllers/article_view/item.coffee
Normal file
@@ -0,0 +1,494 @@
|
||||
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'
|
||||
'click .js-fetchOriginalFormatting': 'fetchOriginalFormatting'
|
||||
|
||||
@SEE_MORE_MIN_HEIGHT: 90
|
||||
@SEE_MORE_MAX_HEIGHT: 560
|
||||
|
||||
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
|
||||
@originalFormattingURL = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?disposition=attachment"
|
||||
link =
|
||||
url: @originalFormattingURL
|
||||
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 = @constructor.SEE_MORE_MAX_HEIGHT
|
||||
minHeight = @constructor.SEE_MORE_MIN_HEIGHT
|
||||
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)
|
||||
)
|
||||
|
||||
fetchOriginalFormatting: (e) ->
|
||||
return if not @originalFormattingURL
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
originalFormattingLink = document.createElement('a')
|
||||
originalFormattingLink.href = @originalFormattingURL
|
||||
originalFormattingLink.target = '_blank'
|
||||
originalFormattingLink.click()
|
||||
originalFormattingLink.remove()
|
||||
|
||||
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
|
||||
@@ -174,6 +174,10 @@ class CustomerTicketCreate extends App.ControllerAppContent
|
||||
ticket.save(
|
||||
done: ->
|
||||
|
||||
# Reset article after ticket create, to avoid unwanted sideeffects at other places.
|
||||
localTicket = App.Ticket.findNative(@id)
|
||||
localTicket.article = undefined
|
||||
|
||||
# add sidebar params
|
||||
if ui.sidebarWidget
|
||||
ui.sidebarWidget.commit(ticket_id: @id)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -47,7 +47,7 @@ class App.KnowledgeBaseDelete extends App.KnowledgeBaseForm
|
||||
style: 'block'
|
||||
null: true
|
||||
display: __('Permanently Delete Knowledge Base')
|
||||
help: "Deleting your knowledge base requires an additional verification step. To proceed, enter its name below (\"#{@object().guaranteedTitle()}\"). THIS ACTION CANNOT BE UNDONE."
|
||||
help: App.i18n.translatePlain('Deleting your knowledge base requires an additional verification step. To proceed, enter its name below ("%s"). THIS ACTION CANNOT BE UNDONE.', @object().guaranteedTitle())
|
||||
tag: 'input'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -42,7 +42,7 @@ class Login extends App.ControllerFullPage
|
||||
data: JSON.stringify(params)
|
||||
processData: true
|
||||
success: (verify_data, status, xhr) =>
|
||||
data.showAdminPasswordLogin = true
|
||||
@showAdminPasswordLogin = true
|
||||
data.username = verify_data.user_login
|
||||
|
||||
@render(data)
|
||||
@@ -83,6 +83,9 @@ class Login extends App.ControllerFullPage
|
||||
App.PublicLink.unsubscribe(@publicLinksSubscribeId)
|
||||
|
||||
render: (data = {}) ->
|
||||
if @showAdminPasswordLogin
|
||||
data.showAdminPasswordLogin = true
|
||||
|
||||
auth_provider_all = App.Config.get('auth_provider_all')
|
||||
auth_providers = []
|
||||
for key, provider of auth_provider_all
|
||||
|
||||
@@ -23,7 +23,6 @@ class Package extends App.ControllerSubContent
|
||||
success: (data) =>
|
||||
@packages = data.packages
|
||||
@package_installation = data.package_installation
|
||||
@local_gemfiles = data.local_gemfiles
|
||||
@render()
|
||||
)
|
||||
|
||||
@@ -43,7 +42,6 @@ class Package extends App.ControllerSubContent
|
||||
head: __('Dashboard')
|
||||
packages: @packages
|
||||
package_installation: @package_installation
|
||||
local_gemfiles: @local_gemfiles
|
||||
)
|
||||
|
||||
action: (e) ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ class App.Search extends App.Controller
|
||||
@formMeta = data.form_meta
|
||||
@bindId = App.TicketOverviewCollection.bind(load)
|
||||
|
||||
release: =>
|
||||
App.TicketOverviewCollection.unbindById(@bindId)
|
||||
|
||||
meta: =>
|
||||
title = @query || App.i18n.translateInline('Extended Search')
|
||||
|
||||
|
||||
@@ -106,8 +106,6 @@ class App.TicketOverview extends App.Controller
|
||||
@contentController.show()
|
||||
return
|
||||
|
||||
App.TicketOverviewCollection.fetch()
|
||||
|
||||
# remember last view
|
||||
@viewLast = @view
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,7 +37,7 @@ class App.TicketZoomAttributeBar extends App.Controller
|
||||
@render(data)
|
||||
)
|
||||
|
||||
@listenTo(App.Group, 'refresh', (refreshed_group) =>
|
||||
@controllerBind('Group:create Group:update Group:touch Group:destroy', (refreshed_group) =>
|
||||
selected_group_id = @el.closest('.content').find('[name=group_id]').val()
|
||||
selected_group = App.Group.find selected_group_id
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -73,12 +73,16 @@ class App.TicketZoomTimeAccounting extends App.ControllerModal
|
||||
return if !@cancelCallback
|
||||
@cancelCallback()
|
||||
|
||||
onSubmit: =>
|
||||
onSubmit: (e) =>
|
||||
e.preventDefault()
|
||||
@formDisable(e)
|
||||
|
||||
params = @formParams()
|
||||
|
||||
errors = @form.validate(params)
|
||||
|
||||
if !_.isEmpty(errors)
|
||||
@formEnable(e)
|
||||
@formValidate(form: @form.el, errors: errors)
|
||||
return false
|
||||
|
||||
@@ -87,6 +91,7 @@ class App.TicketZoomTimeAccounting extends App.ControllerModal
|
||||
if isNaN(parseFloat(params.time_unit)) or /[A-z]|\s/.test(params.time_unit)
|
||||
errors =
|
||||
time_unit: __('is not a number')
|
||||
@formEnable(e)
|
||||
@formValidate(form: @form.el, errors: errors)
|
||||
return false
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -89,6 +89,17 @@ class App.WidgetPlaceholder extends App.Controller
|
||||
content: content
|
||||
}
|
||||
|
||||
# Add HTML format of articles
|
||||
if (_.filter(all, (item) -> item.name.startsWith('Article')).length > 0)
|
||||
all.push {
|
||||
# coffeelint: disable=no_interpolation_in_single_quotes
|
||||
name: __('Article > Text (HTML)'),
|
||||
id: '#{article.body_as_html}',
|
||||
keywords: '#{article.body_as_html}',
|
||||
content: '#{article.body_as_html}',
|
||||
# coffeelint: enable=no_interpolation_in_single_quotes
|
||||
}
|
||||
|
||||
# modify article placeholders
|
||||
replaces = [
|
||||
{ display: __('Last Article'), name: 'last_article' },
|
||||
|
||||
@@ -77,6 +77,8 @@ class App.TicketBatch extends App.Controller
|
||||
startDragItem: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
App.TicketOverviewCollection.fetch()
|
||||
|
||||
@grabbedItem = $(event.currentTarget)
|
||||
offset = @grabbedItem.offset()
|
||||
@batchDragger = $(App.view('ticket_overview/batch_dragger')())
|
||||
|
||||
@@ -27,20 +27,19 @@ class App.TicketBulkForm extends App.Controller
|
||||
load = (data) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
@formMeta = data.form_meta
|
||||
@render()
|
||||
@bindId = App.TicketOverviewCollection.bind(load)
|
||||
|
||||
release: =>
|
||||
App.TicketOverviewCollection.unbind(@bindId)
|
||||
App.TicketOverviewCollection.unbindById(@bindId)
|
||||
|
||||
render: ->
|
||||
@el.css('right', App.Utils.getScrollBarWidth())
|
||||
@el.addClass('no-sidebar') if @noSidebar
|
||||
|
||||
@html(App.view('agent_ticket_view/bulk')())
|
||||
|
||||
handlers = @Config.get('TicketZoomFormHandler')
|
||||
|
||||
@controllerFormBulk?.releaseController()
|
||||
@controllerFormBulk = new App.ControllerForm(
|
||||
el: @$('#form-ticket-bulk')
|
||||
mixedAttributes: @configure_attributes_ticket
|
||||
@@ -56,7 +55,8 @@ class App.TicketBulkForm extends App.Controller
|
||||
|
||||
@controllerFormBulk.$('[data-attribute-name="group_id"] .controls').addClass('form-control')
|
||||
|
||||
new App.ControllerForm(
|
||||
@controllerFormBulkComment?.releaseController()
|
||||
@controllerFormBulkComment = new App.ControllerForm(
|
||||
el: @$('#form-ticket-bulk-comment')
|
||||
model:
|
||||
configure_attributes: [{ name: 'body', display: __('Comment'), tag: 'textarea', rows: 4, null: true, upload: false, item_class: 'flex' }]
|
||||
@@ -71,7 +71,8 @@ class App.TicketBulkForm extends App.Controller
|
||||
{ name: 'internal', display: __('Visibility'), tag: 'select', null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: '', default: false, translate: true }
|
||||
]
|
||||
|
||||
new App.ControllerForm(
|
||||
@controllerFormBulkTypeVisibility?.releaseController()
|
||||
@controllerFormBulkTypeVisibility = new App.ControllerForm(
|
||||
el: @$('#form-ticket-bulk-typeVisibility')
|
||||
model:
|
||||
configure_attributes: @confirm_attributes
|
||||
@@ -107,6 +108,9 @@ class App.TicketBulkForm extends App.Controller
|
||||
@$('.js-confirm-step').addClass('hide')
|
||||
|
||||
show: =>
|
||||
return if @visible
|
||||
App.TicketOverviewCollection.fetch()
|
||||
|
||||
@el.removeClass('hide')
|
||||
@visible = true
|
||||
@makeSpaceForTableRows()
|
||||
|
||||
@@ -82,7 +82,7 @@ class App extends Spine.Controller
|
||||
displayValue = attributeConfig.options[elem]
|
||||
|
||||
if displayValue && attributeConfig.translate
|
||||
displayValue = App.i18n.translateInline(displayValue)
|
||||
displayValue = App.i18n.translatePlain(displayValue)
|
||||
|
||||
value = displayValue || elem
|
||||
|
||||
@@ -121,7 +121,7 @@ class App extends Spine.Controller
|
||||
resultLocal = item.label
|
||||
|
||||
if attributeConfig.translate
|
||||
resultLocal = App.i18n.translateInline(resultLocal)
|
||||
resultLocal = App.i18n.translatePlain(resultLocal)
|
||||
|
||||
# execute callback on content
|
||||
if attributeConfig.callback
|
||||
|
||||
@@ -35,7 +35,10 @@ class App.i18n
|
||||
_instance ?= new _i18nSingleton()
|
||||
_instance.timestamp(args, offset)
|
||||
|
||||
@translateDate: (args, offset = 0) ->
|
||||
# Since zammad usually stores the date as simple string (like 1994-08-18) without time zone, we need to enable epoch shifting (second translateDate argument),
|
||||
# to act like having a completely time zone independent date.
|
||||
# Otherwise `new Date("1994-08-18")` would parse it like `new Date("1994-08-18 00:00:00 UTC")` and transforms/outputs it using the local time zone.
|
||||
@translateDate: (args, offset = true) ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _i18nSingleton()
|
||||
_instance.date(args, offset)
|
||||
|
||||
@@ -29,6 +29,7 @@ class App.PopoverProvider
|
||||
return if !@checkPermissions()
|
||||
@clear(@popovers)
|
||||
@bind() if !buildParams.doNotBind
|
||||
return if buildParams.isTouchDevice is true
|
||||
@popovers = @buildPopovers()
|
||||
|
||||
checkPermissions: ->
|
||||
@@ -39,12 +40,16 @@ class App.PopoverProvider
|
||||
|
||||
bind: ->
|
||||
|
||||
onHide: (event, elem) ->
|
||||
|
||||
onShow: (event, elem) ->
|
||||
|
||||
buildPopovers: (supplementaryData = {}) ->
|
||||
context = @
|
||||
|
||||
selector = supplementaryData.selector || ".#{@cssClass()}"
|
||||
|
||||
@params.parentController.el.find(selector).popover(
|
||||
@params.parentController.el.find(selector).popover('destroy').popover(
|
||||
trigger: 'hover'
|
||||
container: 'body'
|
||||
html: true
|
||||
@@ -57,6 +62,8 @@ class App.PopoverProvider
|
||||
content: ->
|
||||
context.buildContentFor(@, supplementaryData)
|
||||
)
|
||||
@params.parentController.el.find(selector).on('show.bs.popover', (e) -> context.onShow(e, @))
|
||||
@params.parentController.el.find(selector).on('hide.bs.popover', (e) -> context.onHide(e, @))
|
||||
|
||||
clear: ->
|
||||
return if !@popovers
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
class App.PopoverProviderAjax extends App.PopoverProvider
|
||||
onHide: (event, elem) ->
|
||||
$(event.target).attr('data-content', App.view('generic/page_loading')()).data('bs.popover').setContent()
|
||||
|
||||
onShow: (event, elem) ->
|
||||
if @popoverLoop
|
||||
@popoverLoop = false
|
||||
return
|
||||
|
||||
@fetch(event, elem)
|
||||
|
||||
fetch: (event, elem) ->
|
||||
|
||||
replaceOnShow: (event, content) ->
|
||||
$(event.target).popover('hide')
|
||||
$(event.target).attr('data-content', content).data('bs.popover').setContent()
|
||||
@popoverLoop = true
|
||||
$(event.target).popover('show')
|
||||
|
||||
buildTitleFor: (elem) ->
|
||||
$(elem).find('[title="*"]').val()
|
||||
|
||||
buildContentFor: (elem, supplementaryData) ->
|
||||
App.view('generic/page_loading')()
|
||||
@@ -1,37 +1,39 @@
|
||||
class App.UserTicketPopoverProvider extends App.PopoverProvider
|
||||
class UserTicket extends App.PopoverProviderAjax
|
||||
@templateName = 'user_ticket_list'
|
||||
|
||||
fetch: (buildParams) ->
|
||||
fetch: (event, elem) ->
|
||||
@params.parentController.ajax(
|
||||
type: 'GET'
|
||||
url: "#{App.Config.get('api_path')}/ticket_customer"
|
||||
data:
|
||||
customer_id: buildParams.user_id
|
||||
customer_id: @buildParams.user_id
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
|
||||
ticketsList = { open: data.ticket_ids_open, closed: data.ticket_ids_closed }
|
||||
@callback(ticketsList: ticketsList, selector: buildParams.selector)
|
||||
ticketList = { open: data.ticket_ids_open, closed: data.ticket_ids_closed }
|
||||
html = @buildContentFor(elem, { ticketList: ticketList, selector: @buildParams.selector })
|
||||
|
||||
@replaceOnShow(event, html[0].outerHTML)
|
||||
)
|
||||
|
||||
build: (buildParams) ->
|
||||
return if !@checkPermissions()
|
||||
@fetch(buildParams)
|
||||
|
||||
callback: (supplementaryData) ->
|
||||
@clear(@popovers)
|
||||
@popovers = @buildPopovers(supplementaryData)
|
||||
|
||||
buildTitleFor: (elem) ->
|
||||
$(elem).find('[title="*"]').val()
|
||||
@buildParams = buildParams
|
||||
@popovers = @buildPopovers(ticketList: {}, selector: buildParams.selector)
|
||||
|
||||
buildContentFor: (elem, supplementaryData) ->
|
||||
return super if _.isEmpty(supplementaryData.ticketList)
|
||||
|
||||
type = $(elem).filter('[data-type]').data('type')
|
||||
ticket_ids = supplementaryData.ticketsList[type] || []
|
||||
ticket_ids = supplementaryData.ticketList[type] || []
|
||||
|
||||
tickets = ticket_ids.map (ticketId) -> App.Ticket.fullLocal(ticketId)
|
||||
@buildHtmlContent(
|
||||
ticketList: App.view('generic/ticket_list')(
|
||||
tickets: tickets
|
||||
show_id: true
|
||||
)
|
||||
)
|
||||
|
||||
# insert data
|
||||
@buildHtmlContent(tickets: tickets)
|
||||
|
||||
App.PopoverProvider.registerProvider('UserTicket', UserTicket)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
class UserTicket extends App.PopoverProvider
|
||||
@templateName = 'user_ticket_list'
|
||||
|
||||
fetch: (buildParams) ->
|
||||
@params.parentController.ajax(
|
||||
type: 'GET'
|
||||
url: "#{App.Config.get('api_path')}/ticket_customer"
|
||||
data:
|
||||
customer_id: buildParams.user_id
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
|
||||
ticketsList = { open: data.ticket_ids_open, closed: data.ticket_ids_closed }
|
||||
@callback(ticketsList: ticketsList, selector: buildParams.selector)
|
||||
)
|
||||
|
||||
build: (buildParams) ->
|
||||
return if !@checkPermissions()
|
||||
@fetch(buildParams)
|
||||
|
||||
callback: (supplementaryData) ->
|
||||
@clear(@popovers)
|
||||
@popovers = @buildPopovers(supplementaryData)
|
||||
|
||||
buildTitleFor: (elem) ->
|
||||
$(elem).find('[title="*"]').val()
|
||||
|
||||
buildContentFor: (elem, supplementaryData) ->
|
||||
type = $(elem).filter('[data-type]').data('type')
|
||||
ticket_ids = supplementaryData.ticketsList[type] || []
|
||||
|
||||
tickets = ticket_ids.map (ticketId) -> App.Ticket.fullLocal(ticketId)
|
||||
|
||||
# insert data
|
||||
@buildHtmlContent(
|
||||
ticketList: App.view('generic/ticket_list')(
|
||||
tickets: tickets
|
||||
show_id: true
|
||||
)
|
||||
)
|
||||
App.PopoverProvider.registerProvider('UserTicket', UserTicket)
|
||||
@@ -83,6 +83,7 @@ class App.Utils
|
||||
'color:#b3b3b3' # use in UI, ignore it
|
||||
'color:rgb(34, 34, 34)' # use in UI, ignore it
|
||||
'color:rgb(219, 219, 220)' # use in UI, ignore it
|
||||
'color:rgb(255, 255, 255)' # use in UI, ignore it
|
||||
],
|
||||
'SPAN': [
|
||||
'color:white',
|
||||
@@ -97,6 +98,7 @@ class App.Utils
|
||||
'color:#b3b3b3' # use in UI, ignore it
|
||||
'color:rgb(34, 34, 34)' # use in UI, ignore it
|
||||
'color:rgb(219, 219, 220)' # use in UI, ignore it
|
||||
'color:rgb(255, 255, 255)' # use in UI, ignore it
|
||||
],
|
||||
'TABLE': [
|
||||
'font-size:0',
|
||||
@@ -880,10 +882,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())
|
||||
@@ -905,22 +907,47 @@ class App.Utils
|
||||
else if dataRef isnt undefined && dataRef isnt null && dataRef.toString
|
||||
# in case if we have a references object, check what datatype the attribute has
|
||||
# and e. g. convert timestamps/dates to browser locale
|
||||
attributeName = level
|
||||
objectName = levels[levels.length - 2]
|
||||
className = dataRefLast?.constructor?.className
|
||||
lastLevelName = levels[levels.length - 2]
|
||||
if lastLevelName && !className
|
||||
className = lastLevelName.charAt(0).toUpperCase() + lastLevelName.slice(1)
|
||||
if !className && objectName
|
||||
className = objectName.charAt(0).toUpperCase() + objectName.slice(1)
|
||||
|
||||
if level is 'value'
|
||||
attributeName = levels[levels.length - 2]
|
||||
objectName = levels[levels.length - 3]
|
||||
className = objectName.charAt(0).toUpperCase() + objectName.slice(1)
|
||||
|
||||
if className && App[className]
|
||||
localClassRef = App[className]
|
||||
if localClassRef?.attributesGet
|
||||
attributes = localClassRef.attributesGet()
|
||||
if attributes?[level]
|
||||
if attributes[level]['tag'] is 'datetime'
|
||||
value = App.i18n.translateTimestamp(dataRef)
|
||||
else if attributes[level]['tag'] is 'date'
|
||||
value = App.i18n.translateDate(dataRef)
|
||||
else if attributes[level]['tag'] is 'autocompletion_ajax_external_data_source'
|
||||
value = dataRef.value
|
||||
if attributes?[attributeName]
|
||||
dataType = attributes[attributeName]['tag']
|
||||
|
||||
if level is 'value'
|
||||
switch dataType
|
||||
when 'select'
|
||||
key = dataRefLast[attributeName]
|
||||
value = attributes[attributeName]['historical_options'][key]
|
||||
when 'multiselect'
|
||||
key = dataRefLast[attributeName] || []
|
||||
value = key.map((element) -> attributes[attributeName]['historical_options'][element]).join(', ')
|
||||
when 'autocompletion_ajax_external_data_source'
|
||||
value = dataRefLast.label
|
||||
else
|
||||
value = dataRefLast[attributeName]
|
||||
else
|
||||
switch dataType
|
||||
when 'datetime'
|
||||
value = App.i18n.translateTimestamp(dataRef)
|
||||
when 'date'
|
||||
value = App.i18n.translateDate(dataRef)
|
||||
when 'multiselect'
|
||||
dataRef = dataRef || []
|
||||
value = dataRef.map((element) -> element).join(', ')
|
||||
when 'autocompletion_ajax_external_data_source'
|
||||
value = dataRef.value || '-'
|
||||
|
||||
# as fallback use value of toString()
|
||||
if !value
|
||||
|
||||
@@ -67,18 +67,19 @@ class App.ExternalDataSourceAjaxSelect extends App.SearchableAjaxSelect
|
||||
"#{objectName}+#{attributeName}+#{query}"
|
||||
|
||||
ajaxAttributes: =>
|
||||
objectName = @options.attribute.objectName
|
||||
attributeName = @options.attribute.attributeName or @options.attribute.nameRaw
|
||||
query = @input.val()
|
||||
|
||||
params = if @delegate.params?.id then @delegate.params else App.ControllerForm.params(@delegate.form)
|
||||
|
||||
objectName = @options.attribute.objectName
|
||||
attributeName = @options.attribute.attributeName or @options.attribute.nameRaw
|
||||
query = @input.val()
|
||||
search_context = {}
|
||||
if params.id
|
||||
search_context["#{@delegate.model.className.toLowerCase()}_id"] = params.id
|
||||
|
||||
if params.customer_id
|
||||
search_context.customer_id = params.customer_id
|
||||
if @delegate
|
||||
params = if @delegate.params?.id then @delegate.params else App.ControllerForm.params(@delegate.form)
|
||||
|
||||
if params.id
|
||||
search_context["#{@delegate.model.className.toLowerCase()}_id"] = params.id
|
||||
|
||||
if params.customer_id
|
||||
search_context.customer_id = params.customer_id
|
||||
|
||||
{
|
||||
id: @options.attribute.id
|
||||
|
||||
@@ -381,7 +381,7 @@
|
||||
}, this)
|
||||
|
||||
// resize if to big
|
||||
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert)
|
||||
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, imageFile.type, 'auto', insert)
|
||||
}, this)
|
||||
|
||||
reader.readAsDataURL(imageFile)
|
||||
@@ -450,7 +450,7 @@
|
||||
// look for images
|
||||
if (file.type.match('image.*')) {
|
||||
var reader = new FileReader()
|
||||
reader.onload = (function(e) {
|
||||
reader.onload = $.proxy(function(e) {
|
||||
var result = e.target.result
|
||||
var img = document.createElement('img')
|
||||
img.src = result
|
||||
@@ -462,19 +462,23 @@
|
||||
//}
|
||||
|
||||
//Insert the image at the carat
|
||||
insert = function(dataUrl, width, height, isResized) {
|
||||
insert = $.proxy(function(dataUrl, width, height, isResized) {
|
||||
|
||||
//console.log('dataUrl', dataUrl)
|
||||
//console.log('scaleFactor', scaleFactor, isResized, maxWidth, width, height)
|
||||
this.log('image inserted')
|
||||
result = dataUrl
|
||||
if (this.options.imageWidth == 'absolute') {
|
||||
img = "<img tabindex=\"0\" style=\"width: " + width + "px; max-width: 100%;\" src=\"" + result + "\">"
|
||||
}
|
||||
else {
|
||||
img = "<img tabindex=\"0\" style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
|
||||
|
||||
img.setAttribute('tabindex', '0')
|
||||
img.setAttribute('src', dataUrl)
|
||||
|
||||
if(this.options.imageWidth == 'absolute') {
|
||||
var imageStyle = 'width:' + width + 'px; max-width: 100%;'
|
||||
} else {
|
||||
var imageStyle = 'width: 100%; max-width:' + width + 'px;'
|
||||
}
|
||||
|
||||
img.setAttribute('style', imageStyle)
|
||||
|
||||
if (document.caretPositionFromPoint) {
|
||||
var pos = document.caretPositionFromPoint(x, y)
|
||||
range = document.createRange();
|
||||
@@ -489,11 +493,11 @@
|
||||
else {
|
||||
console.log('could not find carat')
|
||||
}
|
||||
}
|
||||
}, this)
|
||||
|
||||
// resize if to big
|
||||
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert)
|
||||
})
|
||||
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, file.type, 'auto', insert)
|
||||
}, this)
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
@@ -49,9 +49,11 @@ InstanceMethods =
|
||||
popover.build(buildParams)
|
||||
|
||||
renderPopovers: (buildParams = {}) ->
|
||||
# do not render popovers for touch
|
||||
|
||||
# detect touch device
|
||||
# do not show/render popovers for touch
|
||||
if window.matchMedia('(pointer: coarse)').matches
|
||||
return
|
||||
buildParams.isTouchDevice = true
|
||||
|
||||
if !@initializedPopovers
|
||||
@initializePopovers()
|
||||
|
||||
@@ -90,6 +90,11 @@ App.ViewHelpers =
|
||||
time.setFullYear(time.getFullYear() + diff)
|
||||
|
||||
if includeTime
|
||||
# Clear seconds & miliseconds
|
||||
# Because time picker allows to put in hours and minutes only
|
||||
# If time contains seconds, detection of changed input malfunctions
|
||||
time.setSeconds(0)
|
||||
time.setMilliseconds(0)
|
||||
time.toISOString()
|
||||
else
|
||||
time.toISOString().substring(0, 10)
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -124,6 +124,10 @@ class App.Ticket extends App.Model
|
||||
|
||||
# apply macro
|
||||
@macro: (params) ->
|
||||
isTimeTag = (attribute) ->
|
||||
config = _.findWhere(App.Ticket.configure_attributes, { name: attribute })
|
||||
_.includes(['date', 'datetime'], config?.tag)
|
||||
|
||||
for key, content of params.macro
|
||||
attributes = key.split('.')
|
||||
|
||||
@@ -146,7 +150,7 @@ class App.Ticket extends App.Model
|
||||
@tagAdd(params.ticket.id, tag)
|
||||
|
||||
# apply pending date changes
|
||||
else if attributes[1] is 'pending_time' && content.operator is 'relative'
|
||||
else if isTimeTag(attributes[1]) && content.operator is 'relative'
|
||||
params.ticket[attributes[1]] = App.ViewHelpers.relative_time(content.value, content.range)
|
||||
|
||||
# apply user changes
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -320,11 +320,11 @@ class App.User extends App.Model
|
||||
|
||||
@outOfOfficeTextPlaceholder: ->
|
||||
today = new Date()
|
||||
outOfOfficeText = App.i18n.translateContent('Christmas holiday')
|
||||
outOfOfficeText = App.i18n.translatePlain('Christmas holiday')
|
||||
if today.getMonth() < 3
|
||||
outOfOfficeText = App.i18n.translateContent('Easter holiday')
|
||||
outOfOfficeText = App.i18n.translatePlain('Easter holiday')
|
||||
else if today.getMonth() < 9
|
||||
outOfOfficeText = App.i18n.translateContent('Summer holiday')
|
||||
outOfOfficeText = App.i18n.translatePlain('Summer holiday')
|
||||
outOfOfficeText
|
||||
|
||||
outOfOfficeText: ->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<p><%- @T('Example:') %></p>
|
||||
|
||||
<code>
|
||||
curl -H "Authorization: Token token=YOUR_TOKEN" <%= @C('http_type') %>://<%= @C('fqdn') %>/api/v1/groups
|
||||
curl -H "Authorization: Token token=…" <%= @C('http_type') %>://<%= @C('fqdn') %>/api/v1/groups
|
||||
</code>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<% if _.isEmpty(calendar.public_holidays_preview): %>-<% end %>
|
||||
<% for holiday, meta of calendar.public_holidays_preview: %>
|
||||
<tr <% if !meta.active: %>class="is-inactive"<% end %>>
|
||||
<td style="width: 96px"><%- @Tdate(holiday, true) %>
|
||||
<td style="width: 96px"><%- @Tdate(holiday) %>
|
||||
<td><%= meta.summary %>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
@@ -137,14 +137,51 @@
|
||||
|
||||
<p><%- marked(@T('Insert the widget code into the source code of every page the chat shall be visible on. It should be placed at the end of the page\'s source code before the §</body>§ closing tag.')) %></p>
|
||||
|
||||
<h3><%- @T('Requirements') %></h3>
|
||||
<p><%- @T("Zammad Chat requires jQuery. If you don't already use it on your website, you can add it like this:") %></p>
|
||||
<pre><code class="language-html js-code"><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script></code></pre>
|
||||
<div class="manage-chat-jquery-switch-container">
|
||||
<div class="zammad-switch zammad-switch--small">
|
||||
<input name="jquery" type="checkbox" id="chat-jquery" class="js-chat-jquery-widget">
|
||||
<label for="chat-jquery"></label>
|
||||
</div>
|
||||
With jQuery
|
||||
</div>
|
||||
|
||||
<h3><%- @T('Show chat automatically') %> (<%- @T('default') %>)</h3>
|
||||
<p><%- @T('The chat will show up once the connection to the server got established and if there is an operator online to chat with.') %></p>
|
||||
<div class="js-chat-jquery-widget-vanilla">
|
||||
<h3><%- @T('Show chat automatically') %> (<%- @T('default') %>)</h3>
|
||||
<p><%- @T('The chat will show up once the connection to the server got established and if there is an operator online to chat with.') %></p>
|
||||
|
||||
<pre><code class="language-html js-code"><script src="<%= @baseurl %>/assets/chat/chat.min.js"></script>
|
||||
<pre><code class="language-html js-code"><script src="<%= @baseurl %>/assets/chat/chat-no-jquery.min.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
new ZammadChat({
|
||||
<span class="js-modal-params"></span>
|
||||
});
|
||||
})();
|
||||
</script></code></pre>
|
||||
|
||||
<h3><%- @T('Manually open chat') %></h3>
|
||||
<p><%- @T('If you want to open the chat by clicking a button, set the option §show§ to §false§ and add the class §open-zammad-chat§ to the button.') %></p>
|
||||
<pre><code class="language-html js-code"><button class="open-zammad-chat">Chat with us</button>
|
||||
|
||||
<script src="<%= @baseurl %>/assets/chat/chat-no-jquery.min.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
new ZammadChat({
|
||||
<span class="js-modal-params"></span>,
|
||||
show: false
|
||||
});
|
||||
})();
|
||||
</script></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="js-chat-jquery-widget-jquery hide">
|
||||
<h3><%- @T('Requirements') %></h3>
|
||||
<p><%- @T("If you don't already use jQuery on your website, you can add it like this:") %></p>
|
||||
<pre><code class="language-html js-code"><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script></code></pre>
|
||||
|
||||
<h3><%- @T('Show chat automatically') %> (<%- @T('default') %>)</h3>
|
||||
<p><%- @T('The chat will show up once the connection to the server got established and if there is an operator online to chat with.') %></p>
|
||||
|
||||
<pre><code class="language-html js-code"><script src="<%= @baseurl %>/assets/chat/chat.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
new ZammadChat({
|
||||
@@ -166,6 +203,7 @@ $(function() {
|
||||
});
|
||||
});
|
||||
</script></code></pre>
|
||||
</div>
|
||||
|
||||
<h3><%- @T("Why doesn't the chat show up?") %></h3>
|
||||
<p>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<%- @Icon('arrow-right') %>
|
||||
<%- @Icon('arrow-down') %>
|
||||
<% end %>
|
||||
<%- @T(@attribute.display) %>
|
||||
<%- @Ti(@attribute.display) %>
|
||||
<span><% if !@attribute.null: %>*<% end %></span>
|
||||
</label>
|
||||
<% if @attribute.collapsible: %></a><% end %>
|
||||
|
||||
@@ -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: %>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||
<span class="label-text"><%= permission.displayName() %></span>
|
||||
<span class="help-text"><%- @T(permission.note, @T(permission.preferences.translations)) %></span>
|
||||
<span class="help-text"><%- @T(permission.note, @Tp(permission.preferences.translations)) %></span>
|
||||
</label>
|
||||
<% else: %>
|
||||
<div class="checkbox-child js-subPermissionList">
|
||||
@@ -15,7 +15,7 @@
|
||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||
<span class="label-text"><%= permission.displayName().replace(/^.+?\./, '') %></span>
|
||||
<span class="help-text"><%- @T(permission.note, @T(permission.preferences.translations)) %></span>
|
||||
<span class="help-text"><%- @T(permission.note, @Tp(permission.preferences.translations)) %></span>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
name="<%= @attribute.name %>"
|
||||
<%= @attribute.autofocus %>
|
||||
value="<%= @attribute.valueRaw or @attribute.value %>"
|
||||
data-value-type="<%= @attribute.valueType %>"
|
||||
tabindex="-1"
|
||||
<% if @attribute.disabled: %> disabled<% end %>
|
||||
>
|
||||
@@ -52,7 +53,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>
|
||||
|
||||
@@ -12,19 +12,9 @@
|
||||
<%- @T('After installing, updating, or uninstalling packages the following commands need to be executed on the server:') %>
|
||||
<ul>
|
||||
<% if @package_installation: %>
|
||||
<% if @local_gemfiles: %>
|
||||
<li><code>root> zammad config:set BUNDLE_DEPLOYMENT=0</code></li>
|
||||
<li><code>root> zammad run bundle config set --local deployment 'false'</code></li>
|
||||
<li><code>root> zammad run bundle install</code></li>
|
||||
<% end %>
|
||||
<li><code>root> zammad run rake zammad:package:migrate</code></strong></li>
|
||||
<li><code>root> zammad run rake assets:precompile</code></strong></li>
|
||||
<% else: %>
|
||||
<% if @local_gemfiles: %>
|
||||
<li><code>zammad> bundle install</code></li>
|
||||
<% end %>
|
||||
<li><code>zammad> rake zammad:package:migrate</code></strong></li>
|
||||
<li><code>zammad> rake assets:precompile</code></strong></li>
|
||||
<li><code>root> zammad run rake zammad:package:post_install</code></li>
|
||||
<% else: %>
|
||||
<li><code>zammad> rake zammad:package:post_install</code></li>
|
||||
<% end %>
|
||||
<li><code>root> systemctl restart zammad</code></li>
|
||||
</ul>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user