Compare commits

...

55 Commits

Author SHA1 Message Date
Florian Liebe
27f4405b9a Release Zammad 6.3.1. 🚀 2024-05-15 08:20:30 +02:00
Martin Gruner
0cfe32ca87 Maintenance: Apply security update for nokogiri gem. 2024-05-15 07:39:13 +02:00
Martin Gruner
67d518eab1 Maintenance: Apply Ruby 3.2.4 security update. 2024-05-14 08:52:22 +01:00
Rolf Schmidt
31b02d8770 Fixes #5168 - Ticket history shows html when updating articles. 2024-05-14 09:38:08 +02:00
Martin Gruner
483d36ec78 Maintenance: Add translated file. 2024-05-14 06:54:34 +02:00
Weblate
6e14152f45 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Svyrydov Semen <pincher2006@gmail.com>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/uk/
Translation: Zammad/Zammad (stable version)
2024-05-09 09:02:07 +02:00
jr-timme
6baf144b23 Fixes: #5142, closes: #5143 - contrib/packager.io/preinstall.sh does not check the db adapter setting and fails if both psql and mysql are installed
Make sure the correct database is used depending on the adapter setting


(cherry picked from commit 506c295c1d)
2024-05-08 13:29:02 +02:00
Weblate
4cd46cfee4 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Mantas Masalskis <mantas@idev.lt>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/lt/
Translation: Zammad/Zammad (stable version)
2024-05-08 06:57:09 +02:00
Mantas Masalskis
90f23fd150 Fixes #5145 - Public Knowledge Base Bread crumbs unexpectly translated 2024-05-06 16:16:32 +02:00
Weblate
8a769b4f67 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Leroy Aronds <leroy@nimbizz.com>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/nl/
Translation: Zammad/Zammad (stable version)
2024-05-06 14:17:59 +02:00
Mantas Masalskis
8fddcb562a Maintenance: Update IP address in blocked-by-country test 2024-05-06 09:18:56 +02:00
Mantas Masalskis
50ac9d1425 Fixes #5000 - Ticket templates creation interface is broken if a Ticket... 2024-05-03 18:06:09 +02:00
Martin Gruner
86c0f001a8 Maintenance: Generated translated files. 2024-05-03 10:31:25 +02:00
Weblate
fbaa76d386 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Benjamin Scharf <bs@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: YosefAdPro <yosef@adpronet.com>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/es/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/he_IL/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sr_Cyrl/
Translation: Zammad/Zammad (stable version)
2024-05-03 10:19:16 +02:00
Martin Gruner
260aeb1276 Fixes: #5105 - IllegalArgumentException on reports with time range in report profile and time selection in the report UI 2024-05-02 11:54:51 +02:00
Dusan Vuckovic
597e6a535b Fixes #5156 - User login flow is interrupted when two-factor authentication method security keys is used. 2024-05-01 21:02:24 +02:00
Martin Edenhofer
a51103363d Fixes #5117 - Caller is not identified if the caller has only entered a telephone number (no firstname, lastname, email). 2024-04-30 10:58:00 +02:00
Martin Gruner
fc028a09fc Maintenance: Verify gem permissions. 2024-04-29 09:26:39 +02:00
Martin Gruner
c71d937418 Maintenance: Update simpleidn gem. 2024-04-26 13:48:06 +02:00
Martin Gruner
1d75238ede Maintenance: Updated translated files. 2024-04-26 07:46:13 +02:00
Weblate
ec72163f80 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: M0ustach3 <pablobondialuttiau@gmail.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Misha <misha@association-enoria.org>
Co-authored-by: Müslüm Cengiz <muslumcengiz@windowslive.com>
Co-authored-by: Rowdy van der Veen <rowdy@rowdyengeesje.nl>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/nl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/tr/
Translation: Zammad/Zammad (stable version)
2024-04-26 07:44:20 +02:00
Mantas Masalskis
bc703d4d5a Fixes #5115 - ".value" in text modules for external data sources breaks ticket zoom 2024-04-25 14:50:31 +02:00
Rolf Schmidt
8300e2b147 Maintenance: Add preserve symlinks to support package linking in development environments.
Co-authored-by: Dominik Klein <dk@zammad.com>
2024-04-25 09:42:59 +02:00
Rolf Schmidt
fe9ddad5e4 Maintenance: Split up article view and item into smaller files and make them patchable.
Co-authored-by: Florian Liebe <fl@zammad.com>
2024-04-24 15:11:10 +02:00
Dusan Vuckovic
d1107375ed Fixes #5144 - Wrong background color for ticket creation links in light mode. 2024-04-23 16:20:57 +02:00
Florian Liebe
c5266fae1b Fixes #5101 - Fetching LdapSource for unknown users fails. 2024-04-23 09:38:45 +02:00
Florian Liebe
979314a718 Fixes #5139 - Unable to send WhatsApp automatic reminders after 23 hours.
Co-authored-by: Tobias Schäfer <ts@zammad.com>
2024-04-22 12:55:38 +02:00
Rolf Schmidt
46bfcf1201 Maintenance: Improve default selection of download backend when switching metrics. 2024-04-19 13:26:38 +01:00
Rolf Schmidt
d6d4e789f1 Follow up f4300935f4 - Fixes #5138 - Add missing deep dup for comparisons.
(cherry picked from commit 6fa0ae2aaf)

ebf079c6 Follow up f4300935f4 - Fixes #5138 - Add missing...
68b8906b use release to be more stable
2024-04-19 13:26:17 +01:00
Rolf Schmidt
f4557ca3c3 Fixes #5138 - Reporting: Worse performance because UI always request all aggregation data instead of the selection.
Co-authored-by: Florian Liebe <fl@zammad.com>


(cherry picked from commit f4300935f4)

aa35f99c Fixes #5138 - Reporting: Worse performance because UI always request all...
2024-04-19 13:26:17 +01:00
Rolf Schmidt
bf4df2a123 Fixes #5137 - Reporting: No button and output when no tickets found in a time range.
Co-authored-by: Florian Liebe <fl@zammad.com>
2024-04-19 13:46:26 +02:00
Weblate
7663724cb1 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Aivars Miška <allefm@gmail.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Francisco Rocha <francisco@rocha.com.pt>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Rowdy van der Veen <rowdy@rowdyengeesje.nl>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: YosefAdPro <yosef@adpronet.com>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/he_IL/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/lv/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/nl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/pt_PT/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sr_Cyrl/
Translation: Zammad/Zammad (stable version)
2024-04-19 03:05:10 +02:00
Mantas Masalskis
5739774139 Fixes #5123 - Once applied kb editor permissions cannot be revoked 2024-04-18 17:36:36 +02:00
Florian Liebe
30572f2e5d Follow up ce4abf8 - Fixes #5076 - Duplicates in calendar. 2024-04-18 10:04:13 +02:00
Florian Liebe
1e2f95d33f Release Zammad 6.3.0. 🚀 2024-04-17 07:34:17 +02:00
Weblate
f2800594bc Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: IT SMBDA <it@smb.gov.az>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/az/
Translation: Zammad/Zammad (stable version)
2024-04-16 11:28:56 +02:00
Martin Gruner
8a0d1ba69d Fixes: #5095 - Translation in French - bad escaping of quote characters on some strings. 2024-04-16 11:14:18 +02:00
Dominik Klein
56fe514ade Fixes #5122 - Improve 24 hours WhatsApp banner to update it's remaining time. 2024-04-15 17:43:25 +02:00
Weblate
61523ae0e6 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: IT SMBDA <it@smb.gov.az>
Co-authored-by: Martin Gruner <mg@zammad.com>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/az/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/de/
Translation: Zammad/Zammad (stable version)
2024-04-15 13:57:52 +02:00
Weblate
6155304366 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Anael Mobilia <contrib@anael.eu>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fr/
Translation: Zammad/Zammad (stable version)
2024-04-12 07:27:09 +02:00
Mantas Masalskis
a7f5c4ea2c Fixes #5118 - Article notes text is greyed out 2024-04-11 15:43:55 +02:00
Weblate
6849ecc717 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Benjamin Scharf <bs@zammad.com>
Co-authored-by: IT SMBDA <it@smb.gov.az>
Co-authored-by: Mantas Masalskis <mantas@idev.lt>
Co-authored-by: Rowdy van der Veen <rowdy@rowdyengeesje.nl>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/az/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/es/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/lt/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/nl/
Translation: Zammad/Zammad (stable version)
2024-04-10 15:23:15 +02:00
Dominik Klein
250a9abdb9 Fixes #5114 - OTRS import: When importing queues with a parent queue to Zammad... 2024-04-10 07:56:37 +02:00
Martin Gruner
8fda74cdb3 Maintenance: Updated translated files. 2024-04-08 10:39:49 +02:00
Weblate
9a37029906 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Benjamin Scharf <bs@zammad.com>
Co-authored-by: IT SMBDA <it@smb.gov.az>
Co-authored-by: MBekspert <michal.bosak@ekspert.biz>
Co-authored-by: Mantas Masalskis <mantas@idev.lt>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: chrand818 <can@telenabler.com>
Co-authored-by: gfced <gianluca.fadda@comune.oristano.it>
Co-authored-by: lukenpi <lucapasquini89@gmail.com>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/az/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/es/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/it/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/lt/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/pl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sv/
Translation: Zammad/Zammad (stable version)
2024-04-08 10:38:31 +02:00
Martin Gruner
89d64b2ef4 Maintenance: Extend desktop view login tests.
(cherry picked from commit 106f9d9dbc)

2e0cb018 Maintenance: Extend desktop view login tests.
23845527 Drop tests that are not specified in the scenario.
8d8ea2b3 Added stub for omniauth test.
a3edc91f Add first version of SAML test, pending refactoring.
ee7aa834 Added SAML to login test, refactored.
7c2a6215 Silence gitleaks.
ba5cec7f Also test for logout.
6850d48b Verify SAML login and logout.
f4d6c25c Verify SAML logout, wip
c04c8a6a Improved waiting, wip
0ab1e656 Apply workarounds for broken SAML redirects in CI.
fa4ab636 Refactor logout handling.
1ea7b307 Improve check for mobile SAML spec
922ba3a4 Revert "Improve check for mobile SAML spec"
4951543c Use :stable tag for zammad-auth docker image as for all others.
ae641213 Switch saml_spec.rb to be compatible to keycloak 24.
53231e18 Revert unneeded changes to common_actions.rb.
727e4d89 Improve checking for logged-in state.
2024-04-05 13:25:47 +02:00
Mantas Masalskis
97a9499a00 Fixes #5111 - Email Channel cannot update XOauth2 token if identical channel exists 2024-04-05 12:56:38 +02:00
Martin Gruner
7a054c275d Maintenance: Update translated files. 2024-04-03 09:43:09 +02:00
Weblate
aa63f44ceb Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Ambroise Arbey <ambroise.arbey@gmail.com>
Co-authored-by: Decio E. Silva <decio@verinatec.com.br>
Co-authored-by: DjMagicFingers <amir@srvit.co.il>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Emre ÇAPAN <emrecapanogluu@gmail.com>
Co-authored-by: IT SMBDA <it@smb.gov.az>
Co-authored-by: Ivan Perovic <ip@zammad.com>
Co-authored-by: MBekspert <michal.bosak@ekspert.biz>
Co-authored-by: Mantas Masalskis <mantas@idev.lt>
Co-authored-by: Ralf Schmid <rsc@zammad.com>
Co-authored-by: SamolJacek <samol.jacek@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: YosefAdPro <yosef@adpronet.com>
Co-authored-by: chrand818 <can@telenabler.com>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/az/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/he_IL/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/hr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/lt/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/pl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/pt_BR/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sr_Cyrl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sv/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/tr/
Translation: Zammad/Zammad (stable version)
2024-04-03 09:40:18 +02:00
Dominik Klein
6d42717cd2 Fixes #5096 - #{user.avatar}-size ignored in mobile-view. 2024-04-02 10:45:29 +02:00
Dusan Vuckovic
9dbb3a96b8 Follow-up c7afe512ad - Fixes #5089 - Improve localization of triggers and schedulers. 2024-03-26 12:38:47 +01:00
Rolf Schmidt
46e1a1956a Maintenance: Improve calendar subscription backends. 2024-03-26 10:40:13 +01:00
Mantas
e13a4105b7 Follow up 58ec7e9 - Fixes: #5022 - Zammad should not write to the var/ folder
(cherry picked from commit b1d1f481ed)

b1d1f481 Follow up 58ec7e9 - Fixes: #5022 - Zammad should not write to the var/ folder
2024-03-26 08:48:31 +01:00
Dominik Klein
7f76afc0d4 Follow-up cffdc2c6 - Fixes #4875 - Translation Management Improvements. 2024-03-26 07:43:37 +01:00
Martin Gruner
f5d7b4a0e1 Maintenance: update Security.md. 2024-03-25 15:18:29 +01:00
189 changed files with 8318 additions and 25405 deletions

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@
- !reference [.scripts, zammad_db_init]
# Then, switch to the current commit, migrate to it and run a few selected tests.
- git checkout $CI_COMMIT_SHA
- rvm use 3.2.3
- rvm use 3.2.4
- !reference [.scripts, bundle_install]
# Force redis usage, even if it was disabled by the initial configure_environment script of stable.
- export REDIS_URL=redis://redis

View File

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

View File

@@ -1 +1 @@
3.2.3
3.2.4

View File

@@ -1,9 +1,27 @@
# Change Log
## [6.3.0](https://github.com/zammad/zammad/tree/6.3.0) (2024-xx-xx)
## [6.3.1](https://github.com/zammad/zammad/tree/6.3.1) (2024-05-15)
[Full Changelog](https://github.com/zammad/zammad/compare/6.2.0...6.3.0)
[Full Changelog](https://github.com/zammad/zammad/compare/6.3.0...6.3.1)
**Implemented enhancements:**
- User login flow is interrupted when two-factor authentication method security keys is used [5156](https://github.com/zammad/zammad/issues/5156) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[authentication](https://github.com/zammad/zammad/labels/authentication)]
**Fixed bugs:**
- Ticket history shows html when updating articles [5168](https://github.com/zammad/zammad/issues/5168) [[bug](https://github.com/zammad/zammad/labels/bug)]
- contrib/packager.io/preinstall.sh does not check the db adapter setting and fails if both psql and mysql are installed [5142](https://github.com/zammad/zammad/issues/5142) [[needs verification](https://github.com/zammad/zammad/labels/needs%20verification)]
- fixes #5142| fix db check in preinstall.sh [5143](https://github.com/zammad/zammad/pull/5143)
- Public Knowledge Base Bread crumbs unexpectly translated [5145](https://github.com/zammad/zammad/issues/5145) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
- Ticket templates creation interface is broken if a Ticket attribute has a lengthy name. [5000](https://github.com/zammad/zammad/issues/5000) [[bug](https://github.com/zammad/zammad/labels/bug)]
- IllegalArgumentException on reports with time range in report profile and time selection in the report UI [5105](https://github.com/zammad/zammad/issues/5105) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
- [CTI] Caller is not identified if the caller has only entered a telephone number (no firstname, lastname, email). [5117](https://github.com/zammad/zammad/issues/5117) [[bug](https://github.com/zammad/zammad/labels/bug)]
- ".value" in text modules for external data sources breaks ticket zoom [5115](https://github.com/zammad/zammad/issues/5115) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[variable processing](https://github.com/zammad/zammad/labels/variable%20processing)]
- Wrong background color for ticket creation links in light mode [5144](https://github.com/zammad/zammad/issues/5144) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
- Fetching LdapSource for unknown users fails [5101](https://github.com/zammad/zammad/issues/5101) [[bug](https://github.com/zammad/zammad/labels/bug)] [[LDAP](https://github.com/zammad/zammad/labels/LDAP)]
- Unable to send WhatsApp automatic reminders after 23 hours [5139](https://github.com/zammad/zammad/issues/5139) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Reporting: Worse performance because UI always request all aggregation data instead of the selection [5138](https://github.com/zammad/zammad/issues/5138) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)] [[performance](https://github.com/zammad/zammad/labels/performance)]
- Reporting: No button and output when no tickets found in a time range [5137](https://github.com/zammad/zammad/issues/5137) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
- Once applied kb editor permissions cannot be revoked [5123](https://github.com/zammad/zammad/issues/5123) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
- Duplicates in calendar [5076](https://github.com/zammad/zammad/issues/5076) [[bug](https://github.com/zammad/zammad/labels/bug)] [[calendar](https://github.com/zammad/zammad/labels/calendar)]

View File

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

View File

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

View File

@@ -334,7 +334,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2024.0305)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
mini_portile2 (2.8.6)
minitest (5.22.3)
minitest-profile (0.0.2)
msgpack (1.7.2)
@@ -361,7 +361,7 @@ GEM
net-protocol
nio4r (2.7.0)
nkf (0.2.0)
nokogiri (1.16.3)
nokogiri (1.16.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nori (2.7.0)
@@ -601,7 +601,7 @@ GEM
activesupport (>= 5.2.0)
simple_oauth (0.3.1)
simple_po_parser (1.1.6)
simpleidn (0.2.1)
simpleidn (0.2.2)
unf (~> 0.1.4)
slack-notifier (2.4.0)
slack-ruby-client (2.3.0)
@@ -846,7 +846,7 @@ DEPENDENCIES
zendesk_api
RUBY VERSION
ruby 3.2.3p157
ruby 3.2.4p170
BUNDLED WITH
2.4.1

View File

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

View File

@@ -1 +1 @@
6.3.x
6.3.1

View File

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

View File

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

View File

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

View File

@@ -616,6 +616,11 @@ class App.TicketCreate extends App.Controller
# to replace in text modules properly
params.customer = App.User.find(params.customer_id) || {}
# show selected user display name in customer attribute in UI
if _.isEmpty(@el.find('input[name=customer_id_completion]').val())
if params.customer && params.customer.displayName
@el.find('input[name=customer_id_completion]').val(params.customer.displayName())
@sidebarWidget.render(params)
@textModule.reload(
config: App.Config.all()

View File

@@ -0,0 +1,477 @@
class App.ArticleViewItem extends App.ControllerObserver
model: 'TicketArticle'
observe:
from: true
to: true
cc: true
subject: true
body: true
internal: true
preferences: true
elements:
'.textBubble-content': 'textBubbleContent'
'.textBubble-content img': 'textBubbleImages'
'.textBubble-overflowContainer': 'textBubbleOverflowContainer'
events:
'click .article-meta-permanent': 'toggleMetaWithDelay'
'click .textBubble': 'toggleMetaWithDelay'
'click .textBubble a': 'stopPropagation'
'click .js-toggleFold': 'toggleFold'
'click .richtext-content img': 'imageView'
'click .attachments img': 'imageView'
'click .file-calendar .js-preview': 'calendarView'
'click .js-securityRetryProcess': 'retrySecurityProcess'
'click .js-retryWhatsAppAttachmentDownload': 'retryWhatsAppAttachmentDownload'
constructor: ->
super
@seeMoreOpen = false
# set expand of text area only once
@controllerBind('ui::ticket::shown', (data) =>
return if data.ticket_id.toString() isnt @ticket.id.toString()
# set highlighter
@setHighlighter()
# set see more
@setSeeMore()
)
setHighlighter: =>
return if @el.is(':hidden')
# use delay do no ui blocking
#@highlighter.loadHighlights(@object_id)
d = =>
if @highlighter
@highlighter.loadHighlights(@object_id)
@delay(d, 200)
render: (article) =>
# set @el attributes
@el.addClass("ticket-article-item #{article.sender.name.toLowerCase()}")
@el.attr('data-id', article.id)
@el.attr('id', "article-#{article.id}")
if article.internal
@el.addClass('is-internal')
else
@el.removeClass('is-internal')
# check if email link needs to be updated
links = clone(article.preferences.links) || []
if article.type.name is 'email'
link =
name: __('Raw')
url: "#{@Config.get('api_path')}/ticket_article_plain/#{article.id}"
target: '_blank'
links.push link
# attachments prepare
attachments = App.TicketArticle.contentAttachments(article)
if article.attachments
for attachment in article.attachments
dispositionParams = ''
if attachment?.preferences['Content-Type'] isnt 'text/html'
dispositionParams = '?disposition=attachment'
attachment.url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}#{dispositionParams}"
attachment.preview_url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?view=preview"
if attachment && attachment.preferences && attachment.preferences['original-format'] is true
link =
url: "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?disposition=attachment"
name: __('Original Formatting')
target: '_blank'
links.push link
# prepare html body
if article.content_type is 'text/html'
body = article.body
if article.preferences && article.preferences.signature_detection
signatureDetected = '<span class="js-signatureMarker"></span>'
body = body.replace(signatureDetected, '')
body = body.split('<br>')
body.splice(article.preferences.signature_detection, 0, signatureDetected)
body = body.join('<br>')
else
body = App.Utils.signatureIdentifyByHtml(body)
article['html'] = body
else
# client signature detection
bodyHtml = App.Utils.text2html(article.body)
article['html'] = App.Utils.signatureIdentifyByPlaintext(bodyHtml)
# if no signature detected or within first 25 lines, check if signature got detected in backend
if article['html'] is bodyHtml || (article.preferences && article.preferences.signature_detection < 25)
signatureDetected = false
body = article.body
if article.preferences && article.preferences.signature_detection
signatureDetected = '########SIGNATURE########'
# coffeelint: disable=no_unnecessary_double_quotes
body = body.split("\n")
body.splice(article.preferences.signature_detection, 0, signatureDetected)
body = body.join("\n")
# coffeelint: enable=no_unnecessary_double_quotes
if signatureDetected
body = App.Utils.textCleanup(body)
article['html'] = App.Utils.text2html(body)
article['html'] = article['html'].replace(signatureDetected, '<span class="js-signatureMarker"></span>')
if article.preferences.delivery_message
@html App.view('ticket_zoom/article_view_delivery_failed')(
ticket: @ticket
article: article
attachments: attachments
links: links
)
return
if article.sender.name is 'System' && article.type.name isnt 'note'
#if article.sender.name is 'System' && article.preferences.perform_origin is 'trigger'
@html App.view('ticket_zoom/article_view_system')(
ticket: @ticket
article: article
attachments: attachments
links: links
)
return
if article.preferences?.whatsapp
icon = null
msg = null
if article.preferences?.whatsapp?.timestamp_read
icon = 'double-checkmark'
msg = __('read by the customer')
else if article.preferences?.whatsapp?.timestamp_delivered
icon = 'double-checkmark-outline'
msg = __('delivered to the customer')
else if article.preferences?.whatsapp?.timestamp_sent
icon = 'checkmark-outline'
msg = __('sent to the customer')
article['delivery_status_icon'] = icon
article['delivery_status_message'] = msg
@html App.view('ticket_zoom/article_view')(
ticket: @ticket
article: article
attachments: App.view('generic/attachments')(attachments: attachments, has_body: !!article.html)
links: links
)
new App.WidgetAvatar(
el: @$('.js-avatar')
object_id: article.origin_by_id || article.created_by_id
size: 40
)
@articleActions = new App.TicketZoomArticleActions(
el: @$('.js-article-actions')
ticket: @ticket
article: article
lastAttributes: @lastAttributes
form_id: @form_id
)
# set see more
@shown = false
a = =>
@setSeeMore()
@delay(a, 50)
# set highlighter
@setHighlighter()
# set see more options
setSeeMore: =>
return if @el.is(':hidden')
return if @shown
@shown = true
@textBubbleImages.each (i, el) =>
if !el.complete
$(el).one 'load', @measureSeeMore
@measureSeeMore()
measureSeeMore: =>
maxHeight = 560
minHeight = 90
bubbleContent = @textBubbleContent
bubbleOverflowContainer = @textBubbleOverflowContainer
# expand if see more is already clicked
if @seeMoreOpen
bubbleContent.css('height', 'auto')
else
# reset bubble height and "see more" opacity
bubbleContent.css('height', '')
bubbleOverflowContainer.css('opacity', '')
# remember offset of "see more"
signatureMarker = bubbleContent.find('.js-signatureMarker')
if !signatureMarker.get(0)
signatureMarker = bubbleContent.find('div [data-signature=true]')
offsetTop = signatureMarker.position()
# safari - workaround
# in safari sometimes the marker is directly on top via .top and inspector but it isn't
# in this case use the next element
if offsetTop && offsetTop.top is 0
offsetTop = signatureMarker.next('div, p, br').position()
# remember bubble content height
bubbleContentHeight = bubbleContent.height()
# get marker height
if offsetTop
markerHeight = offsetTop.top
# if signature marker exists and height is within maxHeight
if markerHeight && markerHeight < maxHeight
newHeight = markerHeight + 30
if newHeight < minHeight
newHeight = minHeight
bubbleContent.attr('data-height', bubbleContentHeight + 30)
bubbleContent.attr('data-height-origin', newHeight)
bubbleContent.css('height', "#{newHeight}px")
bubbleOverflowContainer.removeClass('hide')
# if height is higher then maxHeight
else if bubbleContentHeight > maxHeight
bubbleContent.attr('data-height', bubbleContentHeight + 30)
bubbleContent.attr('data-height-origin', maxHeight)
newHeight = if @seeMoreOpen then 'auto' else "#{maxHeight}px"
bubbleContent.css('height', newHeight)
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(@label)
bubbleOverflowContainer.removeClass('hide')
else
bubbleOverflowContainer.addClass('hide')
retrySecurityProcess: (e) ->
e.preventDefault()
e.stopPropagation()
article_id = $(e.target).closest('.ticket-article-item').data('id')
article = App.TicketArticle.find(article_id)
@ajax(
id: 'retrySecurityProcess'
type: 'POST'
url: "#{@apiPath}/ticket_articles/#{article_id}/retry_security_process"
processData: true
success: (encryption_data, status, xhr) =>
for data in encryption_data
continue if article.preferences.security.type isnt data.type
if data.sign.success
@notify
type: 'success'
msg: App.i18n.translateContent('The signature was successfully verified.')
else if data.sign.comment
comment = App.i18n.translateContent('Signature verification failed!') + ' ' + App.i18n.translateContent(data.sign.comment || '', data.sign.commentPlaceholders)
@notify
type: 'error'
msg: comment
timeout: 2000
if data.encryption.success
@notify
type: 'success'
msg: App.i18n.translateContent('Decryption was successful.')
else if data.encryption.comment
comment = App.i18n.translateContent('Decryption failed!') + ' ' + App.i18n.translateContent(data.encryption.comment || '', data.encryption.commentPlaceholders)
@notify
type: 'error'
msg: comment
timeout: 2000
error: (xhr) =>
@notify
type: 'error'
msg: App.i18n.translateContent('The retried security process failed!')
)
retryWhatsAppAttachmentDownload: (e) ->
e.preventDefault()
e.stopPropagation()
article_id = $(e.target).closest('.ticket-article-item').data('id')
@ajax(
id: 'retryWhatsAppAttachmentDownload'
type: 'POST'
url: "#{@apiPath}/ticket_articles/#{article_id}/retry_whatsapp_attachment_download"
processData: true
success: (data, status, xhr) =>
@notify
type: 'success'
msg: App.i18n.translateContent('Downloading attachments…')
error: (data, status, xhr) =>
details = data.responseJSON || {}
@notify
type: 'error'
msg: App.i18n.translateContent(details.error)
)
stopPropagation: (e) ->
e.stopPropagation()
toggleMetaWithDelay: (e) =>
# allow double click select
# by adding a delay to the toggle
delay = 300
article = $(e.target).closest('.ticket-article-item')
if @elementContainsSelection(article.get(0))
@stopPropagation(e)
return false
if @lastClick and +new Date - @lastClick < delay
clearTimeout(@toggleMetaTimeout)
else
@toggleMetaTimeout = setTimeout(@toggleMeta, delay, e)
@lastClick = +new Date
toggleMeta: (e) =>
e.preventDefault()
animSpeed = 300
article = $(e.target).closest('.ticket-article-item')
metaTopClip = article.find('.article-meta-clip.top')
metaBottomClip = article.find('.article-meta-clip.bottom')
metaTop = article.find('.article-content-meta.top')
metaBottom = article.find('.article-content-meta.bottom')
if @elementContainsSelection(article.get(0))
@stopPropagation(e)
return false
if !metaTop.hasClass('hide')
article.removeClass('state--folde-out')
# scroll back up
article.velocity 'scroll',
container: article.scrollParent()
offset: -article.offset().top - metaTop.outerHeight()
duration: animSpeed
easing: 'easeOutQuad'
metaTop.velocity
properties:
translateY: 0
opacity: [ 0, 1 ]
options:
speed: animSpeed
easing: 'easeOutQuad'
complete: -> metaTop.addClass('hide')
metaBottom.velocity
properties:
translateY: [ -metaBottom.outerHeight(), 0 ]
opacity: [ 0, 1 ]
options:
speed: animSpeed
easing: 'easeOutQuad'
complete: -> metaBottom.addClass('hide')
metaTopClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
metaBottomClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
else
article.addClass('state--folde-out')
metaBottom.removeClass('hide')
metaTop.removeClass('hide')
# balance out the top meta height by scrolling down
article.velocity('scroll',
container: article.scrollParent()
offset: -article.offset().top + metaTop.outerHeight()
duration: animSpeed
easing: 'easeOutQuad'
)
metaTop.velocity
properties:
translateY: [ 0, metaTop.outerHeight() ]
opacity: [ 1, 0 ]
options:
speed: animSpeed
easing: 'easeOutQuad'
metaBottom.velocity
properties:
translateY: [ 0, -metaBottom.outerHeight() ]
opacity: [ 1, 0 ]
options:
speed: animSpeed
easing: 'easeOutQuad'
metaTopClip.velocity({ height: metaTop.outerHeight() }, animSpeed, 'easeOutQuad')
metaBottomClip.velocity({ height: metaBottom.outerHeight() }, animSpeed, 'easeOutQuad')
toggleFold: (e) ->
e.preventDefault()
e.stopPropagation()
bubbleContent = @textBubbleContent
bubbleOverflowContainer = @textBubbleOverflowContainer
if @seeMoreOpen
@label = App.i18n.translateContent('See more')
height = bubbleContent.attr('data-height-origin')
@seeMoreOpen = false
else
@label = App.i18n.translateContent('See less')
height = bubbleContent.attr('data-height')
@seeMoreOpen = true
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(@label)
bubbleContent.velocity
properties:
height: height
options:
duration: 300
isOrContains: (node, container) ->
while node
if node is container
return true
node = node.parentNode
false
elementContainsSelection: (el) ->
sel = window.getSelection()
if sel.rangeCount > 0 && sel.toString()
for i in [0..sel.rangeCount-1]
if !@isOrContains(sel.getRangeAt(i).commonAncestorContainer, el)
return false
return true
false
remove: =>
@el.remove()
imageView: (e) ->
# take care of images surrounded by a link
if e.target && e.target.parentNode && e.target.parentNode.nodeName.toLowerCase() == 'a'
return false
e.preventDefault()
e.stopPropagation()
new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML, parentElement: $(e.currentTarget))
calendarView: (e) ->
e.preventDefault()
e.stopPropagation()
parentElement = $(e.target).closest('.attachment.file-calendar')
new App.TicketZoomArticleCalendarView(calendar: parentElement.get(0).outerHTML)
updateFormId: (newFormId) ->
@articleActions?.form_id = newFormId

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -880,10 +880,10 @@ class App.Utils
dataRef = objects
dataRefLast = undefined
for level in levels
if typeof dataRef is 'object' && level of dataRef
if _.isObject(dataRef) && dataRef && level of dataRef
dataRefLast = dataRef
dataRef = dataRef[level]
else if typeof dataRef is 'object' && typeof level is 'string' && matches = level.match(/(?<functionName>\w+)\((?<params>.*?)\)/)
else if _.isObject(dataRef) && typeof level is 'string' && matches = level.match(/(?<functionName>\w+)\((?<params>.*?)\)/)
dataRefLast = dataRef
{ functionName, params } = matches.groups
parameters = params.split(',').map((param) -> param.trim())

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,7 @@
<span class="searchableSelect-autocomplete-visible js-autocomplete-visible"></span>
</div>
<% if @attribute.ajax and not @attribute.multiple and not @attribute.disabled: %>
<div role="button" title="<%= @T('Clear Selection') %>" tabindex="0" class="dropdown-clear <% if @attribute.showArrowIcon: %>dropdown-clear--with-arrow-icon<% end %> js-clear <% if not @attribute.value or _.isEmpty(@attribute.value): %>hide<% end %>">
<div role="button" title="<%- @T('Clear Selection') %>" tabindex="0" class="dropdown-clear <% if @attribute.showArrowIcon: %>dropdown-clear--with-arrow-icon<% end %> js-clear <% if not @attribute.value or _.isEmpty(@attribute.value): %>hide<% end %>">
<%- @Icon('diagonal-cross') %>
</div>
<% end %>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
<div class="sidebar-git-issue-content">
<div class="sidebar-block">
<a href="<%- issue.url %>" target="_blank">
<span class="icon-holder" title="<%= @T(issue.icon_state) %>">
<span class="icon-holder" title="<%- @T(issue.icon_state) %>">
<%- @Icon('task-state', issue.icon_state) %>
</span>
#<%= issue.id %> <%= issue.title %>
@@ -16,7 +16,7 @@
<% if issue.milestone: %>
<div class="sidebar-block">
<label><%- @T('Milestone') %></label>
<%= @T(issue.milestone) %>
<%- @T(issue.milestone) %>
</div>
<% end %>
<% if issue.assignees.length > 0: %>

View File

@@ -12,7 +12,7 @@
</div>
<% for entry in @list: %>
<div class="accounted-time-value-row">
<div class="accounted-time-value-item" title="<%= @T(entry[0]) %>"><%= @T(entry[0]) %></div>
<div class="accounted-time-value-item" title="<%- @T(entry[0]) %>"><%- @T(entry[0]) %></div>
<div class="accounted-time-value-item"><%= parseFloat(entry[1]).toFixed(1) %></div>
<% if @displayUnit: %>
<div class="accounted-time-value-item">

View File

@@ -9,7 +9,7 @@
<div class="btn-group btn-group--full" role="group" aria-label="">
<% for month, i in @timeRangeMonth: %>
<div class="btn btn--text btn--large js-timePickerMonth<%- ' is-selected' if @month is i+1 %>" data-type="<%= i+1 %>"><%= @T(month) %></div>
<div class="btn btn--text btn--large js-timePickerMonth<%- ' is-selected' if @month is i+1 %>" data-type="<%= i+1 %>"><%- @T(month) %></div>
<% end %>
</div>
</div>

View File

@@ -20,7 +20,7 @@
<select id="timeAccountingUnit" class="form-control">
<% if @timeUnits: %>
<% for row in @timeUnits: %>
<option value="<%= row.value %>" <% if row.value is @timeAccountingUnit: %>selected<%end %>><%= @T(row.display) %></option>
<option value="<%= row.value %>" <% if row.value is @timeAccountingUnit: %>selected<%end %>><%- @T(row.display) %></option>
<% end %>
<% end %>
</select>

View File

@@ -4,7 +4,7 @@
<div class="js-suggestions">
<h3><%- @T('Find translation suggestion') %><span class="subtitle js-suggestionsCounterContainer hide"> (<span class="js-suggestionsCounter">?</span> <%- @T('matches found') %>)</span> <span class="tiny loading icon js-suggestionsLoader hide" style="margin-left: 3px;"></span></h3>
<div class="form-group">
<input class="form-control js-suggestionsSearchInput" type="text" placeholder="<%= @T('Search…') %>">
<input class="form-control js-suggestionsSearchInput" type="text" placeholder="<%- @T('Search…') %>">
</div>
<div class="scroll-table-container js-suggestionsTable"></div>
</div>

View File

@@ -11,7 +11,7 @@
</div>
<div class="controls controls--button ignore-readonly">
<input readonly id="callback_url" name="callback_url" name="callback_url" type="text" value="<%= @callback_url %>" class="form-control">
<div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%= @T('Copy to clipboard') %>">
<div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%- @T('Copy to clipboard') %>">
<span class="controls-button-inner">
<%- @Icon('clipboard') %>
</span>
@@ -25,7 +25,7 @@
</div>
<div class="controls controls--button ignore-readonly">
<input readonly id="verify_token" type="text" name="verify_token" value="<%= @channel?.options?.verify_token %>" class="form-control">
<div class="controls-button u-clickable js-copy" role="button" data-target-field="verify_token" aria-label="<%= @T('Copy to clipboard') %>">
<div class="controls-button u-clickable js-copy" role="button" data-target-field="verify_token" aria-label="<%- @T('Copy to clipboard') %>">
<span class="controls-button-inner">
<%- @Icon('clipboard') %>
</span>

View File

@@ -1,37 +1,37 @@
<div class="two-factor-auth flex">
<p><%= @T('To set up Authenticator App for your account, follow the steps below:') %></p>
<p><%- @T('To set up Authenticator App for your account, follow the steps below:') %></p>
<ol>
<li>
<p><%= @T('Unless you already have it, install one of the following authenticator apps on your mobile device:') %></p>
<p><%- @T('Unless you already have it, install one of the following authenticator apps on your mobile device:') %></p>
<ul>
<li>
<a href="https://support.google.com/accounts/answer/1066447" target="_blank"><%= @T('Google Authenticator') %></a>
<a href="https://support.google.com/accounts/answer/1066447" target="_blank"><%- @T('Google Authenticator') %></a>
</li>
<li>
<a href="https://support.authy.com/hc/en-us/articles/115001945848-Installing-Authy-apps/" target="_blank"><%= @T('Authy') %></a>
<a href="https://support.authy.com/hc/en-us/articles/115001945848-Installing-Authy-apps/" target="_blank"><%- @T('Authy') %></a>
</li>
<li>
<p>
<a href="https://support.microsoft.com/en-us/account-billing/download-and-install-the-microsoft-authenticator-app-351498fc-850a-45da-b7b6-27e523b8702a" target="_blank"><%= @T('Microsoft Authenticator') %></a>
<a href="https://support.microsoft.com/en-us/account-billing/download-and-install-the-microsoft-authenticator-app-351498fc-850a-45da-b7b6-27e523b8702a" target="_blank"><%- @T('Microsoft Authenticator') %></a>
</p>
</li>
</ul>
</li>
<li>
<p><%= @T('Open your authenticator app and scan the QR code below:') %></p>
<p><%- @T('Open your authenticator app and scan the QR code below:') %></p>
<div class="authenticator-app">
<canvas class="qr-code-canvas js-qr-code-canvas" title="<%= @T('Show authenticator app secret') %>"></canvas>
<div class="secret js-secret" title="<%= @T('Hide authenticator app secret') %>" style="display: none;" aria-label="<%= @T('Authenticator app secret') %>"><%= @config.secret %></div>
<canvas class="qr-code-canvas js-qr-code-canvas" title="<%- @T('Show authenticator app secret') %>"></canvas>
<div class="secret js-secret" title="<%- @T('Hide authenticator app secret') %>" style="display: none;" aria-label="<%- @T('Authenticator app secret') %>"><%= @config.secret %></div>
</p>
</li>
<li>
<p><%= @T('Enter the security code generated by the authenticator app:') %></p>
<p><%- @T('Enter the security code generated by the authenticator app:') %></p>
<p>
<div class="method-form js-payload-form"></div>
</p>
</li>
<li>
<p><%= @T('Press the button below to finish the setup.') %></p>
<p><%- @T('Press the button below to finish the setup.') %></p>
</li>
</ol>
</div>

View File

@@ -1,5 +1,5 @@
<div class="two-factor-auth flex">
<p><%= @T('Please save your recovery codes listed below somewhere safe. You can use them to sign in if you lose access to another two-factor method:') %></p>
<p><%- @T('Please save your recovery codes listed below somewhere safe. You can use them to sign in if you lose access to another two-factor method:') %></p>
<pre><code readonly><% for elem, i in @recovery_codes: %><%= elem %><% if i == @recovery_codes.length - 1: %><% else if i % 2: %>

View File

@@ -1,3 +1,3 @@
<p><%= @T('Security keys are hardware or software credentials that can be used as your two-factor authentication method.') %></p>
<p><%- @T('Security keys are hardware or software credentials that can be used as your two-factor authentication method.') %></p>
<div class="js-table"></div>
<p><%= @T('To register a new security key with your account, press the button below.') %></p>
<p><%- @T('To register a new security key with your account, press the button below.') %></p>

View File

@@ -1,4 +1,4 @@
<div class="main two-factor-auth flex">
<p class="text-center"><%= @T('Getting key information from the browser…') %></p>
<p class="text-center"><%- @T('Getting key information from the browser…') %></p>
<div class="loading icon"></div>
</div>

View File

@@ -12,7 +12,7 @@
<div class="form-controls">
<% if @errorMessage: %>
<p class="text-center"><%= @T('Try using your security key again.') %></p>
<p class="text-center"><%- @T('Try using your security key again.') %></p>
<button class="btn btn--primary js-retry" type="button"><%- @T('Retry') %></button>
<% else: %>
<input name="username" type="hidden" value="<%= @formPayload.username %>" />
@@ -21,7 +21,7 @@
<input name="two_factor_method" type="hidden" value="<%= @twoFactorMethodDetails.key %>" />
<div class="double-spacer"></div>
<p class="text-center"><%= @T('Verifying key information…') %></p>
<p class="text-center"><%- @T('Verifying key information…') %></p>
<div class="double-spacer"></div>
<div class="centered">
<div class="loading icon"></div>

View File

@@ -1,5 +1,5 @@
<h3 class="text-center">
<%= @T('Try Another Method') %>
<%- @T('Try Another Method') %>
</h3>
<div class="two-factor-auth">
<div class="two-factor-auth-method-buttons">
@@ -9,7 +9,7 @@
<%- @Icon(method.icon) %>
<%- @T(method.label) %>
</button>
<p class="help-block"><%= @T(method.description) %></p>
<p class="help-block"><%- @T(method.description) %></p>
</div>
<% end %>
</div>

View File

@@ -9237,6 +9237,10 @@ li.list-item-none {
background: var(--background-secondary-hover);
}
.user-menu li.add .dropdown-menu.dropdown-menu--light li > a:hover {
color: var(--text-normal);
}
.dropdown-menu.dropdown-menu--light li:hover {
background: none;
}
@@ -10777,6 +10781,14 @@ label + .wizard-buttonList {
border-top: none;
}
&-attribute {
max-width: 250px;
.modal--veryLarge & {
max-width: 400px;
}
}
select,
input {
border-color: var(--border-alt);

View File

@@ -31,6 +31,7 @@ class ReportsController < ApplicationController
backend[:condition] = condition
end
next if !backend[:adapter]
next if params['backends'][backend[:name]].blank?
result[backend[:name]] = backend[:adapter].aggs(
range_start: get_params[:start],

View File

@@ -45,8 +45,6 @@ const props = defineProps<Props>()
const reactiveContext = toRef(props, 'context')
const { currentValue } = useValue(reactiveContext)
// TODO: add maybe something to extract the props from the context, instead of using context.XYZ (or props.context.XYZ)
const disabledPlugins = Object.entries(props.context.meta || {})
.filter(([, value]) => value.disabled)
.map(([key]) => key as EditorCustomPlugins)

View File

@@ -10,6 +10,8 @@ import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
import testFlags from '#shared/utils/testFlags.ts'
const props = defineProps(nodeViewProps)
const initialHeight = props.node.attrs.height
const initialWidth = props.node.attrs.width
const needBase64Convert = (src: string) => {
return !(src.startsWith('data:') || src.startsWith('cid:'))
@@ -59,8 +61,9 @@ const onLoadImage = (e: Event) => {
const img = e.target as HTMLImageElement
const { naturalWidth, naturalHeight } = img
dimensions.width = naturalWidth
dimensions.height = naturalHeight
dimensions.width = initialWidth !== '100%' ? initialWidth : naturalWidth
dimensions.height = initialHeight !== 'auto' ? initialHeight : naturalWidth
dimensions.maxHeight = naturalHeight
dimensions.maxWidth = naturalWidth
imageLoaded.value = true
@@ -100,6 +103,8 @@ const wrapperStyle = computed(() => {
:style="style"
:src="src"
:alt="node.attrs.alt"
:width="node.attrs.width"
:height="node.attrs.height"
:title="node.attrs.title"
:draggable="isDraggable"
@load="onLoadImage"

View File

@@ -32,7 +32,7 @@ module KnowledgeBaseBreadcrumbHelper
def calculate_breadcrumb_to_category(category)
return [] if category.blank?
output = [category]
output = [find_category(category)]
parent = category
while (parent = find_category(parent&.parent_id))

View File

@@ -12,7 +12,7 @@ class Channel < ApplicationModel
scope :active, -> { where(active: true) }
scope :in_area, ->(area) { where(area: area) }
validates_with Validations::EmailAccountUniquenessValidator
validates_with Validations::ChannelEmailAccountUniquenessValidator
after_create :email_address_check
after_update :email_address_check

View File

@@ -120,7 +120,7 @@ returns
def process(channel, msg, exception = true)
process_with_timeout(channel, msg)
rescue => e
failed_email = ::FailedEmail.create(data: msg, parsing_error: e)
failed_email = ::FailedEmail.create!(data: msg, parsing_error: e)
message = <<~MESSAGE.chomp
Can't process email. Run the following command to get the message for issue report at https://github.com/zammad/zammad/issues:

View File

@@ -283,7 +283,7 @@ returns
preferences_maybe = {}
preferences_maybe[direction] = []
lookup(extract_numbers(caller_id)).each do |record|
lookup(extract_numbers(caller_id)).each do |record| # rubocop:disable Metrics/BlockLength
if record.level == 'known'
preferences_known[direction].push record.attributes
else
@@ -294,6 +294,16 @@ returns
user = User.lookup(id: record.user_id)
if user
comment += user.fullname
if comment.blank?
%w[phone mobile].each do |item| # rubocop:disable Performance/CollectionLiteralInLoop
next if user[item].blank? # rubocop:disable Metrics/BlockNesting
comment += user[item]
break
end
end
end
elsif record.comment.present?
comment += record.comment

View File

@@ -10,6 +10,7 @@ class LdapSource < ApplicationModel
store :preferences
def self.by_user(user)
return if user.blank? || user.source.blank?
return if !%r{^Ldap::(\d+)$}.match?(user.source)
LdapSource.find(user.source.split('::')[1])

View File

@@ -261,7 +261,7 @@ returns
if !has_editor && !has_reader
elem.destroy!
elsif !has_editor && has_reader
elem.update!(access: 'reader') if permission.access == 'editor'
elem.update!(access: 'reader') if elem.access == 'editor'
end
end

View File

@@ -74,7 +74,7 @@ translate strings in Ruby context, e. g. for notifications
=end
def self.translate(locale, string, *args)
translated = find_source(locale, string)&.target || string
translated = find_source(locale, string)&.target.presence || string
translated %= args if args.any?

View File

@@ -15,7 +15,8 @@ class Service::Translation::Upsert < Service::Base
translation = Translation.find_source(locale, source)
if translation
return translation.update!(target: target)
translation.update!(target: target)
return translation
end
Translation.create!(locale: locale, source: source, target: target, is_synchronized_from_codebase: false)

View File

@@ -1,4 +1,4 @@
Inicio de sesión como administrador en #{config.product_name}
Inicio de sesión como administrador a #{config.product_name}
<div>Hola, #{user.firstname}:</div>
<br>

View File

@@ -1,8 +1,8 @@
#{config.product_name} için yönetim girişi
#{config.product_name} için yönetici girişi
<div>Merhaba #{user.firstname},</div>
<br>
<div>#{config.product_name} hesabınız için bir yönetim girişi talebi aldık <b>#{user.login}</b>.</div>
<div>#{config.product_name} hesabınız için bir yönetici girişi talebi aldık <b>#{user.login}</b>.</div>
<br>
<div>Devam etmek için lütfen aşağıdaki bağlantıya tıklayın (veya URL'yi kopyalayıp tarayıcınıza yapıştırın):</div>
<br>
@@ -10,6 +10,6 @@
<br>
<div>Bu bağlantı sizi giriş yapabileceğiniz bir sayfaya yönlendirir.</div>
<br>
<div>Giriş yapmak istemiyorsanız, lütfen bu mesajı dikkate almayın.</div>
<div>Giriş yapmak istemiyorsanız, lütfen bu mesajı dikkate almayın ve tamamen sildiğinizden emin olun.</div>
<br>
<div>#{config.product_name} Ekibiniz</div>

View File

@@ -0,0 +1,10 @@
[undeliverable] Mesaj çok büyük
Sayın #{mail.from_display_name},
Maalesef "#{mail.subject}" başlıklı e-postanız bir veya daha fazla alıcıya teslim edilemedi.
Mesajınızın boyutu #{mail.msg_size} MB, ancak yalnızca #{config.postmaster_max_size} MB'a kadar olan mesajları kabul ediyoruz.
Lütfen mesaj boyutunu küçültüp tekrar deneyin. Anlayışınız için teşekkür ederiz.
#{config.fqdn} E-Posta Yöneticisi

View File

@@ -12,4 +12,4 @@ Restablecer contraseña en #{config.product_name}
<br>
<div>Si no desea restablecer su contraseña puede ignorar este mensaje. La contraseña no se cambiará.</div>
<br>
<div>El equipo de #{config.product_name}</div>
<div>Su equipo #{config.product_name}</div>

View File

@@ -8,7 +8,7 @@ Ponovno postavite svoju #{config.product_name} lozinku
<br>
<div><a href="#{url}">#{url}</a></div>
<br>
<div>Ovaj link vas vodi na stranicu gdje možete resetirati Vašu lozinku.</div>
<div>Ovaj link Vas vodi na stranicu gdje možete promijeniti Vašu lozinku.</div>
<br>
<div>Ukoliko to ne želite, molimo Vas da ignorirate ovu poruku. Vaša lozinka neće biti resetirana.</div>
<br>

View File

@@ -10,6 +10,6 @@ Odzyskiwanie hasła #{config.product_name}
<br>
<div>Link przeniesie Cię na witrynę, na której możliwa jest zmiana hasła.</div>
<br>
<div>Jeżeli nie chcesz zmieniać swego hasła, zignoruj tego maila. Twoje hasło nie zostanie zmienione.</div>
<div>Jeżeli nie chcesz zmieniać swego hasła, zignoruj ten e-mail. Twoje hasło nie zostanie zmienione.</div>
<br>
<div>Zespół #{config.product_name}</div>

View File

@@ -0,0 +1,15 @@
#{config.product_name} şifreni sıfırla.
<div>Merhaba #{user.firstname},</div>
<br>
<div>#{config.product_name} Şifrenizin sıfırlanması için bir talep aldık. Hesap: <b>#{user.login}</b>.</div>
<br>
<div>Şifrenizi sıfırlamak istiyorsanız aşağıdaki bağlantıya tıklayın (veya URL'yi kopyalayıp tarayıcınıza yapıştırın):</div>
<br>
<div><a href="#{url}">#{url}</a></div>
<br>
<div>Bu bağlantı sizi şifrenizi değiştirebileceğiniz bir sayfaya yönlendirir.</div>
<br>
<div>Şifrenizi sıfırlamak istemiyorsanız lütfen bu mesajı dikkate almayın ve tamamen sildiğinizden emin olun. Şifreniz sıfırlanmayacaktır.</div>
<br>
<div>#{config.product_name} Ekibi</div>

View File

@@ -1,4 +1,4 @@
Confirme la cuenta de #{config.product_name}, #{user.firstname} #{user.lastname}
Confirma tu cuenta #{config.product_name}, #{user.firstname} #{user.lastname}
<div>Hola, #{user.firstname}:</div>
<br>

View File

@@ -1,8 +1,8 @@
#{user.firstname} #{user.lastname}, potvrdite svoj #{config.product_name} račun
Potvrdite svoj #{config.product_name} račun, #{user.firstname} #{user.lastname}
<div>Bok #{user.firstname},</div>
<br>
<div>potvrdite svoju mail adresu za otvaranje #{config.product_name}računa. Vrlo je jednostavno, samo kliknite na link ispod.</div>
<div>potvrdite svoju mail adresu za dovršavanje #{config.product_name} računa. Vrlo je jednostavno, samo kliknite na link ispod.</div>
<br>
<div><a href="#{url}">#{url}</a></div>
<br>

View File

@@ -1,8 +1,8 @@
Potwierdź swoje konto na #{config.product_name}, #{user.firstname} #{user.lastname}
<div>Witaj #{user.firstname},</div>
<div>Cześć #{user.firstname},</div>
<br>
<div>Potwierdź swój adres e-mail, aby aktywować swoje konto #{config.product_name}. To proste, użyj linku poniżej.</div>
<div>Potwierdź swój adres e-mail, aby ukończyć tworzenie konta #{config.product_name}. To jest łatwe, po prostu kliknij w link poniżej.</div>
<br>
<div><a href="#{url}">#{url}</a></div>
<br>

View File

@@ -2,8 +2,8 @@
<div>Merhaba #{user.firstname},</div>
<br>
<div>#{config.product_name} hesabınızı tamamlamak için e-posta adresinizi onaylayın. Çok kolay, aşağıdaki bağlantıya tıklamanız yeterli.</div>
<div>#{config.product_name} kaydınızı tamamlamak için e-posta adresinizi onaylayın. Aşağıdaki bağlantıya tıklamanız yeterli.</div>
<br>
<div><a href="#{url}">#{url}</a></div>
<div><a href="#{config.http_type}://#{config.fqdn}/#email_verify/#{token.token}">#{config.http_type}://#{config.fqdn}/#email_verify/#{token.token}</a></div>
<br>
<div>#{config.product_name} Ekibiniz</div>

View File

@@ -12,4 +12,4 @@ Restablecer su contraseña de #{config.product_name}
<br>
<div>Si no desea cambiar su contraseña, ignore este mensaje. Su contraseña no será restablecida.</div>
<br>
<div>El equipo de #{config.product_name}</div>
<div>Su equipo #{config.product_name}</div>

View File

@@ -8,7 +8,7 @@ Ponovno postavite svoju #{config.product_name} lozinku
<br>
<div><a href="#{url}">#{url}</a></div>
<br>
<div>Ovaj link vas vodi na stranicu gdje možete resetirati Vašu lozinku.</div>
<div>Ovaj link Vas vodi na stranicu gdje možete resetirati Vašu lozinku.</div>
<br>
<div>Ukoliko to ne želite, molimo, ignorirajte ovu poruku. Vaša lozinka neće biti resetirana.</div>
<br>

View File

@@ -0,0 +1,15 @@
#{config.product_name} şifrenizi sıfırlayın
<div>Merhaba #{user.firstname},</div>
<br>
<div>Siz veya bir başkası bu e-posta adresiyle kaydolmayı denediniz. Ancak bu e-posta zaten kullanılıyor.</div>
<br>
<div>Şifrenizi sıfırlamak istiyorsanız aşağıdaki bağlantıya tıklayın (veya URL'yi kopyalayıp tarayıcınıza yapıştırın):</div>
<br>
<div><a href="#{url}">#{url</a></div>
<br>
<div>Bu bağlantı sizi şifrenizi değiştirebileceğiniz bir sayfaya yönlendirir.</div>
<br>
<div>Şifrenizi sıfırlamak istemiyorsanız lütfen bu mesajı dikkate almayın ve tamamen sildiğinizden emin olun. Şifreniz sıfırlanmayacak.</div>
<br>
<div>#{config.product_name} Ekibiniz</div>

View File

@@ -0,0 +1,9 @@
Deneme Bileti!
<div>Sayın #{agent.firstname},</div>
<br>
<div>Bu bir <b>test biletidir</b>. Ben bir müşteriyim ve yardıma ihtiyacım var! :)</div>
<br>
<div>#{customer.fullname}</div>
<br>
<div>#{config.product_name} Ekibiniz</div>

View File

@@ -0,0 +1,24 @@
Yeni bilet (#{ticket.title})
<div>Merhaba #{recipient.firstname},</div>
<br>
<div>Yeni bilet (#{ticket.title}), "<b>#{current_user.longname}</b>" tarafından oluşturuldu.</div>
<br>
<div>
Grup: #{ticket.group.name}<br>
Sahip: #{ticket.owner.fullname}<br>
Konum: #{t(ticket.state.name)}<br>
</div>
<br>
<% if @objects[:article] %>
<div>
Bilgi:
<blockquote type="cite">
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">Görüntüle #{config.product_name}</a>
</div>

View File

@@ -0,0 +1,18 @@
Bilet eskale edildi (#{ticket.title})
<div>Merhaba #{recipient.firstname},</div>
<br>
<div>(#{ticket.title}) bileti "<b>#{ticket.customer.longname}</b>" tarafından, "#{ticket.escalation_at}" eskale edildi.</div>
<br>
<% if @objects[:article] %>
<div>
Bilgi:
<blockquote type="cite">
#{article.body_as_html}
</blockquote>
</div>
<% end %>
<br>
<div>
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{config.product_name} üzerinde görüntüle</a>
</div>

View File

@@ -0,0 +1,19 @@
#{config.product_name} התחברות ממכשיר חדש
<div>שלום #{user.firstname},</div>
<br>
<div>נראה שנכנסת לחשבון שלך <b>באמצעות מכשיר חדש</b> ב-"#{user_device.created_at}":</div>
<br>
<div>
המכשיר שלך: #{user_device.name}<br>
המיקום שלך (יחסי): #{user_device.location}<br>
ה-IP שלך: #{user_device.ip}<br>
</div>
<br>
<div>המכשיר שלך נוסף לרשימת המכשירים המוכרים שלך, שאותה תוכל להציג כאן:</div>
<br>
<div><a href="#{config.http_type}://#{config.fqdn}/#profile/devices">#{config.http_type}://#{config.fqdn}/#profile/devices </a></div>
<br>
<div>אם זה לא היית אתה, אנא הסר את המכשיר, שנה את סיסמת החשבון שלך ופנה למנהל המערכת שלך. ייתכן שמישהו קיבל גישה לא מורשית לחשבון שלך.</div>
<br>
<div>צוות #{config.product_name} שלך</div>

View File

@@ -0,0 +1,13 @@
#{config.fqdn} adresindeki #{config.product_name} için davetiye
<div>Merhaba #{user.firstname},</div>
<br>
<div>Ben (#{current_user.firstname} #{current_user.lastname}) sizi müşteri destek platformumuz #{config.product_name}'e davet ediyorum.</div>
<br>
<div><a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.token}">burayı</a> tıklayın ve şifrenizi ayarlayın.</div>
<br>
<div>İyi günler...</div>
<br>
<div>#{current_user.firstname} #{current_user.lastname}</div>
<br>
<div>#{config.product_name} Ekibiniz</div>

Some files were not shown because too many files have changed in this diff Show More