Compare commits

...

51 Commits

Author SHA1 Message Date
Florian Liebe
1f09f838a2 Release Zammad 6.4.0. 🚀 2024-11-06 07:38:34 +01:00
Mantas Masalskis
762d7688b0 Fixes #5392 - New checklists will not be indexed according to regular conversion in ticket records 2024-11-05 17:41:29 +01:00
Martin Gruner
8e26723b73 Maintenance: GraphQL RSpec could allow indifferent access 2024-11-05 16:59:44 +02:00
Mantas Masalskis
8623b79379 Fixed #5367 - Checklist partial rerendering. 2024-11-05 13:39:01 +01:00
Dominik Klein
34c3085115 Fixes #5390 - Taskbar uniqueness check leads to problems when old unexpected... 2024-11-05 12:09:55 +01:00
Florian Liebe
b1a28c201e Fixes #5385 - Missing attachment alert is shown if 'attachment' is written in the signature.
Co-authored-by: Dominik Klein <dk@zammad.com>
2024-11-04 14:53:34 +01:00
Weblate
89b6b3f5c0 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Michal <m.plawsiuk@razit.pl>
Co-authored-by: Random <random_r@users.noreply.translations.zammad.org>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/it/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/pl/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-11-02 09:00:01 +01:00
Weblate
8d794975d4 Maintenance: Update translated files and catalog 2024-10-30 06:39:46 +00:00
Weblate
3c53eddaf4 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Anael Mobilia <contrib@anael.eu>
Co-authored-by: Decio E. Silva <decio@verinatec.com.br>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Nikita <nyakovlev@iko.tech>
Co-authored-by: Rok Poje <rok.poje@mintpos.si>
Co-authored-by: Weblate <noreply@weblate.org>
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/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/pt_BR/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/ru/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sr_Cyrl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sv/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-29 23:30:06 +01:00
Martin Gruner
fd2b8c3bce Maintenance: Align ES testing strategy with other service containers
(cherry picked from commit 654316a39a)

Co-authored-by: Martin Gruner <mg@zammad.com>
2024-10-29 09:50:40 +01:00
Florian Liebe
65810f7ba0 Maintenance: Update dependency rexml to v3.3.9 2024-10-29 08:33:32 +01:00
Martin Gruner
366231aaaf Maintenance: Solve false gitleaks positives
(cherry picked from commit d4099e5003)

d4099e50 Maintenance: Solve false gitleaks positives

Co-authored-by: Martin Gruner <mg@zammad.com>
2024-10-28 14:49:43 +01:00
Martin Gruner
93a85c0357 Maintenance: Allow testing against different redis service versions 2024-10-28 11:12:40 +01:00
Rolf Schmidt
8db459ce53 Fixes #5381 - Typo in database migration of Thirdparty-Login No user creation on logon.
Co-authored-by: Florian Liebe <fl@zammad.com>


(cherry picked from commit 4d1f0e2bf5)

f23871a1 Fixes #5381 - Typo in database migration of Thirdparty-Login No user creation on logon.
f7b3401a throw error

Co-authored-by: Rolf Schmidt <rolf.schmidt@zammad.com>
2024-10-28 10:00:46 +01:00
Martin Gruner
84133a875f Maintenance: Create basic performance tests in CI
(cherry picked from commit fa925a4a79)

c004f1f3 Maintenance: Create basic performance tests in CI
58df87a4 Run in push pipelines for now
1b903920 Add missing service
afa9440d Typo
4b5b70bc Fill database
fd12f3cb Change invocation
150dcfd1 Test from ME
8fb81835 Refactor
9bbbe487 Add test for assets:precompile
c69bfb99 Clear activerecord cache
dbd1614e Improved output
2e615e93 Add branch switching
a94fa15b Fix gitlab syntax
24beed7a Rails 6.1 compat
789989f5 Save to file for later comparison
68e18cf7 Fix compatibilty to Rails 6.1
378e84d0 Drop file handling code again, and switch to versin based limits instead
21d7234e Improved pipeline configuration and import speed
a1b0abdc Improve output and expectations
3b0b2784 Simplified code, add performance fixes.
99a26b76 Decrease the limit
52768dc9 Switch test stage
06dd026f Improve comment
796f45ca Revert branch trigger change for packager.io.

Co-authored-by: Martin Gruner <mg@zammad.com>
2024-10-28 09:40:01 +01:00
Weblate
4df1e55b18 Maintenance: Update translated files and catalog 2024-10-28 08:14:39 +00:00
Weblate
2bda998d6f Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Anael Mobilia <contrib@anael.eu>
Co-authored-by: Gius4 <giuseppe@socci.eu>
Co-authored-by: JPB <JPB@users.noreply.translations.zammad.org>
Co-authored-by: Nikita <nyakovlev@iko.tech>
Co-authored-by: Ralf Schmid <rsc@zammad.com>
Co-authored-by: Random <random_r@users.noreply.translations.zammad.org>
Co-authored-by: Rok Poje <rok.poje@mintpos.si>
Co-authored-by: Weblate <noreply@weblate.org>
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/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/it/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/nl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/ru/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sv/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-28 09:12:25 +01:00
Mantas Masalskis
5e2004aa1d Maintenance: Improve checklists db structure and assets generation. 2024-10-25 19:53:16 +02:00
Martin Gruner
34e874626a Maintenance: Fix false gitleaks positives 2024-10-18 17:02:37 +02:00
Dominik Klein
5cf2bc4273 Fixes #5376 - Kayako import ignores closed cases that are older than 30 days. 2024-10-18 08:23:03 +02:00
Martin Gruner
ca361ad8c2 Maintenance: Stabilize selector test 2024-10-18 07:31:29 +02:00
Martin Gruner
684d8fe3e7 Maintenance: Stabilize search tests 2024-10-18 07:31:06 +02:00
Martin Gruner
b639db89e3 Deleted translation using Weblate (Kazakh) 2024-10-17 14:10:20 +02:00
Weblate
cf922c0ce7 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-17 13:40:59 +02:00
Weblate
534391ecf5 Maintenance: Update translated files and catalog 2024-10-17 12:20:57 +01:00
Weblate
ada583d0f8 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Anael Mobilia <contrib@anael.eu>
Co-authored-by: Nikita <nyakovlev@iko.tech>
Co-authored-by: Random <random_r@users.noreply.translations.zammad.org>
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/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/it/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/ru/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-17 13:14:21 +02:00
Dominik Klein
5c460457ad Fixes #5375 - Kayako import fails when post includes json-files as attachment. 2024-10-17 12:37:52 +02:00
Benjamin Scharf
e22b0f5c6e Follow up 12e75243 - Improve Checklist handling 2024-10-17 11:24:59 +02:00
Martin Gruner
b2946c81dc Maintenance: Update Rails to version 7.1.4.1.
(cherry picked from commit e7efc69dcf)

a4c09ddb Maintenance: Update Rails to version 7.1.4.1.
0fd0e13a Maintenance: Update Rails to version 7.1.4.1.

Co-authored-by: Florian Liebe <fl@zammad.com>
2024-10-17 06:37:11 +02:00
Weblate
f6fd7a48ad Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Lorenzo Milesi <lorenzo@mile.si>
Co-authored-by: Nikita <nyakovlev@iko.tech>
Co-authored-by: Random <random_r@users.noreply.translations.zammad.org>
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/it/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/ru/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-16 07:59:20 +02:00
Weblate
74c0d4cf51 Maintenance: Update translated files and catalog 2024-10-15 12:37:39 +01:00
Weblate
fdb8ecddca Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Elodie Massa <elodie@association-enoria.org>
Co-authored-by: Ivan Perovic <ip@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Random <random_r@users.noreply.translations.zammad.org>
Co-authored-by: Sajjad Jazini <s.jazini@vatanmail.ir>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fa/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/hr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/it/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-15 13:32:31 +02:00
Florian Liebe
3f31b555f1 Fixes #5361 - Fetching mails with protonmail-bridge is not possible due to closed stream error.
Co-authored-by: Florian Liebe <fl@zammad.com>
Co-authored-by: Tobias Schäfer <ts@zammad.com>
2024-10-15 13:10:24 +02:00
Rolf Schmidt
3d46cab28d Maintenance: Add missing license note about status report icon.
(cherry picked from commit e1e2f46d4d)

e1e2f46d Maintenance: Add missing license note about status report icon.

Co-authored-by: Rolf Schmidt <rolf.schmidt@zammad.com>
2024-10-14 14:54:25 +02:00
Rolf Schmidt
00a4b00194 Maintenance: Add status report icon.
(cherry picked from commit 86c30bdd13)

86c30bdd Maintenance: Add status report icon.

Co-authored-by: Rolf Schmidt <rolf.schmidt@zammad.com>
2024-10-14 14:48:14 +02:00
Rolf Schmidt
13e4f810d5 Fixes #5310 - Icons are not up to date.
Co-authored-by: Mantas Masalskis <mm@zammad.com>


(cherry picked from commit 006fc5c818)

dcafe124 Fixes #5310 - Icons are not up to date.
ed71ab6c wip
25e3a13f remove code
5c7263f4 change more
c0e7eea7 change url
03aa0b62 readd comment
3091b536 add application helper
1a9d50ee remove obsolete code
ed400287 add icons url to kb
c8ec305e improve icons url selector
7fe6f14f queryElement -> documentElement
94bbe9c9 (un)check is now a single action
443bc214 fix html utils test
5f9a82f4 remove util var
5150f2a2 Revert "(un)check is now a single action"
1d0be7c1 fix icon
085b2e0c changing to @Icon in some strange copy-pastas
3bf91cc7 one more capypaste cleaned up
9bb0b36c fix urls
5a7f9798 add request cache for icons url

Co-authored-by: Rolf Schmidt <rolf.schmidt@zammad.com>
2024-10-10 15:48:36 +02:00
Tarek Shehab
9622724696 Maintenance: Automate creation of redis service on sles in postinstall.sh 2024-10-10 13:20:07 +02:00
Weblate
d8c0c889c0 Maintenance: Update translated files and catalog 2024-10-09 23:18:33 +01:00
Weblate
17803e445f Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Ivan Perovic <ip@zammad.com>
Co-authored-by: JPB <JPB@users.noreply.translations.zammad.org>
Co-authored-by: Lorenzo Milesi <lorenzo@mile.si>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Nikita <nyakovlev@iko.tech>
Co-authored-by: Ralf Schmid <rsc@zammad.com>
Co-authored-by: Weblate <noreply@weblate.org>
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/de/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/hr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/it/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/nl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/ru/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sr_Cyrl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sv/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-10 00:16:29 +02:00
Martin Gruner
0d42c95552 Maintenance: Automate testing of packager.io packages
(cherry picked from commit 25bdaa11b6)

aefb873e Maintenance: Automate testing of packager.io packages
f1acc0a8 Make branch configurable
767a6040 Try to add to CI pipeline
b1f63c8b Fix class name
acc7c2ec Fix variables key
49ff547a Use docker image
72977a5e Switch to sh
76df04fa Switch to fixed branch name
a487d5dc Debug
441167db debug
82c69a82 Avoid config mount
cff3b272 Add debian 11, improve cleanup
2870ae22 Improve cleanup
fd53b8e7 Add ubuntu 24.04
2bbf0a65 Refactor, add ubuntu 20.04 and 22.04
48e02fda Fix subshell
6f9f203c Fix scenario script
133397bd Fix scenario script
39716f31 Switch to noninteractive deb
20ee2cf5 Improve cleanup
4a55b6c9 Add rhel 9
6f35a6ab Add rhel 8
e6bee1b0 Improve curl check and un-silence apt
d495fefa Solve concurrency failures
e1d48a8d Remove sleep
4d0ca19b Use ipv4 for testing
7026d15b Add sles 15
fa97646d Fix suse, drop hardcoded branch
4ba95d11 Fix redis on suse, revert CI stage
0e5dfdef Add interruptible
13406384 Fix rhel-8
f7f01a79 Add missing dependencies
df0af150 Drop dep workarounds
752ea259 Fix branch name interpolation
607bd5a6 reduce redundancy
65c117c7 Add update test, simlifications
a2bcbb3c Only enable for scheduled and manual pipelines

Co-authored-by: Martin Gruner <mg@zammad.com>
2024-10-09 16:41:20 +02:00
Rolf Schmidt
9147c3be88 Fixes #5362 - Fix behaviour of array values in CSV export/import.
Co-authored-by: Florian Liebe <fl@zammad.com>


(cherry picked from commit 0b11fbf48b)

a0ad036a Fixes #5362 - Fix behaviour of array values in CSV export/import.
fd16c58a fix import csv
d676d2c0 remove newline
aebefdcb fix role import test
233de890 fix member import test

Co-authored-by: Rolf Schmidt <rolf.schmidt@zammad.com>
2024-10-09 15:17:48 +02:00
Rolf Schmidt
ed48331cec Maintenance: Improve database name fetching to be compatible with docker.
(cherry picked from commit 9d15959a42)

9d15959a Maintenance: Improve database name fetching to be compatible with docker.

Co-authored-by: Rolf Schmidt <rolf.schmidt@zammad.com>
2024-10-09 15:17:17 +02:00
Mantas Masalskis
1d542d1b31 Fixes #5364 - Searching for a ticket with a keyword from an checklist item is not working 2024-10-08 17:01:28 +02:00
Dusan Vuckovic
40f32aa102 Fixes #5308 - Ticket creation with Send Email turns into Net::SMTPSyntaxError if customer has no email address.
Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Florian Liebe <fl@zammad.com>
2024-10-08 16:55:52 +02:00
Weblate
3974df9a2d Maintenance: Update translated files and catalog 2024-10-08 08:49:59 +01:00
Weblate
238b03fba7 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Arturo <r2r0@posteo.de>
Co-authored-by: Ivan Perovic <ip@zammad.com>
Co-authored-by: Jeroen Baten <jbaten@i2rs.nl>
Co-authored-by: Misha <misha@association-enoria.org>
Co-authored-by: Sajjad Jazini <s.jazini@vatanmail.ir>
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/es/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fa/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/hr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/nl/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-08 09:43:43 +02:00
Dominik Klein
f5b7299c2b Knowledge Base update with very big images is not working correctly (image and following content not saved) 2024-10-08 09:36:33 +02:00
Weblate
95c6b69d64 Maintenance: Update translated files and catalog 2024-10-07 09:54:00 +01:00
Weblate
e7be8d42a0 Maintenance: Translations update from translations.zammad.org.
Special thanks to the amazing translation team. You rock! 💪

Co-authored-by: Alfredo Sola <alfredo@sola.es>
Co-authored-by: Anael Mobilia <contrib@anael.eu>
Co-authored-by: DG-Caille <didier.grondin@caille.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Ivan Perovic <ip@zammad.com>
Co-authored-by: Leif-Jöran Olsson <info@friprogramvarusyndikatet.se>
Co-authored-by: Lorenzo Milesi <lorenzo@mile.si>
Co-authored-by: Louise Forsen <louise.forsen@entryevent.se>
Co-authored-by: MBekspert <michal.bosak@ekspert.biz>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Misha <misha@association-enoria.org>
Co-authored-by: Nikita <nyakovlev@iko.tech>
Co-authored-by: Ralf Schmid <rsc@zammad.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: sigurbjartur <github@sigurbjartur.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/fr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/hr/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/is/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/it/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/pl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/ru/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sr_Cyrl/
Translate-URL: https://translations.zammad.org/projects/zammad/zammad-stable/sv/
Translation: Zammad/Zammad (pre-release version, this will become Zammad 6.4 soon)
2024-10-07 10:51:29 +02:00
Martin Gruner
71f992a7fa Maintenance: Repair broken badge in Readme 2024-10-04 14:51:56 +02:00
Martin Gruner
b54fda8ac2 Maintenance: Update security info on supported versions for the upcoming release 2024-10-04 11:04:15 +02:00
271 changed files with 8585 additions and 28351 deletions

View File

@@ -2,10 +2,8 @@
source_rvm:
- source /etc/profile.d/rvm.sh # ensure RVM is loaded
bundle_install:
- echo -e "\\e[0Ksection_start:`date +%s`:bundle_install[collapsed=true]\\r\\e[0Kbundle install"
- bundle config set --local deployment 'true'
- bundle install -j $(nproc)
- echo -e "\\e[0Ksection_end:`date +%s`:bundle_install\\r\\e[0K"
- bundle install -j $(nproc) > /dev/null
pnpm_init: &pnpm_init
- pnpm config set store-dir ${CI_PROJECT_DIR}/tmp/cache/pnpm-store
pnpm_install: &pnpm_install

View File

@@ -84,7 +84,7 @@
alias: selenium-firefox
redis:
name: $CI_REGISTRY/docker/zammad-redis:stable
name: $CI_REGISTRY/docker/zammad-redis:$REDIS_TAG
alias: redis
memcached:

View File

@@ -17,10 +17,12 @@ variables:
# Settings for elasticsearch, if active
ES_INDEX: 'estest.ci'
ELASTICSEARCH_TAG: '7.16'
ELASTICSEARCH_TAG: 'stable'
# PostgreSQL
# Define image versions. Regular commits test against 'stable' (lowest supported version),
# scheduled pipelines against 'latest' (newest stable release of the services).
POSTGRESQL_TAG: 'stable'
REDIS_TAG: 'stable'
# Compile assets only once.
CI_SKIP_ASSETS_PRECOMPILE: 'true'

View File

@@ -13,21 +13,15 @@
- bundle exec rspec --tag searchindex --tag ~type:system --profile 10
# Lowest supported version
rspec:es:7.8:
rspec:es:stable:
extends:
- .template_integration_es
variables:
ELASTICSEARCH_TAG: '7.8.1'
ELASTICSEARCH_TAG: 'stable'
# Highest supported version (except for newer releases that are not yet in CI)
rspec:es:7.16:
rspec:es:latest:
extends:
- .template_integration_es
variables:
ELASTICSEARCH_TAG: '7.16'
rspec:es:8.1:
extends:
- .template_integration_es
variables:
ELASTICSEARCH_TAG: '8.1'
ELASTICSEARCH_TAG: 'latest'

View File

@@ -0,0 +1,61 @@
.packager.io:
image: docker
stage: test
interruptible: false
rules:
- if: $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"
services: []
cache: []
before_script: []
script:
- .gitlab/packager.io/run_scenarios.sh
after_script:
- .gitlab/packager.io/cleanup.sh
packager.io:debian-11:
extends: [.packager.io]
variables:
DISTRIBUTION: debian
DISTRIBUTION_VERSION: '11'
packager.io:debian-12:
extends: [.packager.io]
variables:
DISTRIBUTION: debian
DISTRIBUTION_VERSION: '12'
packager.io:ubuntu-20.04:
extends: [.packager.io]
variables:
DISTRIBUTION: ubuntu
DISTRIBUTION_VERSION: '20.04'
packager.io:ubuntu-22.04:
extends: [.packager.io]
variables:
DISTRIBUTION: ubuntu
DISTRIBUTION_VERSION: '22.04'
packager.io:ubuntu-24.04:
extends: [.packager.io]
variables:
DISTRIBUTION: ubuntu
DISTRIBUTION_VERSION: '24.04'
packager.io:rhel-8:
extends: [.packager.io]
variables:
DISTRIBUTION: rhel
DISTRIBUTION_VERSION: '8'
packager.io:rhel-9:
extends: [.packager.io]
variables:
DISTRIBUTION: rhel
DISTRIBUTION_VERSION: '9'
packager.io:sles-15:
extends: [.packager.io]
variables:
DISTRIBUTION: sles
DISTRIBUTION_VERSION: '15'

View File

@@ -0,0 +1,40 @@
performance:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- .gitlab/performance/**/*
# Make sure this gets executed exclusively on a runner.
tags: ['no_concurrency']
services:
- !reference [.services, postgresql]
- !reference [.services, redis]
cache: []
before_script:
# Copy performance checking code to tmp/ so that it survives branch switching
- cp -rv .gitlab/performance tmp/
# First, checkout stable and set it up.
- git fetch --unshallow >/dev/null 2>&1
- git checkout stable-6.0
- !reference [.scripts, source_rvm]
- rvm use 3.1.3
- !reference [.scripts, bundle_install]
- !reference [.scripts, configure_environment]
- !reference [.scripts, zammad_db_init]
- yarn install --silent # Required for assets:precompile test
script:
# Run performance tests for stable.
- bundle exec rails r tmp/performance/run_tests.rb
# Then, switch to the current commit, migrate to it and run a few selected tests.
- git checkout $CI_COMMIT_SHA
- 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
- bundle exec rails db:migrate > /dev/null
- rm -rf node_modules && pnpm install > /dev/null # Required for assets:precompile test
# Run performance tests for current branch.
- bundle exec rails r .gitlab/performance/run_tests.rb
after_script: []

15
.gitlab/packager.io/cleanup.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -eu
cd "$(dirname $0)"
docker compose down --timeout 0
docker volume prune --all --force
DELETE_IMAGES=$(docker image ls "zammad-packagerio-ci-${CI_JOB_ID}" -q)
if [ -n "$DELETE_IMAGES" ]
then
# shellcheck disable=SC2086
docker image rm $DELETE_IMAGES
fi

View File

@@ -0,0 +1,16 @@
---
name: zammad-packagerio-ci-${CI_JOB_ID}
services:
zammad:
image: zammad-packagerio-ci-${CI_JOB_ID}:${DISTRIBUTION}-${DISTRIBUTION_VERSION}
build:
context: "."
platforms: ["linux/amd64"]
dockerfile: dockerfiles/Dockerfile.${DISTRIBUTION}
args:
DISTRIBUTION: ${DISTRIBUTION}
DISTRIBUTION_VERSION: ${DISTRIBUTION_VERSION}
CI_COMMIT_REF_NAME: ${CI_COMMIT_REF_NAME}
privileged: true
restart: unless-stopped
expose: ["80"]

View File

@@ -0,0 +1,9 @@
ARG DISTRIBUTION_VERSION
FROM docker.io/library/debian:${DISTRIBUTION_VERSION}
ARG DISTRIBUTION
ARG DISTRIBUTION_VERSION
ARG CI_COMMIT_REF_NAME
ADD dockerfiles dockerfiles
ADD scenarios scenarios
RUN bash /dockerfiles/bootstrap.debian.sh
ENTRYPOINT [ "/sbin/init" ]

View File

@@ -0,0 +1,9 @@
ARG DISTRIBUTION_VERSION
FROM docker.io/library/rockylinux:${DISTRIBUTION_VERSION}
ARG DISTRIBUTION
ARG DISTRIBUTION_VERSION
ARG CI_COMMIT_REF_NAME
ADD dockerfiles dockerfiles
ADD scenarios scenarios
RUN bash /dockerfiles/bootstrap.rhel.sh
ENTRYPOINT [ "/sbin/init" ]

View File

@@ -0,0 +1,9 @@
ARG DISTRIBUTION_VERSION
FROM docker.io/opensuse/leap:${DISTRIBUTION_VERSION}
ARG DISTRIBUTION
ARG DISTRIBUTION_VERSION
ARG CI_COMMIT_REF_NAME
ADD dockerfiles dockerfiles
ADD scenarios scenarios
RUN bash /dockerfiles/bootstrap.sles.sh
ENTRYPOINT [ "/sbin/init" ]

View File

@@ -0,0 +1,9 @@
ARG DISTRIBUTION_VERSION
FROM docker.io/library/ubuntu:${DISTRIBUTION_VERSION}
ARG DISTRIBUTION
ARG DISTRIBUTION_VERSION
ARG CI_COMMIT_REF_NAME
ADD dockerfiles dockerfiles
ADD scenarios scenarios
RUN bash /dockerfiles/bootstrap.ubuntu.sh
ENTRYPOINT [ "/sbin/init" ]

View File

@@ -0,0 +1,12 @@
#!/usr/bin/bash
set -eu
export DEBIAN_FRONTEND=noninteractive
cd "$(dirname $0)"
bash ./shared.deb.sh
echo "deb [signed-by=/usr/share/keyrings/zammad-archive-keyring.gpg] https://dl.packager.io/srv/deb/zammad/zammad/${CI_COMMIT_REF_NAME}/debian ${DISTRIBUTION_VERSION} main" > /etc/apt/sources.list.d/zammad.list
apt-get update && apt-get install -y --download-only zammad

View File

@@ -0,0 +1,26 @@
#!/usr/bin/bash
set -eu
dnf update -y
dnf install -y systemd epel-release
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
echo "[elasticsearch-7.x]
name=Elasticsearch repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md"| tee /etc/yum.repos.d/elasticsearch-7.x.repo
dnf install -y elasticsearch
rpm --import https://dl.packager.io/srv/zammad/zammad/key
curl -o /etc/yum.repos.d/zammad.repo \
https://dl.packager.io/srv/zammad/zammad/${CI_COMMIT_REF_NAME}/installer/el/${DISTRIBUTION_VERSION}.repo
dnf update -y
dnf install -y --downloadonly zammad

View File

@@ -0,0 +1,26 @@
#!/usr/bin/bash
set -eu
zypper update -y
zypper install -y systemd
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
echo "[elasticsearch-7.x]
name=Elasticsearch repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md"| tee /etc/zypp/repos.d/elasticsearch-7.x.repo
zypper install -y elasticsearch
rpm --import https://dl.packager.io/srv/zammad/zammad/key
curl -o /etc/zypp/repos.d/zammad.repo \
https://dl.packager.io/srv/zammad/zammad/${CI_COMMIT_REF_NAME}/installer/sles/${DISTRIBUTION_VERSION}.repo
zypper update -y
zypper install -y --download-only zammad

View File

@@ -0,0 +1,12 @@
#!/usr/bin/bash
set -eu
export DEBIAN_FRONTEND=noninteractive
cd "$(dirname $0)"
bash ./shared.deb.sh
echo "deb [signed-by=/usr/share/keyrings/zammad-archive-keyring.gpg] https://dl.packager.io/srv/deb/zammad/zammad/${CI_COMMIT_REF_NAME}/ubuntu ${DISTRIBUTION_VERSION} main" > /etc/apt/sources.list.d/zammad.list
apt-get update && apt-get install -y --download-only zammad

View File

@@ -0,0 +1,18 @@
#!/usr/bin/bash
set -ex
apt-get update && apt-get dist-upgrade -y
apt-get install -y systemd systemd-sysv
rm -f /usr/sbin/policy-rc.d
apt-get install -y curl gnupg2
echo "deb [signed-by=/etc/apt/trusted.gpg.d/elasticsearch.gpg] https://artifacts.elastic.co/packages/7.x/apt stable main" | \
tee -a /etc/apt/sources.list.d/elastic-7.x.list > /dev/null
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | \
gpg --dearmor | tee /etc/apt/trusted.gpg.d/elasticsearch.gpg> /dev/null
apt-get update
apt-get -y install elasticsearch
curl -1sLf https://dl.packager.io/srv/zammad/zammad/key | \
gpg --dearmor -o /usr/share/keyrings/zammad-archive-keyring.gpg

View File

@@ -0,0 +1,26 @@
#!/bin/sh
set -eu
: "${DISTRIBUTION:=debian}"
: "${DISTRIBUTION_VERSION:=12}"
: "${CI_JOB_ID:=1}"
: "${CI_COMMIT_REF_NAME:=develop}"
export DISTRIBUTION DISTRIBUTION_VERSION CI_JOB_ID CI_COMMIT_REF_NAME
echo "Running tests for ${CI_COMMIT_REF_NAME} on ${DISTRIBUTION}-${DISTRIBUTION_VERSION}…"
cd "$(dirname $0)"
docker compose build
# shellcheck disable=SC2043
for SCENARIO in $(cd scenarios; ls -1)
do
docker compose down --timeout 0
docker compose up -d
docker compose exec zammad bash "/scenarios/${SCENARIO}/${DISTRIBUTION}.sh"
docker compose down --timeout 0
done
./cleanup.sh

View File

@@ -0,0 +1,7 @@
#!/usr/bin/bash
set -eu
cd "$(dirname $0)"
bash ./shared.deb.sh

View File

@@ -0,0 +1,11 @@
#!/usr/bin/bash
set -eu
dnf install -y zammad
curl --retry 30 --retry-delay 1 --retry-connrefused http://localhost:3000 | grep "Zammad Helpdesk"
dnf reinstall -y zammad
curl --retry 30 --retry-delay 1 --retry-connrefused http://localhost:3000 | grep "Zammad Helpdesk"

View File

@@ -0,0 +1,13 @@
#!/usr/bin/bash
set -eu
export DEBIAN_FRONTEND=noninteractive
apt-get install -y zammad
curl --retry 30 --retry-delay 1 --retry-connrefused http://localhost:3000 | grep "Zammad Helpdesk"
apt-get install -y --reinstall zammad
curl --retry 30 --retry-delay 1 --retry-connrefused http://localhost:3000 | grep "Zammad Helpdesk"

View File

@@ -0,0 +1,11 @@
#!/usr/bin/bash
set -eu
zypper install -y zammad
curl --retry 30 --retry-delay 1 --retry-connrefused http://localhost:3000 | grep "Zammad Helpdesk"
zypper install -y -f zammad
curl --retry 30 --retry-delay 1 --retry-connrefused http://localhost:3000 | grep "Zammad Helpdesk"

View File

@@ -0,0 +1,7 @@
#!/usr/bin/bash
set -eu
cd "$(dirname $0)"
bash ./shared.deb.sh

110
.gitlab/performance/run_tests.rb Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env ruby
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
require 'rails'
def run
puts "# Zammad Performance Tests\n\n"
ensure_test_data_present
run_tests!
end
def run_tests!
puts 'Run test scenarios…'
[].tap do |results|
run_test_scenarios(results:)
if results.any? { |r| r[:failed] }
puts 'Tests failed.'
exit 1
end
puts 'All tests were successful.'
end
end
def run_test_scenarios(results:)
agent = User.with_permissions('ticket.agent').first
expect(title: 'assets:precompile', max_time: 120, max_sql_queries: nil, results:) do
system('RAILS_ENV=production bundle exec rails assets:precompile > /dev/null 2>&1', exception: true)
end
max_index_queries = version_oldstable? ? 70 : 35
expect(title: 'Ticket::Overviews.index', max_time: 1, max_sql_queries: max_index_queries, results:) do
Ticket::Overviews.index(agent)
end
expect(title: 'Ticket::Overviews.all', max_time: 0.05, max_sql_queries: 5, results:) do
Ticket::Overviews.all(current_user: agent)
end
max_overview_list_time = version_oldstable? ? 15 : 20
max_overview_list_queries = version_oldstable? ? 16_000 : 25_000
expect(title: 'Sessions::Backend::TicketOverviewList#push', max_time: max_overview_list_time, max_sql_queries: max_overview_list_queries, results:) do
Sessions::Backend::TicketOverviewList.new(agent, {}).push
end
end
def version_oldstable?
Version.get.match?(%r{^6\.0\.})
end
def expect(title:, max_time:, max_sql_queries:, results:, &block)
ActiveRecord::Base.connection.query_cache.clear
Rails.cache.clear
sql_queries = 0
failed = false
callback = ->(_name, _start, _finish, _id, payload) { sql_queries += 1 if !payload[:cached] }
time = Benchmark.measure do
ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &block)
end
puts " #{title}: #{time.real}s (#{sql_queries} queries)"
if max_time && time.real > max_time
puts " ERROR: took #{time.real}s, rather than expected maximum of #{max_time}s"
failed = true
end
if max_sql_queries && sql_queries > max_sql_queries
puts " ERROR: caused #{sql_queries} SQL queries, rather than expected maximum of #{max_sql_queries}"
failed = true
end
results.push({ title:, max_time:, time: time.real, max_sql_queries:, sql_queries:, failed: })
end
def ensure_test_data_present
puts 'Ensuring test data with 15k tickets is present…'
return if Ticket.count >= 15_000
# Speed up the import
Setting.set('import_mode', true)
suppress_output do
FillDb.load(
agents: 100,
customers: 4000,
groups: 80,
organizations: 400,
overviews: 4,
tickets: 15_000,
nice: 0,
)
end
Setting.set('import_mode', false)
end
def suppress_output
original_stdout = $stdout.clone
$stdout.reopen(File.new('/dev/null', 'w'))
yield
ensure
$stdout.reopen(original_stdout)
end
run

View File

@@ -14,6 +14,7 @@ targets:
- imlib2
- shared-mime-info
- redis
- findutils
build_dependencies:
- https://download.fedoraproject.org/pub/epel/8/Everything/x86_64/Packages/i/imlib2-1.4.9-8.el8.x86_64.rpm
- https://download.fedoraproject.org/pub/epel/8/Everything/x86_64/Packages/i/imlib2-devel-1.4.9-8.el8.x86_64.rpm
@@ -108,10 +109,14 @@ targets:
- libImlib2-1
- imlib2
- shared-mime-info
- redis7
# We need the fuser binary from the psmisc package to detect the running redis systemd service.
# Because on SUSE systems, this service is arbitrarily named (e.g.) redis@foobar.service
# So we cannot hardcode the service name as we do for other supported distros.
- psmisc
- procps
- libopenssl1_1
- which
build_dependencies:
# Add packages required for build that are not in the official SLES repo.
# Direct URLs must be used since we cannot add repos on packager.io

View File

@@ -1,9 +1,165 @@
# Change Log
## [6.4.0](https://github.com/zammad/zammad/tree/6.4.0) (2024-xx-xx)
## [6.4.0](https://github.com/zammad/zammad/tree/6.4.0) (2024-11-06)
[Full Changelog](https://github.com/zammad/zammad/compare/6.3.0...6.4.0)
**Implemented enhancements:**
- Searching for a ticket with a keyword from an checklist item is not working [5364](https://github.com/zammad/zammad/issues/5364) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[ticket: checklist](https://github.com/zammad/zammad/labels/ticket%3A%20checklist)]
- Improve checklist visibility in ticket context [5336](https://github.com/zammad/zammad/issues/5336) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[ticket: checklist](https://github.com/zammad/zammad/labels/ticket%3A%20checklist)]
- WhatsApp Feedback - 24h Window Reminder Message / Emoji-Reactions [5224](https://github.com/zammad/zammad/issues/5224) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[whatsapp](https://github.com/zammad/zammad/labels/whatsapp)]
- Entry in ticket history should include the checklist item name if someone checks off an item [5320](https://github.com/zammad/zammad/issues/5320) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[history](https://github.com/zammad/zammad/labels/history)] [[ticket: checklist](https://github.com/zammad/zammad/labels/ticket%3A%20checklist)]
- Prevent user creation via Third Party Authentication [4945](https://github.com/zammad/zammad/issues/4945) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[authentication](https://github.com/zammad/zammad/labels/authentication)]
- Enhance usage of Zammad shortcuts (cross browser and cross platform) [4870](https://github.com/zammad/zammad/issues/4870) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[productivity](https://github.com/zammad/zammad/labels/productivity)]
- Checklists / To-do lists [4886](https://github.com/zammad/zammad/issues/4886) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[productivity](https://github.com/zammad/zammad/labels/productivity)] [[A:Miro](https://github.com/zammad/zammad/labels/A%3AMiro)] [[VoteHos](https://github.com/zammad/zammad/labels/VoteHos)]
- Pre-defined webhook for Microsoft Teams needs to be updated [5267](https://github.com/zammad/zammad/issues/5267) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[webhooks](https://github.com/zammad/zammad/labels/webhooks)]
- Core Workflow: Add a new condition operator to catch modified fields in the UI for existing tickets [5147](https://github.com/zammad/zammad/issues/5147) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[core workflows](https://github.com/zammad/zammad/labels/core%20workflows)]
- Subscribe/Unsubscribe from Multiple Tickets at once [5135](https://github.com/zammad/zammad/issues/5135) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[bulk](https://github.com/zammad/zammad/labels/bulk)]
- Laggs after ticket overview selection [5214](https://github.com/zammad/zammad/issues/5214) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[performance](https://github.com/zammad/zammad/labels/performance)] [[core workflows](https://github.com/zammad/zammad/labels/core%20workflows)]
- 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)]
- Improve 24 hours WhatsApp banner to update it's remaining time [5122](https://github.com/zammad/zammad/issues/5122) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Localization of triggers and schedulers [5089](https://github.com/zammad/zammad/issues/5089) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[localization](https://github.com/zammad/zammad/labels/localization)] [[scheduler](https://github.com/zammad/zammad/labels/scheduler)]
- Zammad should not write to the var/ folder [5022](https://github.com/zammad/zammad/issues/5022) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Translation Management Improvements [4875](https://github.com/zammad/zammad/issues/4875) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[localization](https://github.com/zammad/zammad/labels/localization)]
**Fixed bugs:**
- When I try to edit the Checklist fast, the checklist is lagging and then adding items [5367](https://github.com/zammad/zammad/issues/5367) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket: checklist](https://github.com/zammad/zammad/labels/ticket%3A%20checklist)]
- Taskbar uniqueness check leads to problems when old unexpected taskbar duplicates already exists [5390](https://github.com/zammad/zammad/issues/5390) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Missing attachment alert is shown if 'attachment' is written in the signature [5385](https://github.com/zammad/zammad/issues/5385) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket: create](https://github.com/zammad/zammad/labels/ticket%3A%20create)] [[ticket: actions](https://github.com/zammad/zammad/labels/ticket%3A%20actions)]
- Typo in database migration of Thirdparty-Login 'No user creation on logon' [5381](https://github.com/zammad/zammad/issues/5381) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)]
- Kayako import ignores closed cases that are older than 30 days [5376](https://github.com/zammad/zammad/issues/5376) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
- Kayako import fails when post includes json-files as attachment [5375](https://github.com/zammad/zammad/issues/5375) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
- Fetching mails with protonmail-bridge is not possible due to closed stream error [5361](https://github.com/zammad/zammad/issues/5361) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)]
- Icons are not up to date [5310](https://github.com/zammad/zammad/issues/5310) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
- Fix behaviour of array values in CSV export/import [5362](https://github.com/zammad/zammad/issues/5362) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)]
- Ticket creation with Send Email turns into Net::SMTPSyntaxError if customer has no email address [5308](https://github.com/zammad/zammad/issues/5308) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[ticket: create](https://github.com/zammad/zammad/labels/ticket%3A%20create)]
- Apollo client returns an error about exceeding max complexity of the query [5353](https://github.com/zammad/zammad/issues/5353) [[bug](https://github.com/zammad/zammad/labels/bug)] [[📱mobile-view📱](https://github.com/zammad/zammad/labels/%F0%9F%93%B1mobile-view%F0%9F%93%B1)] [[graphql](https://github.com/zammad/zammad/labels/graphql)]
- Data types are incorrect for Excel-exports [5358](https://github.com/zammad/zammad/issues/5358) [[bug](https://github.com/zammad/zammad/labels/bug)] [[export](https://github.com/zammad/zammad/labels/export)] [[excel](https://github.com/zammad/zammad/labels/excel)]
- The save button is visible in the mobile ticket detail view for an not dirty form [5356](https://github.com/zammad/zammad/issues/5356) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[📱mobile-view📱](https://github.com/zammad/zammad/labels/%F0%9F%93%B1mobile-view%F0%9F%93%B1)] [[form](https://github.com/zammad/zammad/labels/form)]
- S/MIME verification does not work when the mail does not contain the chain of certificates [5357](https://github.com/zammad/zammad/issues/5357) [[bug](https://github.com/zammad/zammad/labels/bug)] [[smime](https://github.com/zammad/zammad/labels/smime)]
- Cannot create ticket if ticket attribute called "scope" is used [5221](https://github.com/zammad/zammad/issues/5221) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)]
- Tags are not visible in the admin interface under Admin > Manage > Tags [5206](https://github.com/zammad/zammad/issues/5206) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)]
- Admin -> Reports - Translation in French - bad escaping of quote characters [5348](https://github.com/zammad/zammad/issues/5348) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- WhatsApp Business channel is not flagged with error state in case of permanent errors [5352](https://github.com/zammad/zammad/issues/5352) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[whatsapp](https://github.com/zammad/zammad/labels/whatsapp)]
- Object custom attributes - Bad render on admin [5204](https://github.com/zammad/zammad/issues/5204) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)] [[form](https://github.com/zammad/zammad/labels/form)]
- Display Issue - empty article and missing picture when sending multiple pictures over WhatsApp to Zammad [5163](https://github.com/zammad/zammad/issues/5163) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[whatsapp](https://github.com/zammad/zammad/labels/whatsapp)]
- Duplicate articles created when sending multiple pictures over WhatsApp [5164](https://github.com/zammad/zammad/issues/5164) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[whatsapp](https://github.com/zammad/zammad/labels/whatsapp)]
- Updated by information gets overwritten by handling of 'Sent' event in a WhatsApp Business ticket [5187](https://github.com/zammad/zammad/issues/5187) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[ticket: actions](https://github.com/zammad/zammad/labels/ticket%3A%20actions)] [[whatsapp](https://github.com/zammad/zammad/labels/whatsapp)]
- Triggers and schedulers continue to send mails, even if an inactive mailbox is used for this purpose [5201](https://github.com/zammad/zammad/issues/5201) [[bug](https://github.com/zammad/zammad/labels/bug)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[scheduler](https://github.com/zammad/zammad/labels/scheduler)]
- "Show Search Details" is not translated on language change [5350](https://github.com/zammad/zammad/issues/5350) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Saved organization selection in ticket create screen is not restored properly [5347](https://github.com/zammad/zammad/issues/5347) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[organization](https://github.com/zammad/zammad/labels/organization)] [[ticket: create](https://github.com/zammad/zammad/labels/ticket%3A%20create)]
- Marking text is not possible anymore with shift+left [5337](https://github.com/zammad/zammad/issues/5337) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Admin + Profile - Translation in French - bad escaping of quote characters [5346](https://github.com/zammad/zammad/issues/5346) [[bug](https://github.com/zammad/zammad/labels/bug)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Changing roles with big group mapping takes very long even if there are no changes [5343](https://github.com/zammad/zammad/issues/5343) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[performance](https://github.com/zammad/zammad/labels/performance)]
- Wrong encoding for ticket state name in ticket overview [5342](https://github.com/zammad/zammad/issues/5342) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
- Password validation errors containing %s are not interpolated [5327](https://github.com/zammad/zammad/issues/5327) [[bug](https://github.com/zammad/zammad/labels/bug)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Inactive customers continue to receive trigger mails when agents update the ticket [4665](https://github.com/zammad/zammad/issues/4665) [[bug](https://github.com/zammad/zammad/labels/bug)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Impossible to remove/change user organization, without clicking in the field first [5339](https://github.com/zammad/zammad/issues/5339) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Several admin permissions cannot be assigned or do not work properly [5328](https://github.com/zammad/zammad/issues/5328) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)]
- Customer is member of Organization and Secondary Organization, tabbing through fields gives error Organization is required [5335](https://github.com/zammad/zammad/issues/5335) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
- ObjectAttribute of type Date is displayed one day off when client computer's timezone changes [4998](https://github.com/zammad/zammad/issues/4998) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Time Accounting: Repeatedly pressing Enter duplicates the article [5326](https://github.com/zammad/zammad/issues/5326) [[bug](https://github.com/zammad/zammad/labels/bug)] [[time accounting](https://github.com/zammad/zammad/labels/time%20accounting)]
- MS365 failing to send articles when mail headers too long [5334](https://github.com/zammad/zammad/issues/5334) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Values can be changed via keyboard tabbing when the field is read-only [5324](https://github.com/zammad/zammad/issues/5324) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
- Import as / email archive import no longer triggered [4010](https://github.com/zammad/zammad/issues/4010) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- packager post install script enforce_redis() regex check fails on valid redis urls [5331](https://github.com/zammad/zammad/issues/5331) [[bug](https://github.com/zammad/zammad/labels/bug)] [[install](https://github.com/zammad/zammad/labels/install)]
- Conflict with the extended follow-up detection - which cannot be deactivated [5286](https://github.com/zammad/zammad/issues/5286) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Content of checklist items breaks strangely [5329](https://github.com/zammad/zammad/issues/5329) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[ticket: checklist](https://github.com/zammad/zammad/labels/ticket%3A%20checklist)]
- Checklist badge counter does not update when linked tickets change state. [5319](https://github.com/zammad/zammad/issues/5319) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Checklist: circle is not aligned correctly on long ticket titles [5321](https://github.com/zammad/zammad/issues/5321) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[ticket: checklist](https://github.com/zammad/zammad/labels/ticket%3A%20checklist)]
- Scrollbars are rendered always in the light mode [5322](https://github.com/zammad/zammad/issues/5322) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[dark mode](https://github.com/zammad/zammad/labels/dark%20mode)]
- Zammad not handling X-Forward-For that is a list [5317](https://github.com/zammad/zammad/issues/5317)
- Several problems with postinstall script's function `enforce_redis ()` [5269](https://github.com/zammad/zammad/issues/5269) [[bug](https://github.com/zammad/zammad/labels/bug)] [[install](https://github.com/zammad/zammad/labels/install)]
- Unprocessible mail output for mails with missing from should have a specific output [4826](https://github.com/zammad/zammad/issues/4826) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Make errors on mail sending failures more readable [5261](https://github.com/zammad/zammad/issues/5261) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
- Read only Ticket attribute can not be copied properly [5172](https://github.com/zammad/zammad/issues/5172) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Created trigger can not be edited [5298](https://github.com/zammad/zammad/issues/5298) [[bug](https://github.com/zammad/zammad/labels/bug)] [[trigger](https://github.com/zammad/zammad/labels/trigger)]
- New keyboard shortcut "t" superseds 'new tab' in some browsers [5312](https://github.com/zammad/zammad/issues/5312) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
- "Same Subject & References" follow-up detection only works with "RE: some subject" but not with "FWD: some subject" [5287](https://github.com/zammad/zammad/issues/5287) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Avatar is not clickable on tablet devices or devices in tablet modus [4745](https://github.com/zammad/zammad/issues/4745) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mobile-native](https://github.com/zammad/zammad/labels/mobile-native)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
- Translation - Not translated string [5202](https://github.com/zammad/zammad/issues/5202) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)] [[good first issue](https://github.com/zammad/zammad/labels/good%20first%20issue)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- "Option" button is labeled "Alt" in shortcut overlay with MacOS [4324](https://github.com/zammad/zammad/issues/4324) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[good first issue](https://github.com/zammad/zammad/labels/good%20first%20issue)]
- Microsoft 365 & Google channel ignore Proxy configuration for token refresh / verify [3839](https://github.com/zammad/zammad/issues/3839) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)]
- Unprocessable mails that also match a `PostmasterFilter` with the action `{"x-zammad-ignore"=>{"value"=>true}` don't get deleted upon reprocessing [5049](https://github.com/zammad/zammad/issues/5049) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)] [[postmaster filter](https://github.com/zammad/zammad/labels/postmaster%20filter)]
- Gitlab/Github UI loses links when they are renamed [5015](https://github.com/zammad/zammad/issues/5015) [[bug](https://github.com/zammad/zammad/labels/bug)] [[integration](https://github.com/zammad/zammad/labels/integration)]
- Translators can destroy the whole UI by wrong format translations [5223](https://github.com/zammad/zammad/issues/5223) [[bug](https://github.com/zammad/zammad/labels/bug)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Popup message is not translated [5299](https://github.com/zammad/zammad/issues/5299) [[bug](https://github.com/zammad/zammad/labels/bug)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Group hierarchy is not shown while creating a new ticket as a customer [5301](https://github.com/zammad/zammad/issues/5301) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket: create](https://github.com/zammad/zammad/labels/ticket%3A%20create)]
- Download from integrated reporting: Missing criteria: last_close_at [5158](https://github.com/zammad/zammad/issues/5158) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
- Placetel Integration does not end the call [5233](https://github.com/zammad/zammad/issues/5233) [[bug](https://github.com/zammad/zammad/labels/bug)]
- UserAgent lib does not return a `response.body` for `Net::HTTPClientError` and `Net::HTTPNotFound`. [5304](https://github.com/zammad/zammad/issues/5304) [[bug](https://github.com/zammad/zammad/labels/bug)] [[core](https://github.com/zammad/zammad/labels/core)]
- Empty SMS are not allowed [5289](https://github.com/zammad/zammad/issues/5289) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)]
- Bad data serialization for JSON database columns [5249](https://github.com/zammad/zammad/issues/5249) [[bug](https://github.com/zammad/zammad/labels/bug)] [[smime](https://github.com/zammad/zammad/labels/smime)]
- LDAP role assignment fails when more than 1500 users are part of an active directory group [3634](https://github.com/zammad/zammad/issues/3634) [[bug](https://github.com/zammad/zammad/labels/bug)] [[LDAP](https://github.com/zammad/zammad/labels/LDAP)]
- It takes two clicks to show all groups when creating a new ticket [5280](https://github.com/zammad/zammad/issues/5280) [[bug](https://github.com/zammad/zammad/labels/bug)] [[permission](https://github.com/zammad/zammad/labels/permission)] [[ticket: create](https://github.com/zammad/zammad/labels/ticket%3A%20create)]
- Untranslated error message in /object_manager [5276](https://github.com/zammad/zammad/issues/5276) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Admin password authentication is not possible when 2FA is enabled [5283](https://github.com/zammad/zammad/issues/5283) [[bug](https://github.com/zammad/zammad/labels/bug)] [[authentication](https://github.com/zammad/zammad/labels/authentication)]
- Knowledge base search result order is wrong with ES backend [5099](https://github.com/zammad/zammad/issues/5099) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)] [[VoteSup](https://github.com/zammad/zammad/labels/VoteSup)]
- heavy group usage freezes app on user update with many ticket creates open [5282](https://github.com/zammad/zammad/issues/5282) [[bug](https://github.com/zammad/zammad/labels/bug)] [[performance](https://github.com/zammad/zammad/labels/performance)] [[ticket: create](https://github.com/zammad/zammad/labels/ticket%3A%20create)]
- Public KB header gets broken if logo is wide [5264](https://github.com/zammad/zammad/issues/5264) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
- SSL verification with SAML fails with valid Let's Encrypt certificates [5106](https://github.com/zammad/zammad/issues/5106) [[bug](https://github.com/zammad/zammad/labels/bug)] [[SAML](https://github.com/zammad/zammad/labels/SAML)]
- Activating preserve symlinks option fails the build every time [5273](https://github.com/zammad/zammad/issues/5273) [[regression](https://github.com/zammad/zammad/labels/regression)]
- Mail filter logs "contains not" instead of "contains" [5271](https://github.com/zammad/zammad/issues/5271) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Inactive Ticket States selectable in Mobile View [5268](https://github.com/zammad/zammad/issues/5268) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)] [[ticket: state](https://github.com/zammad/zammad/labels/ticket%3A%20state)]
- Inline images in outgoing mails break HTML structure [5258](https://github.com/zammad/zammad/issues/5258) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Webhook custom payload parser does not accept digits within placeholders [5265](https://github.com/zammad/zammad/issues/5265) [[bug](https://github.com/zammad/zammad/labels/bug)] [[webhooks](https://github.com/zammad/zammad/labels/webhooks)]
- "last_close_at" is visible in Core Workflows [5245](https://github.com/zammad/zammad/issues/5245) [[bug](https://github.com/zammad/zammad/labels/bug)] [[core workflows](https://github.com/zammad/zammad/labels/core%20workflows)]
- Overview is not refreshing [5260](https://github.com/zammad/zammad/issues/5260) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[overviews](https://github.com/zammad/zammad/labels/overviews)]
- Email notifications sending will be aborted mid-list if SMTP error code is received [2133](https://github.com/zammad/zammad/issues/2133) [[bug](https://github.com/zammad/zammad/labels/bug)] [[notification](https://github.com/zammad/zammad/labels/notification)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Shared drafts are not applied in a consistent manner in the ticket create screen [5244](https://github.com/zammad/zammad/issues/5244) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[draft sharing](https://github.com/zammad/zammad/labels/draft%20sharing)] [[ticket: create](https://github.com/zammad/zammad/labels/ticket%3A%20create)]
- Scheduler - Translation in French - bad escaping of quote characters on one string [5242](https://github.com/zammad/zammad/issues/5242) [[bug](https://github.com/zammad/zammad/labels/bug)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Duplicated article after bulk action was executed for newly created tickets without a browser refresh in between [3568](https://github.com/zammad/zammad/issues/3568) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Bulk action form broken on high resolutions [5217](https://github.com/zammad/zammad/issues/5217) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[overviews](https://github.com/zammad/zammad/labels/overviews)]
- Information about remote images is lost/not displayed in Zammad [5193](https://github.com/zammad/zammad/issues/5193) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Zammads HTML wrapper should not set a semikolon [5234](https://github.com/zammad/zammad/issues/5234) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- Wrong app restart command output [5231](https://github.com/zammad/zammad/issues/5231) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
- After the upgrade to Zammad 6.3, a lot of translations are displayed as "modified" in the new translation dialog [5189](https://github.com/zammad/zammad/issues/5189) [[bug](https://github.com/zammad/zammad/labels/bug)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Notification emails about mails sent from Exchange with inline images are displayed in a compressed long line in e-mail client [5222](https://github.com/zammad/zammad/issues/5222) [[bug](https://github.com/zammad/zammad/labels/bug)] [[notification](https://github.com/zammad/zammad/labels/notification)]
- Configuration screen of triggers do not pop up [5196](https://github.com/zammad/zammad/issues/5196) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[permission](https://github.com/zammad/zammad/labels/permission)] [[calendar](https://github.com/zammad/zammad/labels/calendar)]
- "Article Text as HTML" variable is missing when using "::" in the body of an email in a trigger [5200](https://github.com/zammad/zammad/issues/5200) [[bug](https://github.com/zammad/zammad/labels/bug)] [[text modules](https://github.com/zammad/zammad/labels/text%20modules)] [[variable processing](https://github.com/zammad/zammad/labels/variable%20processing)]
- Translation in French - bad escaping of quote characters on one string [5203](https://github.com/zammad/zammad/issues/5203) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Inconsistent tagging behaviour in ticket create vs. ticket edit [4869](https://github.com/zammad/zammad/issues/4869) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[ticket: create](https://github.com/zammad/zammad/labels/ticket%3A%20create)]
- API 301 redirection when accessing through HTTP [5211](https://github.com/zammad/zammad/issues/5211) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Consistent capitalization for WhatsApp [5210](https://github.com/zammad/zammad/pull/5210)
- Tables (spreadsheets) in articles are cut off in Zammad UI [4960](https://github.com/zammad/zammad/issues/4960) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- External data source search gets stuck when it is added as a condition or an action [5148](https://github.com/zammad/zammad/issues/5148) [[bug](https://github.com/zammad/zammad/labels/bug)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)]
- ".value" in text modules is not working [5116](https://github.com/zammad/zammad/issues/5116) [[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)]
- Transparent image is displayed incorrect [4769](https://github.com/zammad/zammad/issues/4769) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Exchange token refresh still applied even on inactive Exchange integration [4961](https://github.com/zammad/zammad/issues/4961) [[bug](https://github.com/zammad/zammad/labels/bug)] [[integration](https://github.com/zammad/zammad/labels/integration)] [[background worker](https://github.com/zammad/zammad/labels/background%20worker)] [[monitoring](https://github.com/zammad/zammad/labels/monitoring)]
- Copied ticket title text/handle in dark-mode into article is rendered in white color [4944](https://github.com/zammad/zammad/issues/4944) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[text editor](https://github.com/zammad/zammad/labels/text%20editor)]
- Add support for new tech stack on package installation [5190](https://github.com/zammad/zammad/issues/5190) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Size of inline images in mails from Zammad are not shown correctly in Microsoft mail clients [4691](https://github.com/zammad/zammad/issues/4691) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)] [[collaboration](https://github.com/zammad/zammad/labels/collaboration)]
- Special characters in a ticket title are escaped if the title is updated by a Trigger [5108](https://github.com/zammad/zammad/issues/5108) [[bug](https://github.com/zammad/zammad/labels/bug)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[variable processing](https://github.com/zammad/zammad/labels/variable%20processing)]
- Setting datetime attributes with macros is not working [4905](https://github.com/zammad/zammad/issues/4905) [[bug](https://github.com/zammad/zammad/labels/bug)] [[macros](https://github.com/zammad/zammad/labels/macros)]
- Webhook custom payload not support complex object attribute values [5060](https://github.com/zammad/zammad/issues/5060) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)] [[webhooks](https://github.com/zammad/zammad/labels/webhooks)]
- No possibility to export merged tickets in reports [4924](https://github.com/zammad/zammad/issues/4924) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
- Align the mail client behavior for <title> tag with Zammad [4583](https://github.com/zammad/zammad/issues/4583) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
- `Mail::AddressList can not parse` error when S/MIME or PGP integration is active [5178](https://github.com/zammad/zammad/issues/5178) [[bug](https://github.com/zammad/zammad/labels/bug)]
- After Znuny import in Zammad: Users without a valid email address gives an 500 error in Zammad when creating a new mail [5166](https://github.com/zammad/zammad/issues/5166) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)] [[import](https://github.com/zammad/zammad/labels/import)] [[regression](https://github.com/zammad/zammad/labels/regression)]
- Many new tickets for one customer leads to massive requests to ticket_customer endpoint [5173](https://github.com/zammad/zammad/issues/5173) [[bug](https://github.com/zammad/zammad/labels/bug)] [[performance](https://github.com/zammad/zammad/labels/performance)]
- 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)]
- Token without expiration timestamp expires "01/01/1970" [478](https://github.com/zammad/zammad/issues/478) [[bug](https://github.com/zammad/zammad/labels/bug)]
- [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)]
- Settings cache is applied for 24 hours after the last change only [5134](https://github.com/zammad/zammad/issues/5134) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Massenversand send error exception [5120](https://github.com/zammad/zammad/issues/5120) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Wrong Setting value is broadcasted if interpolation is used [5133](https://github.com/zammad/zammad/issues/5133) [[bug](https://github.com/zammad/zammad/labels/bug)]
- 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)]
- Using the greater than symbol in group names displays the hmtl code instead of the symbol [5121](https://github.com/zammad/zammad/issues/5121) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)]
- Translation in French - bad escaping of quote characters on some strings [5095](https://github.com/zammad/zammad/issues/5095) [[bug](https://github.com/zammad/zammad/labels/bug)] [[localization](https://github.com/zammad/zammad/labels/localization)]
- Article notes text is greyed out [5118](https://github.com/zammad/zammad/issues/5118) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
- When an update via API fails, a notification is send to the agents that the change was made [4834](https://github.com/zammad/zammad/issues/4834) [[bug](https://github.com/zammad/zammad/labels/bug)] [[API](https://github.com/zammad/zammad/labels/API)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[notification](https://github.com/zammad/zammad/labels/notification)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[webhooks](https://github.com/zammad/zammad/labels/webhooks)]
- OTRS import: When importing queues with a parent queue to Zammad the migration then stops [5114](https://github.com/zammad/zammad/issues/5114) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[VoteSup](https://github.com/zammad/zammad/labels/VoteSup)]
- Email Channel cannot update XOauth2 token if identical channel exists [5111](https://github.com/zammad/zammad/issues/5111) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)]
- #{user.avatar}-size ignored in mobile-view [5096](https://github.com/zammad/zammad/issues/5096) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[📱mobile-view📱](https://github.com/zammad/zammad/labels/%F0%9F%93%B1mobile-view%F0%9F%93%B1)] [[form](https://github.com/zammad/zammad/labels/form)]

View File

@@ -33,35 +33,35 @@ GEM
simple_po_parser (~> 1.1.6)
aasm (5.5.0)
concurrent-ruby (~> 1.0)
actioncable (7.1.4)
actionpack (= 7.1.4)
activesupport (= 7.1.4)
actioncable (7.1.4.1)
actionpack (= 7.1.4.1)
activesupport (= 7.1.4.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.4)
actionpack (= 7.1.4)
activejob (= 7.1.4)
activerecord (= 7.1.4)
activestorage (= 7.1.4)
activesupport (= 7.1.4)
actionmailbox (7.1.4.1)
actionpack (= 7.1.4.1)
activejob (= 7.1.4.1)
activerecord (= 7.1.4.1)
activestorage (= 7.1.4.1)
activesupport (= 7.1.4.1)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.4)
actionpack (= 7.1.4)
actionview (= 7.1.4)
activejob (= 7.1.4)
activesupport (= 7.1.4)
actionmailer (7.1.4.1)
actionpack (= 7.1.4.1)
actionview (= 7.1.4.1)
activejob (= 7.1.4.1)
activesupport (= 7.1.4.1)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
actionpack (7.1.4)
actionview (= 7.1.4)
activesupport (= 7.1.4)
actionpack (7.1.4.1)
actionview (= 7.1.4.1)
activesupport (= 7.1.4.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
@@ -69,27 +69,27 @@ GEM
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.4)
actionpack (= 7.1.4)
activerecord (= 7.1.4)
activestorage (= 7.1.4)
activesupport (= 7.1.4)
actiontext (7.1.4.1)
actionpack (= 7.1.4.1)
activerecord (= 7.1.4.1)
activestorage (= 7.1.4.1)
activesupport (= 7.1.4.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.4)
activesupport (= 7.1.4)
actionview (7.1.4.1)
activesupport (= 7.1.4.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.1.4)
activesupport (= 7.1.4)
activejob (7.1.4.1)
activesupport (= 7.1.4.1)
globalid (>= 0.3.6)
activemodel (7.1.4)
activesupport (= 7.1.4)
activerecord (7.1.4)
activemodel (= 7.1.4)
activesupport (= 7.1.4)
activemodel (7.1.4.1)
activesupport (= 7.1.4.1)
activerecord (7.1.4.1)
activemodel (= 7.1.4.1)
activesupport (= 7.1.4.1)
timeout (>= 0.4.0)
activerecord-import (1.8.1)
activerecord (>= 4.2)
@@ -100,13 +100,13 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 2.0.8, < 4)
railties (>= 6.1)
activestorage (7.1.4)
actionpack (= 7.1.4)
activejob (= 7.1.4)
activerecord (= 7.1.4)
activesupport (= 7.1.4)
activestorage (7.1.4.1)
actionpack (= 7.1.4.1)
activejob (= 7.1.4.1)
activerecord (= 7.1.4.1)
activesupport (= 7.1.4.1)
marcel (~> 1.0)
activesupport (7.1.4)
activesupport (7.1.4.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -315,7 +315,7 @@ GEM
iniparse (1.5.0)
interception (0.5)
io-console (0.7.2)
irb (1.14.0)
irb (1.14.1)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
@@ -376,7 +376,7 @@ GEM
time
net-http (0.4.1)
uri
net-imap (0.4.16)
net-imap (0.4.17)
date
net-protocol
net-ldap (0.19.0)
@@ -495,7 +495,7 @@ GEM
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
racc (1.8.1)
rack (2.2.9)
rack (2.2.10)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-protection (3.2.0)
@@ -510,20 +510,20 @@ GEM
rackup (1.0.0)
rack (< 3)
webrick
rails (7.1.4)
actioncable (= 7.1.4)
actionmailbox (= 7.1.4)
actionmailer (= 7.1.4)
actionpack (= 7.1.4)
actiontext (= 7.1.4)
actionview (= 7.1.4)
activejob (= 7.1.4)
activemodel (= 7.1.4)
activerecord (= 7.1.4)
activestorage (= 7.1.4)
activesupport (= 7.1.4)
rails (7.1.4.1)
actioncable (= 7.1.4.1)
actionmailbox (= 7.1.4.1)
actionmailer (= 7.1.4.1)
actionpack (= 7.1.4.1)
actiontext (= 7.1.4.1)
actionview (= 7.1.4.1)
activejob (= 7.1.4.1)
activemodel (= 7.1.4.1)
activerecord (= 7.1.4.1)
activestorage (= 7.1.4.1)
activesupport (= 7.1.4.1)
bundler (>= 1.15.0)
railties (= 7.1.4)
railties (= 7.1.4.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -535,9 +535,9 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.1.4)
actionpack (= 7.1.4)
activesupport (= 7.1.4)
railties (7.1.4.1)
actionpack (= 7.1.4.1)
activesupport (= 7.1.4.1)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -555,7 +555,7 @@ GEM
regexp_parser (2.9.2)
reline (0.5.10)
io-console (~> 0.5)
rexml (3.3.8)
rexml (3.3.9)
rotp (6.3.0)
rspec-core (3.13.1)
rspec-support (~> 3.13.0)

View File

@@ -764,6 +764,11 @@
"url": "",
"license": "MIT"
},
"status-report.svg": {
"author": "Zammad",
"url": "",
"license": "MIT"
},
"stopwatch.svg": {
"author": "Zammad",
"url": "",
@@ -874,4 +879,4 @@
"url": "",
"license": "MIT"
}
}
}

View File

@@ -11,10 +11,10 @@ You're going to love Zammad!
## Status
- Toolchain: ![docker-release workflow](https://github.com/zammad/zammad/workflows/docker-release/badge.svg) ![build-docker-image workflow](https://github.com/zammad/zammad/workflows/build-docker-image/badge.svg) [![documentation status](https://readthedocs.org/projects/zammad/badge/)](https://docs.zammad.org)
- Toolchain: [![CI](https://github.com/zammad/zammad/workflows/CI/badge.svg)](https://github.com/zammad/zammad/actions/workflows/ci.yaml) [![docker-release workflow](https://github.com/zammad/zammad/workflows/docker-release/badge.svg)](https://github.com/zammad/zammad/actions/workflows/docker-release.yaml) [![documentation status](https://readthedocs.org/projects/zammad/badge/)](https://docs.zammad.org)
- Docker container images: [![Docker images for Zammad](https://img.shields.io/badge/version-stable-blue.svg)](https://hub.docker.com/r/zammad/zammad-docker-compose) [![Dockerhub Pulls](https://badgen.net/docker/pulls/zammad/zammad-docker-compose?icon=docker&label=pulls)](https://hub.docker.com/r/zammad/zammad-docker-compose/)
- Helm chart for Kubernetes: [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/zammad)](https://artifacthub.io/packages/helm/zammad/zammad) [![Release downloads](https://img.shields.io/github/downloads/zammad/zammad-helm/total.svg)](https://github.com/zammad/zammad-helm/releases)
- Download DEB/RPM: [![binary packages for Zammad stable](https://img.shields.io/badge/Branch-stable-blue.svg)](https://packager.io/gh/zammad/zammad/refs/stable) [![binary packages for Zammad develop](https://img.shields.io/badge/Branch-develop-lightgrey.svg)](https://packager.io/gh/zammad/zammad/refs/develop)
- Docker container images: [![Docker images for Zammad](https://img.shields.io/badge/version-stable-blue.svg)](https://hub.docker.com/r/zammad/zammad-docker-compose)
- Helm chart for Kubernetes: [![Zammad Helm chart for Kubernetes](https://img.shields.io/badge/version-stable-blue.svg)](https://artifacthub.io/packages/helm/zammad/zammad)
- License: [![AGPL license](https://img.shields.io/badge/License-AGPL%203.0-brightgreen.svg)](https://github.com/zammad/zammad/blob/develop/LICENSE)
## Installing & Getting Started

View File

@@ -4,8 +4,8 @@
| Version | Supported |
| -------- | ------------------ |
| 6.3.x | :white_check_mark: |
| <= 6.2.x | :x: |
| 6.4.x | :white_check_mark: |
| <= 6.3.x | :x: |
## Reporting a Vulnerability

View File

@@ -1 +1 @@
6.4.x
6.4.0

View File

@@ -964,7 +964,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
link = App.Utils.replaceTags(params.data_option?.linktemplate, objects)
resultLinkIcon = $('<svg class="icon icon-external"><use xlink:href="assets/images/icons.svg#icon-external" /></svg>')
resultLinkIcon = $(App.Utils.icon('external'))
resultLinkAnchor = $('<a />')
.addClass('settings-list-control')
.attr('href', link)

View File

@@ -54,8 +54,8 @@ class App.UiElement.richtext.additions.RichTextToolPopup extends App.ControllerF
@delegate?.getAjaxAttributes?(field, attributes)
onClear: (e) =>
e.preventDefault()
e.stopPropagation()
e?.preventDefault()
e?.stopPropagation()
@clear()

View File

@@ -3,10 +3,22 @@ class App.UiElement.richtext.additions.RichTextToolPopupImage extends App.UiElem
labelExisting: 'Replace'
apply: (callback) ->
@el.find('btn--create').attr('disabled', true)
@el.find('.btn--create').attr('disabled', true)
file = @el.find('input')[0].files[0]
fileSizeInMb = file.size/1024/1024
# The browser may fail while reading too large files as data URL.
# Here we introduce a safe limit check in order to prevent silent errors.
if fileSizeInMb > 25
console.error('App.UiElement.richtext.additions.RichTextToolPopupImage', 'image file size too large', fileSizeInMb, 'in mb')
@onClear()
new App.ControllerErrorModal(
message: __('Image file size is too large, please try inserting a smaller file.')
)
return
reader = new FileReader()
reader.addEventListener('load', =>
@@ -16,40 +28,48 @@ class App.UiElement.richtext.additions.RichTextToolPopupImage extends App.UiElem
reader.readAsDataURL(file)
applyOnto: (dom, base64) ->
applyOnto: (dom, base64, width) ->
dom.attr('src', base64)
dom.attr('width', width)
insertImage: (base64) ->
textEditor = $(@event.currentTarget).closest('.richtext.form-control').find('[contenteditable]')
switch @selection.type
when 'existing'
@applyOnto(@selection.dom, base64)
when 'append'
newElem = $('<img>')[0]
newElem.src = base64
newElem.style = 'width: 1000px; max-width: 100%;'
@selection.dom.append(newElem)
when 'caret'
newElem = $('<img>')
newElem.attr('src', base64)
newElem.attr('style', 'width: 1000px; max-width: 100%;')
insert = (dataUrl, width, height, isResized) =>
switch @selection.type
when 'existing'
@applyOnto(@selection.dom, dataUrl, width)
when 'append'
newElem = $('<img>')[0]
newElem.src = dataUrl
newElem.style = 'width: ' + width + 'px; max-width: 100%;'
@selection.dom.append(newElem)
when 'caret'
newElem = $('<img>')
newElem.attr('src', dataUrl)
newElem.attr('style', 'width: ' + width + 'px; max-width: 100%;')
surroundingDom = @selection.dom[0]
surroundingDom = @selection.dom[0]
if surroundingDom instanceof Text
@selection.dom[0].splitText(@selection.offset)
if surroundingDom instanceof Text
@selection.dom[0].splitText(@selection.offset)
newElem.insertAfter(@selection.dom)
when 'range'
newElem = $('<img>')
newElem.attr('src', base64)
newElem.attr('style', 'width: 1000px; max-width: 100%;')
newElem.insertAfter(@selection.dom)
when 'range'
newElem = $('<img>')
newElem.attr('src', dataUrl)
newElem.attr('style', 'width: ' + width + 'px; max-width: 100%;')
placeholder = textEditor.find('span.highlight-emulator')
placeholder = textEditor.find('span.highlight-emulator')
placeholder.empty()
placeholder.append(newElem)
placeholder.empty()
placeholder.append(newElem)
App.ImageService.resize(base64, 1200, 'auto', 2, @getImageType(base64), 'auto', insert)
getImageType: (base64) ->
match = base64.match(/^data:(image\/\w+);base64,/)
if match then match[1] else null
clear: ->
switch @selection.type

View File

@@ -28,7 +28,7 @@ class App.KnowledgeBaseAddForm extends App.ControllerModal
params = @formController.paramsForSaving()
params.translations_attributes[0].content_attributes = { body: '' }
@parentController.coordinator.saveChanges(@object, params, @)
@parentController.coordinator.saveChanges(@object, params, @, e)
showAlert: (text) ->
@formController?.showAlert(text)

View File

@@ -103,7 +103,7 @@ class App.KnowledgeBaseContentController extends App.Controller
additional_action = $(e.currentTarget).data('id')
if @remoteDidntChangeSinceStart()
@parentController.coordinator.saveChanges(@object, paramsForSaving, @, additional_action)
@parentController.coordinator.saveChanges(@object, paramsForSaving, @, e, additional_action)
return
new App.ControllerConfirm(
@@ -111,7 +111,7 @@ class App.KnowledgeBaseContentController extends App.Controller
message: __('Your changes may override someone else\'s changes. Do you really want to save?')
buttonClass: 'btn--danger'
callback: =>
@parentController.coordinator.saveChanges(@object, paramsForSaving, @)
@parentController.coordinator.saveChanges(@object, paramsForSaving, @, e)
)
missingTranslation: ->

View File

@@ -28,8 +28,20 @@ class App.KnowledgeBaseEditorCoordinator
else
object.url()
saveChanges: (object, data, formController, action) ->
App.ControllerForm.disable(formController.form)
submitDisable: (e) =>
if e
App.ControllerForm.disable(e)
return
App.ControllerForm.disable(@$('.js-submitContainer'), 'button')
submitEnable: (e) =>
if e
App.ControllerForm.enable(e)
return
App.ControllerForm.enable(@$('.js-submitContainer'), 'button')
saveChanges: (object, data, formController, e, action) ->
@submitDisable(e)
url = @urlFor(object) + '?full=true'
@@ -40,11 +52,12 @@ class App.KnowledgeBaseEditorCoordinator
type: object.writeMethod()
data: JSON.stringify(data)
url: url
success: (data) ->
success: (data) =>
App.Collection.loadAssets(data.assets)
formController.didSaveCallback(data)
error: (xhr) ->
@submitEnable(e)
error: (xhr) =>
data = JSON.parse(xhr.responseText)
App.ControllerForm.enable(formController.form)
formController.showAlert(data.error || __('The changes could not be saved.'))
@submitEnable(e)
)

View File

@@ -199,6 +199,8 @@ class App.TicketZoom extends App.Controller
if beforeRenderDone
App.Event.trigger('ui::ticket::load', data)
App.Event.trigger('ui::ticket::all::loaded', data)
meta: =>
# default attributes
@@ -974,7 +976,8 @@ class App.TicketZoom extends App.Controller
isPendingClose = ticketState.state_type.name is 'pending action' && App.TicketState.find(ticketState.next_state_id).state_type.name is 'closed'
return @submitTimeAccounting(e, ticket, macro, editContollerForm) if !isClosed && !isPendingClose
if !ticket.checklist_incomplete
checklist = App.Checklist.find ticket.checklist_id
if !checklist || checklist.open_items().length is 0
return @submitTimeAccounting(e, ticket, macro, editContollerForm)
new App.TicketZoomChecklistModal(

View File

@@ -11,25 +11,37 @@ class App.TicketZoomMeta extends App.ControllerObserver
created_at: true
updated_at: true
escalation_at: true
checklist_total: true
checklist_incomplete: true
render: (ticket) =>
if App.Config.get('checklist')
checklistState = App.Checklist.calculateState(ticket)
checklistReferences = App.Checklist.calculateReferences(ticket)
@last_ticket = ticket
if App.Config.get('checklist')
@checklistState = App.Checklist.calculateState(ticket)
@checklistReferences = App.Checklist.calculateReferences(ticket)
@html App.view('ticket_zoom/meta')(
ticket: ticket
isCustomer: ticket.currentView() is 'customer'
checklistState: checklistState
checklistReferences: checklistReferences
checklistState: @checklistState
checklistReferences: @checklistReferences
)
@$('.ticket-references-popover').data('tickets', checklistReferences)
@$('.ticket-references-popover').data('tickets', @checklistReferences)
@renderPopovers()
@controllerUnbind('ui::ticket::all::loaded', @updateOnTicketAllLoaded)
@controllerBind('ui::ticket::all::loaded', @updateOnTicketAllLoaded)
updateOnTicketAllLoaded: (data) =>
return if data.ticket_id.toString() isnt @last_ticket.id.toString()
state = App.Checklist.calculateState(@last_ticket)
references = App.Checklist.calculateReferences(@last_ticket)
return if @checklistState == state && @checklistReferences == references
@render(@last_ticket)
copyTicketNumber: =>
text = @el.find('.js-objectNumber').first().data('number') || ''
if text

View File

@@ -7,7 +7,6 @@ class SidebarChecklist extends App.Controller
release: =>
super
@unsubscribe()
@clearWidget()
sidebarActions: =>
result = []
@@ -35,8 +34,6 @@ class SidebarChecklist extends App.Controller
callback: =>
@checklist.destroy(
done: =>
@clearWidget()
@widget = new App.SidebarChecklistStart(el: @elSidebar, parentVC: @)
)
)
@@ -57,47 +54,18 @@ class SidebarChecklist extends App.Controller
}
subscribe: =>
@subscriptions ||= []
@unsubscribe()
# ticket subscriptions
sid = App.Ticket.subscribeItem(
@ticket.id,
(ticket) =>
@delay =>
@badgeRenderLocal()
return if ticket.updated_by_id is App.Session.get().id
@shown() if !@widget?.actionController
)
@subscriptions.push(
object: 'Ticket',
id: @ticket.id,
sid: sid,
)
@controllerBind('ui::ticket::all::loaded', @updateOnTicketAllLoaded)
# Exit early and subscribe only to the ticket when sidebar is not opened
return if !(@widget instanceof App.SidebarChecklistShow)
# Keep going and subscribe to checklist items and tickets in there when sidebar is opened
# checklist subscriptions
checklist = App.Checklist.findByAttribute('ticket_id', @ticket.id)
checklist = App.Checklist.find @ticket.checklist_id
return if !checklist
sid = App.Checklist.subscribeItem(
checklist.id,
(item) =>
return if item.updated_by_id is App.Session.get().id
return if @widget?.actionController
@shown()
)
@subscriptions.push(
object: 'Checklist',
id: checklist.id,
sid: sid,
)
for id in checklist.item_ids
item = App.ChecklistItem.find(id)
continue if !item
@@ -106,8 +74,7 @@ class SidebarChecklist extends App.Controller
sid = App.Ticket.subscribeItem(
item.ticket_id,
(item) =>
return if @widget?.actionController
@shown()
@renderWidget()
)
@subscriptions.push(
object: 'Ticket',
@@ -123,6 +90,14 @@ class SidebarChecklist extends App.Controller
@subscriptions = []
@controllerUnbind('ui::ticket::all::loaded', @updateOnTicketAllLoaded)
updateOnTicketAllLoaded: (data) =>
return if data.ticket_id.toString() isnt @ticket.id.toString()
@badgeRenderLocal()
@renderWidget()
showChecklist: (el) =>
@elSidebar = el
@startLoading()
@@ -130,43 +105,50 @@ class SidebarChecklist extends App.Controller
shown: (enterEditMode = false) =>
@startLoading()
if !@ticket.checklist_id
@renderWidget()
return
@ajax(
id: "checklist_ticket#{@ticket.id}"
type: 'GET'
url: "#{@apiPath}/checklists/by_ticket/#{@ticket.id}"
url: "#{@apiPath}/checklists/#{@ticket.checklist_id}"
processData: true
success: (data, status, xhr) =>
@clearWidget()
@stopLoading()
if data.id
App.Collection.loadAssets(data.assets)
App.Collection.loadAssets(data.assets)
@checklist = App.Checklist.find(data.id)
@widget = new App.SidebarChecklistShow(el: @elSidebar, parentVC: @, checklist: @checklist, readOnly: !@changeable, enterEditMode: enterEditMode)
else
@widget = new App.SidebarChecklistStart(el: @elSidebar, parentVC: @, readOnly: !@changeable)
@subscribe()
@renderActions()
@badgeRenderLocal()
@delay => @renderWidget(enterEditMode)
)
clearWidget: =>
@widget?.el.empty()
@widget?.releaseController()
renderWidget: (enterEditMode = false) =>
@checklist = App.Checklist.find(@ticket.checklist_id)
if @checklist
if @widget instanceof App.SidebarChecklistShow and @checklist?.id is @widget.checklist?.id
@widget.checklist = @checklist
@widget.renderTable()
else
@widget?.releaseController()
@widget = new App.SidebarChecklistShow(el: @elSidebar, parentVC: @, checklist: @checklist, readOnly: !@changeable, enterEditMode: enterEditMode)
else if !(@widget instanceof App.SidebarChecklistStart)
@widget?.releaseController()
@widget = new App.SidebarChecklistStart(el: @elSidebar, parentVC: @, readOnly: !@changeable)
@subscribe()
@checklist = undefined
@renderActions()
@badgeRenderLocal()
metaBadge: =>
checklist = App.Checklist.find @ticket.checklist_id
{
name: 'checklist'
icon: 'checklist'
counterPossible: true
counter: @ticket.checklist_incomplete
counter: checklist?.open_items().length
}
badgeRender: (el) =>

View File

@@ -0,0 +1,85 @@
class App.SidebarChecklistItemEdit extends App.Controller
elements:
'.js-input': 'input'
events:
'click .js-cancel': 'onCancel'
'click .js-confirm': 'onConfirm'
'blur .js-input': 'onBlur'
'keyup #checklistItemEditText': 'onKeyUp'
constructor: ->
super
@render()
releaseController: =>
super
@el.removeClass('edit-widget-active')
@el.closest('tr').find('.dropdown').removeClass('hide')
if @el.closest('tr').find('.dropdown--actions').hasClass('open')
@el.closest('tr').find('.js-table-action-menu').dropdown('toggle')
render: =>
@html App.view('ticket_zoom/sidebar_checklist/item_edit')(value: @object()?.text)
@el.addClass('edit-widget-active')
@el.closest('tr').find('.dropdown').addClass('hide')
@input.focus().val('').val(@object()?.text)
object: =>
App.ChecklistItem.find(@id)
onBlur: (e) =>
return if @el.closest('tr').data('skip-blur')
if $(e.originalEvent.relatedTarget).hasClass('js-cancel')
@onCancel(e)
return
@onConfirm(e)
onCancel: (e) =>
@preventDefaultAndStopPropagation(e)
@releaseController()
row = @parentVC.table.find("tbody tr[data-id=#{@id}]")
@el.text(row.data('text-value'))
onConfirm: (e) =>
@preventDefaultAndStopPropagation(e)
newValue = @input.val()
# Prevent AJAX if user has not changed the value
row = @parentVC.table.find("tbody tr[data-id=#{@id}]")
originalValue = row.data('text-value')
if originalValue == newValue
@releaseController()
@el.text(originalValue)
return
item = @object()
item.text = newValue
@parentVC.setDisabled(e.target)
item.save(
done: =>
@releaseController()
@parentVC.updateDisplayValue(item)
@parentVC.setEnabled(e.target)
fail: (settings, details) =>
@notify(
type: 'error'
msg: App.i18n.translateContent(details.error)
)
@releaseController()
@parentVC.setEnabled(e.target)
@parentVC.renderTable()
)
onKeyUp: (e) =>
switch e.key
when 'Enter' then @onConfirm(e)
when 'Escape' then @onCancel(e)

View File

@@ -0,0 +1,75 @@
class App.SidebarChecklistRename extends App.Controller
elements:
'.js-input': 'input'
events:
'click .js-cancel': 'onCancel'
'click .js-confirm': 'onConfirm'
'blur .js-input': 'onBlur'
'keyup #checklistTitleEditText': 'onKeyUp'
constructor: ->
super
@render()
releaseController: =>
super
@parentVC.isRenamingChecklist = false
render: =>
@html App.view('ticket_zoom/sidebar_checklist/title_edit')(
value: @object()?.name
ticketNumber: @parentVC.parentVC.ticket.number
)
@input.focus().val('').val(@object()?.name)
object: =>
@parentVC.checklist
onBlur: (e) =>
if $(e.originalEvent.relatedTarget).hasClass('js-cancel')
@onCancel(e)
return
@onConfirm(e)
onCancel: (e) =>
@preventDefaultAndStopPropagation(e)
@releaseController()
@parentVC.updateChecklistTitle()
onConfirm: (e) =>
@preventDefaultAndStopPropagation(e)
newValue = @input.val()
checklist = @object()
if newValue == checklist.name
@releaseController()
@parentVC.updateChecklistTitle()
return
@parentVC.setDisabled(e.target)
checklist.name = newValue
checklist.save(
done: =>
@parentVC.setEnabled(e.target)
@releaseController()
@parentVC.updateChecklistTitle()
fail: (settings, details) =>
@notify(
type: 'error'
msg: App.i18n.translateContent(details.error)
)
@parentVC.setEnabled(e.target)
@releaseController()
@parentVC.updateChecklistTitle()
)
onKeyUp: (e) =>
switch e.key
when 'Enter' then @onConfirm(e)
when 'Escape' then @onCancel(e)

View File

@@ -0,0 +1,481 @@
class App.SidebarChecklistShow extends App.Controller
events:
'click .js-reorder': 'onReorder'
'click .js-add': 'onAdd'
'click .js-save-order': 'onSaveOrder'
'click .js-reset-order': 'onResetOrder'
'click .js-action': 'onAction'
'change .js-checkbox': 'onCheckboxClick'
'click .js-title': 'onTitleChange'
'click .js-checklist-item-edit': 'onEntryTextClicked'
elements:
'.js-reorder': 'reorderButton'
'.js-add': 'addButton'
'.js-save-order': 'saveOrderButton'
'.js-reset-order': 'resetOrderButton'
'table': 'table'
constructor: ->
super
@render()
render: ->
@html App.view('ticket_zoom/sidebar_checklist/show')(
checklistTitle: @checklistTitle()
readOnly: @readOnly
)
@renderTable()
checklistTitle: =>
@checklist.name || App.i18n.translateInline('%s Checklist', App.Config.get('ticket_hook') + @parentVC.ticket.number)
onReorder: (e) =>
@preventDefaultAndStopPropagation(e)
@actionController?.releaseController()
@actionController = new ChecklistReorder(parentVC: @)
setDisabled: (node) ->
$(node)
.closest('tr')
.attr('disabled', true)
.addClass('u-unclickable u-low-opacity')
setEnabled: (node) ->
$(node)
.closest('tr')
.attr('disabled', false)
.removeClass('u-unclickable u-low-opacity')
if @actionController instanceof ChecklistReorder
@toggleReorder(true)
onAdd: (e) =>
addButton = e.target.closest('button')
$(addButton).attr('disabled', true)
item = new App.ChecklistItem
item.checklist_id = @checklist.id
item.text = ''
self = @
item.save(
done: (data) ->
self.addNewItem(@)
$(addButton).attr('disabled', false)
fail: (settings, details) =>
$(addButton).attr('disabled', false)
@notify(
type: 'error'
msg: App.i18n.translateContent(details.error)
)
@renderTable()
)
addNewItem: (item) ->
row = $(App.view('ticket_zoom/sidebar_checklist/show_row')(
object: item
))
@checklist.sorted_item_ids.push item.id.toString()
@moveOrInsertRow(item.id.toString(), row[0])
@updateDisplayValue(item)
row.data('text-value', '')
cell = row.find('.checklistItemValue')[0]
@activateItemEditMode(cell, row, item.id)
@table.find('.checklistShowRowContentCellNoItems').remove()
onCheckboxClick: (e) =>
upcomingState = e.currentTarget.checked
id = parseInt(e.currentTarget.value)
@updateChecklistItem(id, upcomingState, e.currentTarget)
onCheckOrUncheck: (e) =>
@preventDefaultAndStopPropagation(e)
row = $(e.currentTarget).closest('tr')
id = row.data('id')
checkbox = row.find('.js-checkbox')[0]
upcomingState = !checkbox.checked
@updateChecklistItem(id, upcomingState, checkbox)
updateChecklistItem: (id, upcomingState, checkboxElem) =>
item = App.ChecklistItem.find(id)
item.checked = upcomingState
$(checkboxElem)
.closest('tr')
.find('.checkbox-replacement-readonly')
.html(App.Utils.icon(if upcomingState then 'checkbox-checked-readonly' else 'checkbox-readonly'))
@setDisabled(checkboxElem)
item.save(
done: =>
@setEnabled(checkboxElem)
fail: =>
@setEnabled(checkboxElem)
item.checked = !upcomingState
@renderTable()
)
onSaveOrder: (e) =>
saveButton = e.target.closest('button')
cancelButton = $(saveButton).prev()
checklistTable = $(document).find('.checklistShow tbody')
$(saveButton).attr('disabled', true)
$(cancelButton).attr('disabled', true)
$(checklistTable).addClass('u-unclickable u-low-opacity')
sorted_item_ids = @table.find('tbody tr').toArray().map (elem) -> elem.dataset.id
item = @checklist
item.sorted_item_ids = sorted_item_ids
item.save(
done: (data) =>
@actionController?.completed()
$(saveButton).attr('disabled', false)
$(cancelButton).attr('disabled', false)
$(checklistTable).removeClass('u-unclickable u-low-opacity')
fail: =>
@notify(
type: 'error'
msg: App.i18n.translateInline('Failed to save the order of the checklist items. Please try again.')
)
$(cancelButton).attr('disabled', false)
$(saveButton).attr('disabled', false)
$(checklistTable).removeClass('u-unclickable u-low-opacity')
)
onResetOrder: (e) =>
@actionController?.releaseController()
onAction: (e) =>
e.stopPropagation()
dropdown = $(e.currentTarget).closest('td').find('.js-table-action-menu')
dropdown.dropdown('toggle')
dropdown
.off('click.dropdown')
.on('click.dropdown', '[data-table-action=delete]', @onDeleteChecklistItem)
.on('click.dropdown', '[data-table-action=edit]', @onEditChecklistItem)
.on('click.dropdown', '[data-table-action=check]', @onCheckOrUncheck)
onTitleChange: (e) =>
return if e?.target && $(e.target).closest('th').find('.js-input').length
@preventDefaultAndStopPropagation(e)
# Close any open dropdowns
@el.find('.dropdown--actions.open').dropdown('toggle')
if e
elem = e.currentTarget
else
elem = @el.find('.js-title')[0]
@isRenamingChecklist = new App.SidebarChecklistRename(el: elem, parentVC: @, originalValue: @checklistTitle())
onEntryTextClicked: (e) =>
return if $(e.target).closest('td').find('.js-input').length
# skip on link openings
return if e.target.tagName is 'A'
@preventDefaultAndStopPropagation(e)
@onEditChecklistItem(e)
toggleSortability: (isSorting, disablingCommand = 'disable') =>
@table.find('tbody').sortable(if isSorting then 'enable' else disablingCommand)
toggleReorder: (isReordering, disablingCommand = 'disable') =>
@toggleSortability(isReordering, disablingCommand)
@table.find('.draggable').toggleClass('hide', !isReordering)
@table.find('.checkbox-replacement').toggleClass('hide', isReordering)
@table.find('.checkbox-replacement-readonly').toggleClass('hide', !isReordering)
@table.find('.dropdown').toggleClass('hide', isReordering)
@table.find('.checklistItemValue').toggleClass('js-checklist-item-edit u-clickable', !isReordering && !@readonly)
@reorderButton.toggleClass('hide', isReordering)
@addButton.toggleClass('hide', isReordering)
@saveOrderButton.toggleClass('hide', !isReordering)
@resetOrderButton.toggleClass('hide', !isReordering)
onEditChecklistItem: (e) =>
@preventDefaultAndStopPropagation(e)
row = $(e.currentTarget).closest('tr')
id = row.data('id')
cell = row.find('.checklistItemValue')[0]
@activateItemEditMode(cell, row, id)
onDeleteChecklistItem: (e) =>
@preventDefaultAndStopPropagation(e)
row = $(e.currentTarget).closest('tr')
id = row.data('id')
dropdown = $(e.currentTarget).closest('td').find('.js-table-action-menu')
dropdown.dropdown('toggle')
item = App.ChecklistItem.find(id)
deleteCallback = =>
@setDisabled(e.currentTarget)
item.destroy(
done: =>
@table
.find("tbody tr[data-id='#{id}']")
.remove()
if !@table.find('tbody tr[data-id]').length
@renderEmpty()
fail: =>
@setEnabled(e.currentTarget)
)
# Skip confirmation dialog if the item has no text.
if _.isEmpty(item.text)
deleteCallback()
return
new ChecklistItemRemoveModal(
item: item
container: @el.closest('.content')
callback: deleteCallback
)
activateItemEditMode: (cell, row, id) =>
# @actionController?.releaseController()
new App.SidebarChecklistItemEdit(
el: cell
parentVC: @
id: id
)
renderEmpty: =>
@table
.find('tbody')
.html(App.view('ticket_zoom/sidebar_checklist/show_no_items')())
renderTable: ->
@updateChecklistTitle()
isReorderingInProgress = @actionController instanceof ChecklistReorder
if !isReorderingInProgress
@reorderButton.toggleClass('hide', @checklist.sorted_item_ids.length < 2)
@parentVC.badgeRenderLocal()
if !@checklist.sorted_item_ids.length
@renderEmpty()
return
@table.find('.checklistShowRowContentCellNoItems').remove()
@cleanUpDeleted()
if!isReorderingInProgress
@renderSorting()
@renderDisplayValues()
@renderSortable()
@renderEditMode()
if @actionController instanceof ChecklistReorder
@toggleReorder(true)
updateChecklistTitle: ->
return if @isRenamingChecklist
@table.find('th').html(@checklistTitle())
renderSorting: =>
for id, current_index in @checklist.sorted_item_ids
@renderSortingSingle(id, current_index)
renderSortingSingle: (id, current_index) =>
row = @table.find("tbody tr[data-id=#{id}]")
object = App.ChecklistItem.find(id)
if row.length and current_index != row.index().toString()
@moveOrInsertRow(id, row)
return
row = $(App.view('ticket_zoom/sidebar_checklist/show_row')(
object: object
))
@updateDisplayValue(object)
@moveOrInsertRow(id, row[0])
renderDisplayValues: =>
for id in @checklist.sorted_item_ids
object = App.ChecklistItem.find(id)
if !object
continue
row = @table.find("tbody tr[data-id=#{id}]")
# Thisi s used for initial filling of an empty display value
# This also works if an old checklist item is converted to a ticket reference.
# The ticket reference cannot be converted back to a text item so opposite conversion is not needed.
if object.ticket_id || row.find('.checklistShowRowDisplayValue').is(':empty')
@updateDisplayValue(object)
@setEnabled(row)
else
row
.find('input.js-checkbox')
.prop('checked', object.checked)
row
.find('.checkbox-replacement-readonly')
.html(App.Utils.icon(if object.checked then 'checkbox-checked-readonly' else 'checkbox-readonly'))
@applyTextValue(object, row)
# row
# .find('li[data-table-action=check],li[data-table-action=edit]')
# .toggleClass('hide', !!object.ticket_id)
applyTextValue: (object, row, displayValue) =>
text = if object.text
App.Utils.linkify(object.text)
else if @readonly
'-'
return if text == row.data('text-value')
row.data('text-value', text)
return if row.find('.checklistItemEdit').length
row.find('.checklistItemValue').html(text)
@setEnabled(row)
updateDisplayValue: (object) =>
row = @table.find("tbody tr[data-id=#{object.id}]")
if object.ticket_id
ticket = App.Ticket.find(object.ticket_id)
ticketAccess = if ticket then ticket.userGroupAccess('read') else false
displayValue = App.view('ticket_zoom/sidebar_checklist/show_row_display_value')(
object: object
ticket: ticket
ticketAccess: ticketAccess
readOnly: @readOnly
)
row
.find('li[data-table-action=check],li[data-table-action=edit]')
.toggleClass('hide', !!object.ticket_id)
row
.find('.checklistShowRowDisplayValue')
.html(displayValue)
@applyTextValue(object, row)
moveOrInsertRow: (id, row) =>
target_index = @checklist.sorted_item_ids.indexOf(id)
is_focused = $(row).find('.js-input').is(':focus')
if is_focused
$(row).data('skip-blur', true)
if target_index == 0
@table
.find('tbody')
.prepend(row)
else
preceding_id = @checklist.sorted_item_ids[target_index - 1]
@table
.find("tbody tr[data-id=#{preceding_id}]")
.after(row)
if is_focused
$(row).find('.js-input').focus()
$(row).data('skip-blur', false)
cleanUpDeleted: =>
rendered_ids = @table
.find('tbody tr')
.toArray()
.map (el) -> $(el).data('id')?.toString()
for id in _.difference(rendered_ids, @checklist.sorted_item_ids)
@table
.find("tbody tr[data-id=#{id}]")
.remove()
renderEditMode: =>
if @enterEditMode
@enterEditMode = undefined
@enterEditModeId = @table.find('tbody tr:last-of-type').data('id')
if @enterEditModeId
cell = @table.find("tbody tr[data-id='" + @enterEditModeId + "']").find('.checklistItemValue')[0]
row = $(cell).closest('tr')
return if !row.length
@enterEditModeId = undefined
@activateItemEditMode(cell, row, row.data('id'))
renderSortable: =>
dndOptions =
tolerance: 'pointer'
distance: 15
opacity: 0.6
forcePlaceholderSize: true
items: 'tr'
@table.find('tbody').sortable(dndOptions)
@table.find('tbody').sortable('disable')
class ChecklistReorder extends App.Controller
constructor: ->
super
@render()
releaseController: =>
@parentVC.toggleReorder(false, 'cancel')
@parentVC.actionController = undefined
@parentVC.renderTable()
completed: =>
@parentVC.toggleReorder(false)
@parentVC.actionController = undefined
render: =>
@parentVC.toggleReorder(true)
class ChecklistItemRemoveModal extends App.ControllerGenericDestroyConfirm
onSubmit: =>
@close()
@callback()

View File

@@ -16,7 +16,7 @@ class App.SidebarChecklistStart extends App.Controller
{ name: 'checklist_template_id', display: __('Select Template'), tag: 'select', multiple: false, null: true, nulloption: true, relation: 'ChecklistTemplate', default: '' },
]
@html App.view('ticket_zoom/sidebar_checklist_start')(
@html App.view('ticket_zoom/sidebar_checklist/start')(
showManageLink: App.User.current()?.permission('admin.checklist')
readOnly: @readOnly
activeTemplateCount: App.ChecklistTemplate.search(filter: { active: true })?.length
@@ -37,7 +37,8 @@ class App.SidebarChecklistStart extends App.Controller
data: JSON.stringify({ ticket_id: @parentVC.ticket.id })
processData: true
success: (data, status, xhr) =>
@parentVC.shown(true)
App.Collection.loadAssets(data.assets)
@parentVC.renderWidget(true)
)
onAddFromTemplate: (e) =>
@@ -56,7 +57,8 @@ class App.SidebarChecklistStart extends App.Controller
url: "#{@apiPath}/checklists"
data: JSON.stringify({ ticket_id: @parentVC.ticket.id, template_id: params.checklist_template_id })
success: (data, status, xhr) =>
@parentVC.shown()
App.Collection.loadAssets(data.assets)
@parentVC.renderWidget()
)
onTemplateChange: (e) =>

View File

@@ -1,411 +0,0 @@
class App.SidebarChecklistShow extends App.Controller
events:
'click .js-reorder': 'onReorder'
'click .js-add': 'onAdd'
'click .js-save-order': 'onSaveOrder'
'click .js-reset-order': 'onResetOrder'
'click .js-action': 'onAction'
'change .js-checkbox': 'onCheckboxClick'
'click .js-title': 'onTitleChange'
'click .js-checklist-item-edit': 'onActionButtonClicked'
elements:
'.js-reorder': 'reorderButton'
'.js-add': 'addButton'
'.js-save-order': 'saveOrderButton'
'.js-reset-order': 'resetOrderButton'
'table': 'table'
constructor: ->
super
@render()
render: ->
@html App.view('ticket_zoom/sidebar_checklist_show')(
checklistTitle: @checklistTitle()
readOnly: @readOnly
)
$('body').off('click').on('click', (e) =>
return if @actionController && @actionController.constructor.name is 'ChecklistReorder'
return if $(e.target).closest('.js-actions').length > 0
return if $(e.target).closest('button').length > 0
return if $(e.target).closest('.checkbox-replacement').length > 0
@actionController?.releaseController()
)
@renderTable()
checklistTitle: =>
@checklist.name || App.i18n.translateInline('%s Checklist', App.Config.get('ticket_hook') + @parentVC.ticket.number)
onReorder: (e) =>
@preventDefaultAndStopPropagation(e)
@actionController?.releaseController()
@actionController = new ChecklistReorder(parentVC: @)
onAdd: (e) =>
@addButton.attr('disabled', true)
callbackDone = (data) =>
@enterEditModeId = data.id
@renderTable()
@parentVC.subscribe()
@addButton.attr('disabled', false)
item = new App.ChecklistItem
item.checklist_id = @checklist.id
item.text = ''
item.save(
done: ->
App.ChecklistItem.full(@id, callbackDone, force: true)
fail: (settings, details) =>
@notify(
type: 'error'
msg: App.i18n.translateContent(details.error)
)
@renderTable()
@addButton.attr('disabled', false)
)
onCheckboxClick: (e) =>
upcomingState = e.currentTarget.checked
id = parseInt(e.currentTarget.value)
e.currentTarget.disabled = true
@updateChecklistItem(id, upcomingState, e.currentTarget)
onCheckOrUncheck: (id, e) =>
row = $(e.currentTarget).closest('tr')
checkbox = row.find('.js-checkbox')[0]
upcomingState = !checkbox.checked
checkbox.disabled = true
@updateChecklistItem(id, upcomingState, checkbox)
updateChecklistItem: (id, upcomingState, checkboxElem) =>
item = App.ChecklistItem.find(id)
item.checked = upcomingState
item.save(
done: =>
checkboxElem.disabled = false
@renderTable()
fail: =>
@renderTable()
)
onSaveOrder: (e) =>
@saveOrderButton.attr('disabled', true)
sorted_item_ids = @table.find('tbody tr').toArray().map (elem) -> elem.dataset.id
item = @checklist
item.sorted_item_ids = sorted_item_ids
item.save(
done: (data) =>
@actionController?.completed()
fail: =>
@notify(
type: 'error'
msg: App.i18n.translateInline('Failed to save the order of the checklist items. Please try again.')
)
@saveOrderButton.attr('disabled', false)
)
onResetOrder: (e) =>
@actionController?.releaseController()
onAction: (e) =>
e.stopPropagation()
dropdown = $(e.currentTarget).closest('td').find('.js-table-action-menu')
dropdown.dropdown('toggle')
dropdown.off('click.dropdown').on('click.dropdown', '[data-table-action]', @onActionButtonClicked)
dropdown.off('keyup.dropdown').on('keyup.dropdown', '[data-table-action]', @onActionButtonKeyup)
onTitleChange: (e) =>
e?.stopPropagation()
return if @actionController && @actionController.constructor.name is 'ChecklistReorder'
# Close any open dropdowns
@el.find('.dropdown--actions.open').dropdown('toggle')
if e
elem = e.currentTarget
else
elem = @el.find('.js-title')[0]
@actionController?.releaseController()
@actionController = new ChecklistRenameEdit(el: elem, parentVC: @, originalValue: @checklistTitle())
onActionButtonKeyup: (e) =>
return if e.key isnt 'Enter'
@onActionButtonClicked(e)
onActionButtonClicked: (e) =>
e?.stopPropagation()
return if @actionController && @actionController.constructor.name is 'ChecklistReorder'
id = $(e.currentTarget).parents('tr').data('id')
name = e.currentTarget.getAttribute('data-table-action')
if name is 'edit'
# skip on link openings
return if e.target.tagName is 'A' && $(e.target).parent().hasClass('checklistItemValue')
@onEditChecklistItem(id, e)
else if name is 'delete'
@onDeleteChecklistItem(id, e)
else if _.contains(['check', 'uncheck'], name)
@onCheckOrUncheck(id, e)
toggleReorder: (isReordering, disablingCommand = 'disable') =>
@table.find('tbody').sortable(if isReordering then 'enable' else disablingCommand)
@table.find('.draggable').toggleClass('hide', !isReordering)
@table.find('.checkbox-replacement').toggleClass('hide', isReordering)
@table.find('.checkbox-replacement-readonly').toggleClass('hide', !isReordering)
@table.find('.dropdown').toggleClass('hide', isReordering)
@reorderButton.toggleClass('hide', isReordering)
@addButton.toggleClass('hide', isReordering)
@saveOrderButton.toggleClass('hide', !isReordering)
@resetOrderButton.toggleClass('hide', !isReordering)
onEditChecklistItem: (id, e) =>
@preventDefaultAndStopPropagation(e)
return if @actionController && @actionController.constructor.name is 'ChecklistItemEdit' && @actionController.id == id
row = $(e.currentTarget).closest('tr')
cell = row.find('.checklistItemValue')[0]
@activateItemEditMode(cell, row, id)
onDeleteChecklistItem: (id, e) =>
@preventDefaultAndStopPropagation(e)
row = $(e.currentTarget).closest('tr')
dropdown = $(e.currentTarget).closest('td').find('.js-table-action-menu')
dropdown.dropdown('toggle')
item = App.ChecklistItem.find(id)
deleteCallback = =>
item.destroy(
done: =>
@renderTable()
@parentVC.subscribe()
)
# Skip confirmation dialog if the item has no text.
if _.isEmpty(item.text)
deleteCallback()
return
new ChecklistItemRemoveModal(
item: item
container: @el.closest('.content')
callback: deleteCallback
)
activateItemEditMode: (cell, row, id) =>
@actionController?.releaseController()
@actionController = new ChecklistItemEdit(
el: cell
parentVC: @
originalValue: cell.textContent.trim()
id: id
)
renderTable: ->
@table.find('tbody').empty()
sorted_items = @checklist.sorted_items()
for object in sorted_items
if object.ticket_id
ticket = App.Ticket.find(object.ticket_id)
ticketAccess = if ticket then ticket.userGroupAccess('read') else false
html = App.view('ticket_zoom/sidebar_checklist_show_row')(
object: object
ticket: ticket
ticketAccess: ticketAccess
readOnly: @readOnly
)
@table.find('tbody').append(html)
if !sorted_items.length
html = App.view('ticket_zoom/sidebar_checklist_show_no_items')()
@table.find('tbody').append(html)
dndOptions =
tolerance: 'pointer'
distance: 15
opacity: 0.6
forcePlaceholderSize: true
items: 'tr'
@table.find('tbody').sortable(dndOptions)
@table.find('tbody').sortable('disable')
@reorderButton.toggleClass('hide', !sorted_items.length || sorted_items.length < 2)
if @enterEditMode
@enterEditMode = undefined
@enterEditModeId = @table.find('tbody tr:last-of-type').data('id')
if @enterEditModeId
cell = @table.find("tbody tr[data-id='" + @enterEditModeId + "']").find('.checklistItemValue')[0]
row = $(cell).closest('tr')
return if !row.length
@enterEditModeId = undefined
@activateItemEditMode(cell, row, row.data('id'))
@parentVC.badgeRenderLocal()
class ChecklistItemEdit extends App.Controller
elements:
'.js-input': 'input'
events:
'click .js-cancel': 'onCancel'
'click .js-confirm': 'onConfirm'
'blur .js-input': 'onConfirm'
'keyup #checklistItemEditText': 'onKeyUp'
constructor: ->
super
@render()
releaseController: =>
super
@el.text(@originalValue)
@el.removeClass('edit-widget-active')
@el.closest('tr').find('.dropdown').removeClass('hide')
if @el.closest('tr').find('.dropdown--actions').hasClass('open')
@el.closest('tr').find('.js-table-action-menu').dropdown('toggle')
@parentVC.actionController = undefined
render: =>
@html App.view('ticket_zoom/sidebar_checklist_item_edit')(value: @object()?.text)
@el.addClass('edit-widget-active')
@el.closest('tr').find('.dropdown').addClass('hide')
@input.focus().val('').val(@object()?.text)
object: =>
App.ChecklistItem.find(@id)
onCancel: (e) =>
@preventDefaultAndStopPropagation(e)
@releaseController()
onConfirm: (e) =>
@preventDefaultAndStopPropagation(e)
item = @object()
item.text = @input.val()
item.save(
done: =>
@parentVC.renderTable()
@parentVC.parentVC.subscribe()
fail: (settings, details) =>
App.ChecklistItem.fetch(id: item.id)
@notify(
type: 'error'
msg: App.i18n.translateContent(details.error)
)
@releaseController()
)
onKeyUp: (e) =>
switch e.key
when 'Enter' then @onConfirm()
when 'Escape' then @releaseController()
class ChecklistRenameEdit extends App.Controller
elements:
'.js-input': 'input'
events:
'click .js-cancel': 'onCancel'
'click .js-confirm': 'onConfirm'
'keyup #checklistTitleEditText': 'onKeyUp'
constructor: ->
super
@render()
releaseController: =>
super
@el.text(@originalValue)
@parentVC.actionController = undefined
render: =>
@html App.view('ticket_zoom/sidebar_checklist_title_edit')(
value: @object()?.name
ticketNumber: @parentVC.parentVC.ticket.number
)
@input.focus().val('').val(@object()?.name)
object: =>
@parentVC.checklist
onCancel: (e) =>
@preventDefaultAndStopPropagation(e)
@releaseController()
onConfirm: (e) =>
@preventDefaultAndStopPropagation(e)
checklist = @object()
checklist.name = @input.val()
checklist.save(
done: =>
@parentVC.render()
)
onKeyUp: (e) =>
switch e.key
when 'Enter' then @onConfirm()
when 'Escape' then @onCancel()
class ChecklistReorder extends App.Controller
constructor: ->
super
@render()
releaseController: =>
@parentVC.toggleReorder(false, 'cancel')
@parentVC.saveOrderButton.attr('disabled', false)
@parentVC.actionController = undefined
completed: =>
@parentVC.toggleReorder(false)
@parentVC.saveOrderButton.attr('disabled', false)
@parentVC.actionController = undefined
render: =>
@parentVC.toggleReorder(true)
class ChecklistItemRemoveModal extends App.ControllerGenericDestroyConfirm
onSubmit: =>
@close()
@callback()

View File

@@ -82,13 +82,26 @@ class App.ImageService
else
quality = 0.6
newType = @validateType(type)
newDataUrl = canvas.toDataURL(newType, quality)
newSizeInMb = (newDataUrl.length * 0.75)/1024/1024 # approx. file size
# Nokogiri parser has a single text node limit of 10 million characters, which may be exceeded for certain file
# types very easily (e.g. image/png), even for resized images. Here we check if the new size exceeds this limit
# and prevent execution of the callback (#5359).
if newDataUrl.length > 10000000
console.error('ImageService', 'image file size too large', newType, newSizeInMb, 'in mb')
new App.ControllerErrorModal(
message: __('Image file size is too large, please try inserting a smaller file.')
)
return
# execute callback with resized image
newDataUrl = canvas.toDataURL(@validateType(type), quality)
if resize
console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quality, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quality, newSizeInMb, 'in mb')
callback(newDataUrl, x/sizeFactor, y/sizeFactor, true)
return
console.log('ImageService', 'no resize', x, y, quality, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
console.log('ImageService', 'no resize', x, y, quality, newSizeInMb, 'in mb')
callback(newDataUrl, x, y, false)
# load image from data url

View File

@@ -1045,21 +1045,22 @@ class App.Utils
@checkAttachmentReference: (message) ->
return false if !message
# remove blockquote from message, check only the unquoted content
# remove blockquote, signatures and images from message, check only the unquoted content
tmp = $('<div>' + message + '</div>')
tmp.find('blockquote').remove()
tmp.find('blockquote, img, div[data-signature="true"]').remove()
text = tmp.text()
matchwords = __('attachment,attached,enclosed,enclosure')
#en
for word in matchwords.split(',')
regexp = new RegExp("\\W#{word}\\W", 'i')
regexp = new RegExp("\\b#{word}\\b", 'i')
return word if text.match(regexp)
# user locale
for word in App.i18n.translateContent(matchwords).split(',')
regexp = new RegExp("\\W#{word}\\W", 'i')
regexp = new RegExp("\\b#{word}\\b", 'i')
return word if text.match(regexp)
false
@@ -1187,7 +1188,7 @@ class App.Utils
# search: <svg class="icon icon-([^\s]+)\s([^"]*).*<\/svg>
# replace: <%- @Icon('$1', '$2') %>
#
path = if window.svgPolyfill then '' else 'assets/images/icons.svg'
path = if window.svgPolyfill then '' else App.Config.get('icons_url')
"<svg class=\"icon icon-#{name} #{className}\"><use xlink:href=\"#{path}#icon-#{name}\" /></svg>"
@fontIcon: (name, font, className = '') ->

View File

@@ -350,6 +350,18 @@
this.log('paste image', clipboardImage)
var imageFile = clipboardImage.getAsFile()
var fileSizeInMb = imageFile.size / 1024 / 1024
// The browser may fail while reading too large files as data URL.
// Here we introduce a safe limit check in order to prevent silent errors.
if (fileSizeInMb > 25) {
console.error('Image file size too large', fileSizeInMb, 'in mb')
new App.ControllerErrorModal({
message: __('Image file size is too large, please try inserting a smaller file.'),
})
return
}
var reader = new FileReader()
reader.onload = $.proxy(function (e) {

View File

@@ -28,27 +28,21 @@ class App.Checklist extends App.Model
else
!item.checked
@completedForTicketId: (ticket_id, callback) =>
App.Ajax.request(
id: 'checklist_completed'
type: 'GET'
url: "#{@apiPath}/tickets/#{ticket_id}/checklist/completed"
success: (data, status, xhr) ->
callback(data)
)
@calculateState: (ticket) ->
return if !ticket.checklist_incomplete
checklist = App.Checklist.find ticket.checklist_id
return if !checklist
{
all: ticket.checklist_total
open: ticket.checklist_incomplete
all: checklist.sorted_item_ids.length
open: checklist.open_items().length
}
@calculateReferences: (ticket) ->
checklists = App.Checklist
return [] if !ticket.referencing_checklist_ids
App.Checklist
.findAll(ticket.referencing_checklist_ids)
.filter (elem) -> !elem.ticket_inaccessible
App.Ticket.findAll checklists.map (elem) -> elem.ticket_id
.map (elem) -> App.Ticket.findByAttribute 'checklist_id', elem.id
.filter (elem) -> !!elem

View File

@@ -17,7 +17,7 @@
<p><%- @T('Example:') %></p>
<code>
curl -H "Authorization: Token token=YOUR_TOKEN" <%= @C('http_type') %>://<%= @C('fqdn') %>/api/v1/groups
curl -H "Authorization: Token token=…" <%= @C('http_type') %>://<%= @C('fqdn') %>/api/v1/groups
</code>
<hr>

View File

@@ -7,8 +7,8 @@
<td class="settings-list-control-cell settings-list-checkbox-cell">
<label class="inline-label checkbox-replacement js-new-tab">
<input type="checkbox" value="1" data-name="new_tab" <% if @item?.new_tab: %>checked<% end %>>
<svg class="icon icon-checkbox icon-unchecked"><use xlink:href="assets/images/icons.svg#icon-checkbox"></use></svg>
<svg class="icon icon-checkbox-checked icon-checked"><use xlink:href="assets/images/icons.svg#icon-checkbox-checked"></use></svg>
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %>
<span class="label-text"><%- @T('New tab') %></span>
</label>

View File

@@ -2,9 +2,7 @@
<div class="scrollPageHeader tabsSidebar-sidebarSpacer" style="<%- if @dir is 'rtl' then 'left' else 'right' %>: <%- @scrollbarWidth %>px">
<small class="ticket-number-copy-header">
<%- @C('ticket_hook') %><span class="ticket-number"><%- @ticket.number %></span>
<svg class="icon icon-clipboard ticketNumberCopy-icon u-clickable">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="assets/images/icons.svg#icon-clipboard"></use>
</svg>
<%- @Icon('clipboard', 'ticketNumberCopy-icon u-clickable') %>
</small>
<div class="js-ticketTitleContainer ticket-title"></div>
<div class="js-highlighterContainer highlighter"></div>

View File

@@ -0,0 +1,44 @@
<tr data-id="<%= @object.id %>">
<td class="noTruncate checklistShowRowContentCell">
<div class="checklistShowRowContent">
<% if !@readOnly: %>
<div class="hide draggable">
<%- @Icon('draggable') %>
</div>
<% end %>
<div class="checklistShowRowDisplayValue"></div>
<% if !@readOnly: %>
<div class="dropdown-container">
<div class="dropdown dropdown--actions">
<button class="btn btn--table btn--text btn--secondary js-action" data-toggle="dropdown">
<%- @Icon('overflow-button') %>
</button>
<ul class="dropdown-menu dropdown-menu-right js-table-action-menu" role="menu">
<li role="menuitem" data-table-action="check" class="<% if @object.ticket_id: %>hide<% end %>">
<span class="dropdown-iconSpacer">
<%- @Icon('checkmark') %>
</span>
<% if !@object.checked: %>
<%- @T('Check item') %>
<% else: %>
<%- @T('Uncheck item') %>
<% end %>
<li role="menuitem" data-table-action="edit" class="<% if @object.ticket_id: %>hide<% end %>">
<span class="dropdown-iconSpacer">
<%- @Icon('pen') %>
</span>
<%- @T('Edit item') %>
<li role="menuitem" class="danger" data-table-action="delete">
<span class="dropdown-iconSpacer">
<%- @Icon('trash') %>
</span>
<%- @T('Remove item') %>
</ul>
<div>
<div>
<% end %>
</div>
</td>
</tr>

View File

@@ -0,0 +1,35 @@
<% if @object.ticket_id: %>
<% if @ticketAccess: %>
<div class="checklistItemTicketStateIndicator" title="<%- @ticket.iconTitle() %>">
<%- @Icon('task-state', @ticket.iconClass()) %>
</div>
<div class="checklistItemTicketTitle">
<a class="name ticket-popover" data-id="<%- @ticket.id %>" href="<%- @ticket.uiUrl() %>">
<%= @ticket.title || '-' %>
</a>
</div>
<% else: %>
<div class="checklistItemTicketStateIndicator">
<%- @Icon('diagonal-cross', 'icon-error') %>
</div>
<div class="checklistItemTicketTitle">
» <%- @T('Not authorized') %> «
</div>
<% end %>
<% else: %>
<div class="checkbox-replacement-readonly<% if !@readOnly: %> hide<% end %>">
<% if @object.checked: %>
<%- @Icon('checkbox-checked-readonly') %>
<% else: %>
<%- @Icon('checkbox-readonly') %>
<% end %>
</div>
<label class="checkbox-replacement<% if @readOnly: %> hide<% end %>">
<input type="checkbox" class="js-checkbox" value="<%= @object.id %>" <% if @object.checked: %>checked<% end %>>
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %>
</label>
<div class="checklistItemValue <% if !@readOnly: %>js-checklist-item-edit u-clickable<% end %>" <% if !@readOnly: %>data-table-action="edit"<% end %>>
</div>
<% end %>

View File

@@ -1,97 +0,0 @@
<tr data-id="<%= @object.id %>">
<td class="noTruncate checklistShowRowContentCell">
<div class="checklistShowRowContent">
<% if !@readOnly: %>
<div class="hide draggable">
<%- @Icon('draggable') %>
</div>
<% end %>
<% if @object.ticket_id: %>
<% if @ticketAccess: %>
<div class="checklistItemTicketStateIndicator" title="<%- @ticket.iconTitle() %>">
<%- @Icon('task-state', @ticket.iconClass()) %>
</div>
<div class="checklistItemTicketTitle">
<a class="name ticket-popover" data-id="<%- @ticket.id %>" href="<%- @ticket.uiUrl() %>">
<%= @ticket.title || '-' %>
</a>
</div>
<% else: %>
<div class="checklistItemTicketStateIndicator">
<%- @Icon('diagonal-cross', 'icon-error') %>
</div>
<div class="checklistItemTicketTitle">
» <%- @T('Not authorized') %> «
</div>
<% end %>
<% else: %>
<div class="checkbox-replacement-readonly<% if !@readOnly: %> hide<% end %>">
<% if @object.checked: %>
<%- @Icon('checkbox-checked-readonly') %>
<% else: %>
<%- @Icon('checkbox-readonly') %>
<% end %>
</div>
<label class="checkbox-replacement<% if @readOnly: %> hide<% end %>">
<input type="checkbox" class="js-checkbox" value="<%= @object.id %>" <% if @object.checked: %>checked<% end %>>
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %>
</label>
<div class="checklistItemValue <% if !@readOnly: %>js-checklist-item-edit u-clickable<% end %>" <% if !@readOnly: %>data-table-action="edit"<% end %>>
<% if @object.text: %>
<%- App.Utils.linkify(@object.text) %>
<% else if @readonly: %>
<%- '—' %>
<% end %>
</div>
<% end %>
<% if !@readOnly: %>
<div class="dropdown-container">
<div class="dropdown dropdown--actions">
<button class="btn btn--table btn--text btn--secondary js-action" data-toggle="dropdown">
<%- @Icon('overflow-button') %>
</button>
<ul class="dropdown-menu dropdown-menu-right js-table-action-menu" role="menu">
<% if !@object.ticket_id: %>
<% if !@object.checked: %>
<li role="presentation" class="" data-table-action="check">
<a role="menuitem" tabindex="-1">
<span class="dropdown-iconSpacer">
<%- @Icon('checkmark') %>
</span>
<%- @T('Check item') %>
</a>
<% else: %>
<li role="presentation" class="" data-table-action="uncheck">
<a role="menuitem" tabindex="-1">
<span class="dropdown-iconSpacer">
<%- @Icon('checkmark') %>
</span>
<%- @T('Uncheck item') %>
</a>
<% end %>
<li role="presentation" class="" data-table-action="edit">
<a role="menuitem" tabindex="-1">
<span class="dropdown-iconSpacer">
<%- @Icon('pen') %>
</span>
<%- @T('Edit item') %>
</a>
<% end %>
<li role="presentation" class="danger" data-table-action="delete">
<a role="menuitem" tabindex="-1">
<span class="dropdown-iconSpacer">
<%- @Icon('trash') %>
</span>
<%- @T('Remove item') %>
</a>
</ul>
<div>
<div>
<% end %>
</div>
</td>
</tr>

View File

@@ -9,7 +9,7 @@
}
this.kbPath = function() {
var html = document.querySelector('html')
var html = document.documentElement
return html.dataset.basePath.replace(encodeURIComponent('{locale}'), targetLanguage)
}
@@ -49,7 +49,7 @@
function LanguageDetector() { }
LanguageDetector.html = function() { return document.querySelector('html') }
LanguageDetector.html = function() { return document.documentElement }
LanguageDetector.document_languages = function() {
return this.html().dataset.availableLocales.split(',')

View File

@@ -19,8 +19,8 @@
KnowledgeBaseSearch.lookupAction = function(query) {
var params = {
knowledge_base_id: document.querySelector('html').dataset.id,
locale: document.querySelector('html').lang,
knowledge_base_id: document.documentElement.dataset.id,
locale: document.documentElement.lang,
query: query,
flavor: 'public'
}
@@ -106,7 +106,7 @@
this.generateIcon = function(iconName, type) {
switch(type) {
case 'KnowledgeBase::Category::Translation':
iconset = document.querySelector('html').dataset.iconset
iconset = document.documentElement.dataset.iconset
return Zammad.Util.generateIcon(iconName, iconset)
default:
return Zammad.Util.generateIcon(iconName)

View File

@@ -3,7 +3,7 @@
Util.generateIcon = function(iconName, iconset) {
if(!iconset) {
return '<svg class="icon icon-' + iconName + '"><use xlink:href="/assets/images/icons.svg#icon-' + iconName + '"></use></svg>'
return '<svg class="icon icon-' + iconName + '"><use xlink:href="/' + document.documentElement.dataset.iconsUrl + '#icon-' + iconName + '"></use></svg>'
}
return '<i data-font="' + iconset + '">&#x' + iconName + '</i>'

View File

@@ -146,6 +146,7 @@
.icon-split { width: 16px; height: 17px; }
.icon-sso-button { width: 29px; height: 24px; }
.icon-status-modified-outer-circle { width: 16px; height: 16px; }
.icon-status-report { width: 22px; height: 22px; }
.icon-status { width: 16px; height: 16px; }
.icon-stopwatch { width: 77px; height: 83px; }
.icon-strikethrough { width: 12px; height: 12px; }

View File

@@ -386,6 +386,10 @@ ul {
pointer-events: none;
}
.u-low-opacity {
opacity: 60%;
}
.u-clickable {
@extend %clickable;
}
@@ -1206,6 +1210,10 @@ pre code {
}
}
}
button[disabled] ~ .dropdown-menu-accessories button {
opacity: 0.33;
}
}
.dropdown-menu-right {
@@ -14971,6 +14979,11 @@ div.accounted-time-value {
justify-content: center;
}
.checklistShowRowDisplayValue {
display: flex;
flex-grow: 1;
}
.draggable {
width: 40px;
height: 28px;

View File

@@ -15,4 +15,5 @@ class ApplicationController < ActionController::Base
include ApplicationController::LogsHttpAccess
include ApplicationController::Authorizes
include ApplicationController::Klass
include ApplicationHelper
end

View File

@@ -16,6 +16,7 @@ module ApplicationController::HandlesErrors
rescue_from Exceptions::Forbidden, with: :forbidden
rescue_from Pundit::NotAuthorizedError, with: :pundit_not_authorized_error
rescue_from Store::Provider::S3::Error, with: :unprocessable_entity
rescue_from Exceptions::MissingAttribute, Exceptions::InvalidAttribute, with: :unprocessable_entity
end
def not_found(e)
@@ -104,7 +105,7 @@ module ApplicationController::HandlesErrors
elsif e.message == 'Exceptions::NotAuthorized'
data[:error] = __('Authorization failed')
data[:error_human] = data[:error]
elsif [ActionController::RoutingError, ActiveRecord::RecordNotFound, Exceptions::UnprocessableEntity, Exceptions::NotAuthorized, Exceptions::Forbidden, Store::Provider::S3::Error, Authorization::Provider::AccountError].include?(e.class)
elsif [ActionController::RoutingError, ActiveRecord::RecordNotFound, Exceptions::UnprocessableEntity, Exceptions::NotAuthorized, Exceptions::Forbidden, Store::Provider::S3::Error, Authorization::Provider::AccountError, Exceptions::MissingAttribute, Exceptions::InvalidAttribute].include?(e.class)
data[:error_human] = data[:error]
end

View File

@@ -12,29 +12,17 @@ class ChecklistTemplatesController < ApplicationController
end
def create
items = params[:items].presence || []
checklist_template = ChecklistTemplate.create!(checklist_template_params)
checklist_template.replace_items!(checklist_template_items_params) if checklist_template_items_params.present?
ChecklistTemplate.create!(
name: params[:name],
active: params[:active],
).tap do |cl|
create_and_sort_checklist_template_items(cl, items)
render json: cl.attributes_with_association_ids, status: :created
end
render json: checklist_template.attributes_with_association_ids, status: :created
end
def update
checklist_template = ChecklistTemplate.find(params[:id])
items = params[:items].presence || []
checklist_template.update!(
name: params[:name],
active: params[:active],
)
create_and_sort_checklist_template_items(checklist_template, items)
checklist_template.update!(checklist_template_params)
checklist_template.replace_items!(checklist_template_items_params) if checklist_template_items_params.present?
render json: checklist_template.attributes_with_association_ids, status: :ok
end
@@ -45,12 +33,13 @@ class ChecklistTemplatesController < ApplicationController
private
def create_and_sort_checklist_template_items(checklist_template, items)
return if items.blank?
def checklist_template_params
params.permit(:name, :active)
end
checklist_template.items.destroy_all if checklist_template.items.present?
checklist_template.sorted_item_ids = []
items.compact_blank.each { |text| ChecklistTemplate::Item.create!(text: text.strip, checklist_template:) }
def checklist_template_items_params
@checklist_template_items_params ||= params[:items]
.presence
&.compact_blank
end
end

View File

@@ -3,35 +3,21 @@
class ChecklistsController < ApplicationController
prepend_before_action :authenticate_and_authorize!
def show_by_ticket
checklist = Checklist.find_by ticket_id: params[:ticket_id]
if checklist
authorize!(checklist, :show?)
assets = ApplicationModel::CanAssets.reduce([checklist] + checklist.items, {})
render json: { id: checklist.id, assets: assets }
return
end
render json: {}
end
def show
model_show_render(Checklist, existing_checklist_params)
end
def create
new_checklist = if params[:template_id].present?
ChecklistTemplate.find_by(id: params[:template_id]).create_from_template!(ticket_id: params[:ticket_id])
else
Checklist.create!(name: '', ticket_id: params[:ticket_id]).tap do |checklist|
Checklist::Item.create!(checklist_id: checklist.id, text: '')
end
end
ticket = Ticket.find params[:ticket_id]
new_checklist.reload
checklist = if params[:template_id].present?
template = ChecklistTemplate.find(params[:template_id])
Checklist.create_from_template!(ticket, template)
else
Checklist.create_fresh!(ticket)
end
render json: { id: new_checklist.id, assets: new_checklist.assets({}) }, status: :created
render json: { id: checklist.id, assets: checklist.assets({}) }, status: :created
end
def update
@@ -44,10 +30,6 @@ class ChecklistsController < ApplicationController
private
def new_checklist_params
params.permit(:ticket_id, :name)
end
def existing_checklist_params
params.permit(:id, :name, sorted_item_ids: [])
end

View File

@@ -54,6 +54,7 @@ module CreatesTicketArticles # rubocop:disable Metrics/ModuleLength
article = Ticket::Article.new(clean_params)
article.ticket_id = ticket.id
article.check_mentions_raises_error = true
article.check_email_recipient_raises_error = true
# store dataurl images to store
attachments_inline = []
@@ -145,7 +146,7 @@ module CreatesTicketArticles # rubocop:disable Metrics/ModuleLength
# clear in-progress state from taskbar
Taskbar
.where(user_id: current_user.id)
.first { |taskbar| taskbar.persisted_form_id == form_id }
.find { |taskbar| taskbar.persisted_form_id == form_id }
&.update!(state: {})
# remove temporary attachment cache

View File

@@ -251,7 +251,7 @@ curl http://localhost/api/v1/organization/{id} -v -u #{login}:#{password} -H "Co
#
# @summary Download of example CSV file.
# @notes The requester have 'admin.organization' permissions to be able to download it.
# @example curl -u 'me@example.com:test' http://localhost:3000/api/v1/organizations/import_example
# @example curl -u #{login}:#{password} http://localhost:3000/api/v1/organizations/import_example
#
# @response_message 200 File download.
# @response_message 403 Forbidden / Invalid session.
@@ -268,8 +268,8 @@ curl http://localhost/api/v1/organization/{id} -v -u #{login}:#{password} -H "Co
#
# @summary Starts import.
# @notes The requester have 'admin.text_module' permissions to be create a new import.
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/organizations.csv' 'https://your.zammad/api/v1/organizations/import?try=true'
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/organizations.csv' 'https://your.zammad/api/v1/organizations/import'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/organizations.csv' 'https://your.zammad/api/v1/organizations/import?try=true'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/organizations.csv' 'https://your.zammad/api/v1/organizations/import'
#
# @response_message 201 Import started.
# @response_message 403 Forbidden / Invalid session.

View File

@@ -326,6 +326,7 @@ class SessionsController < ApplicationController
end
config['core_workflow_config'] = CoreWorkflow.config
config['icons_url'] = icons_url
config
end

View File

@@ -152,7 +152,7 @@ curl http://localhost/api/v1/text_modules.json -v -u #{login}:#{password} -H "Co
#
# @summary Download of example CSV file.
# @notes The requester have 'admin.text_module' permissions to be able to download it.
# @example curl -u 'me@example.com:test' http://localhost:3000/api/v1/text_modules/import_example
# @example curl -u #{login}:#{password} http://localhost:3000/api/v1/text_modules/import_example
#
# @response_message 200 File download.
# @response_message 403 Forbidden / Invalid session.
@@ -173,8 +173,8 @@ curl http://localhost/api/v1/text_modules.json -v -u #{login}:#{password} -H "Co
#
# @summary Starts import.
# @notes The requester have 'admin.text_module' permissions to be create a new import.
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/Textbausteine_final2.csv' 'https://your.zammad/api/v1/text_modules/import?try=true'
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/Textbausteine_final2.csv' 'https://your.zammad/api/v1/text_modules/import'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/Textbausteine_final2.csv' 'https://your.zammad/api/v1/text_modules/import?try=true'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/Textbausteine_final2.csv' 'https://your.zammad/api/v1/text_modules/import'
#
# @response_message 201 Import started.
# @response_message 403 Forbidden / Invalid session.

View File

@@ -209,7 +209,7 @@ class TicketArticlesController < ApplicationController
#
# @summary Download of example CSV file.
# @notes The requester have 'admin' permissions to be able to download it.
# @example curl -u 'me@example.com:test' http://localhost:3000/api/v1/ticket_articles/import_example
# @example curl -u #{login}:#{password} http://localhost:3000/api/v1/ticket_articles/import_example
#
# @response_message 200 File download.
# @response_message 403 Forbidden / Invalid session.
@@ -230,8 +230,8 @@ class TicketArticlesController < ApplicationController
#
# @summary Starts import.
# @notes The requester have 'admin' permissions to be create a new import.
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/ticket_articles.csv' 'https://your.zammad/api/v1/ticket_articles/import?try=true'
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/ticket_articles.csv' 'https://your.zammad/api/v1/ticket_articles/import'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/ticket_articles.csv' 'https://your.zammad/api/v1/ticket_articles/import?try=true'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/ticket_articles.csv' 'https://your.zammad/api/v1/ticket_articles/import'
#
# @response_message 201 Import started.
# @response_message 403 Forbidden / Invalid session.

View File

@@ -96,6 +96,10 @@ class TicketsController < ApplicationController
shared_draft&.destroy
end
# Prevent direct access to checklist via API
# Otherwise users may get unauthorized access to checklists of other tickets
params.delete(:checklist_id)
clean_params = Ticket.association_name_to_id_convert(params)
# overwrite params
@@ -243,6 +247,10 @@ class TicketsController < ApplicationController
ticket = Ticket.find(params[:id])
authorize!(ticket, :follow_up?)
# Prevent direct access to checklist via API
# Otherwise users may get unauthorized access to checklists of other tickets
params.delete(:checklist_id)
clean_params = Ticket.association_name_to_id_convert(params)
clean_params = Ticket.param_cleanup(clean_params, true)
@@ -609,7 +617,7 @@ class TicketsController < ApplicationController
#
# @summary Download of example CSV file.
# @notes The requester have 'admin' permissions to be able to download it.
# @example curl -u 'me@example.com:test' http://localhost:3000/api/v1/tickets/import_example
# @example curl -u #{login}:#{password} http://localhost:3000/api/v1/tickets/import_example
#
# @response_message 200 File download.
# @response_message 403 Forbidden / Invalid session.
@@ -630,8 +638,8 @@ class TicketsController < ApplicationController
#
# @summary Starts import.
# @notes The requester have 'admin' permissions to be create a new import.
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/tickets.csv' 'https://your.zammad/api/v1/tickets/import?try=true'
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/tickets.csv' 'https://your.zammad/api/v1/tickets/import'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/tickets.csv' 'https://your.zammad/api/v1/tickets/import?try=true'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/tickets.csv' 'https://your.zammad/api/v1/tickets/import'
#
# @response_message 201 Import started.
# @response_message 403 Forbidden / Invalid session.
@@ -705,6 +713,17 @@ class TicketsController < ApplicationController
assets = draft.assets(assets)
end
if Setting.get('checklist') && current_user.permissions?('ticket.agent')
ticket.checklist&.assets(assets)
ticket.referencing_checklists
.includes(:ticket)
.each do |elem|
elem.assets(assets)
elem.ticket.assets(assets) if elem.ticket.authorized_asset?
end
end
# return result
{
ticket_id: ticket.id,

View File

@@ -859,7 +859,7 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
#
# @summary Download of example CSV file.
# @notes The requester have 'admin.user' permissions to be able to download it.
# @example curl -u 'me@example.com:test' http://localhost:3000/api/v1/users/import_example
# @example curl -u #{login}:#{password} http://localhost:3000/api/v1/users/import_example
#
# @response_message 200 File download.
# @response_message 403 Forbidden / Invalid session.
@@ -876,8 +876,8 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
#
# @summary Starts import.
# @notes The requester have 'admin.text_module' permissions to be create a new import.
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/users.csv' 'https://your.zammad/api/v1/users/import?try=true'
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/users.csv' 'https://your.zammad/api/v1/users/import'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/users.csv' 'https://your.zammad/api/v1/users/import?try=true'
# @example curl -u #{login}:#{password} -F 'file=@/path/to/file/users.csv' 'https://your.zammad/api/v1/users/import'
#
# @response_message 201 Import started.
# @response_message 403 Forbidden / Invalid session.

View File

@@ -116,6 +116,12 @@ const submitEdit = () => {
// Needs to be checked, because the 'onSubmit' function is not required.
if (!props.onSubmitEdit) return undefined
// Don't trigger a mutation if there is no change
if (props.value === newEditValue.value) {
stopEditing(false)
return
}
if (!checkValidity(inputValue.value)) return
const submitEditResult = props.onSubmitEdit(inputValue.value)

View File

@@ -82,6 +82,8 @@ export const getCoreDesktopClasses: FormThemeExtension = (
treeselect: selectInputClasses(classes.treeselect),
autocomplete: selectInputClasses(classes.autocomplete),
agent: selectInputClasses(classes.agent),
customer: selectInputClasses(classes.customer),
recipient: selectInputClasses(classes.recipient),
toggle: extendClasses(classes.toggle, {
wrapper: 'flex h-10 flex-row-reverse items-center gap-1.5',
label: '!mb-0 grow',

View File

@@ -1,5 +1,7 @@
// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
import { i18n } from '#shared/i18n.ts'
import { useCheckBodyAttachmentReference } from '../useCheckBodyAttachmentReference.ts'
describe('useCheckBodyAttachmentReference', () => {
@@ -36,4 +38,28 @@ describe('useCheckBodyAttachmentReference', () => {
),
).toBeFalsy()
})
it('ignore attachment match words in signatures', () => {
const { missingBodyAttachmentReference } = useCheckBodyAttachmentReference()
expect(
missingBodyAttachmentReference(
'<p>Yes I did already a first look.</p><div data-signature="true"><p>I attached a file did you saw id?</p></div>',
),
).toBeFalsy()
})
it('ignore attachment match words when match word is not a single word', () => {
i18n.setTranslationMap(
new Map([['attachment,attached,enclosed,enclosure', 'Anlage']]),
)
const { missingBodyAttachmentReference } = useCheckBodyAttachmentReference()
expect(
missingBodyAttachmentReference(
'<p>Siehe Screenshot der Telefonanlage</p>',
),
).toBeFalsy()
})
})

View File

@@ -11,9 +11,11 @@ const referenceMatchwords = __('attachment,attached,enclosed,enclosure')
const removeQuotingFromBody = (body: string) => {
const dom = domFrom(body)
// Remove blockquotes and images
// Remove blockquotes, signatures and images
// To not detect matchwords which are not part of the user-written article
dom.querySelectorAll('blockquote, img').forEach((elem) => elem.remove())
dom
.querySelectorAll('blockquote, img, div[data-signature="true"]')
.forEach((elem) => elem.remove())
// Return the modified HTML content as a string.
return dom.innerHTML
@@ -26,7 +28,7 @@ const bodyAttachmentReferenceMatchwordExists = (body: string) => {
const translatedMatchwords = i18n.t(referenceMatchwords).split(',')
return matchwords.concat(translatedMatchwords).some((word) => {
const findWord = new RegExp(word, 'i')
const findWord = new RegExp(`\\b${word}\\b`, 'i')
return findWord.test(cleanBody)
})
}

View File

@@ -10,19 +10,17 @@ module Gql::Mutations
field :checklist, Gql::Types::ChecklistType, null: true, description: 'Created checklist'
def authorized?(ticket:, template_id: nil)
Pundit.authorize(context.current_user, Checklist.new(ticket:), :create?)
Setting.get('checklist') && Pundit.authorize(context.current_user, ticket, :agent_update_access?)
end
def resolve(ticket:, template_id: nil)
checklist = if template_id
Gql::ZammadSchema.verified_object_from_id(template_id, type: ::ChecklistTemplate).create_from_template!(ticket_id: ticket.id)
else
::Checklist.create!(name: '', ticket:).tap do |checklist|
Checklist::Item.create!(checklist_id: checklist.id, text: '')
end
end
template = Gql::ZammadSchema.verified_object_from_id(template_id, type: ::ChecklistTemplate)
checklist.reload
Checklist.create_from_template!(ticket, template)
else
Checklist.create_fresh!(ticket)
end
{ checklist: }
end

View File

@@ -22,6 +22,10 @@ module Gql::Mutations
.new(current_user: context.current_user)
.execute(ticket_data: input)
}
rescue Exceptions::InvalidAttribute => e
field = e.attribute == 'email_recipient' ? 'customer_id' : e.attribute
error_response({ field:, message: e.message })
end
end
end

View File

@@ -24,6 +24,10 @@ module Gql::Mutations
.new(current_user: context.current_user)
.execute(ticket: ticket, ticket_data: input, skip_validators: meta&.dig(:skip_validators), macro: meta&.dig(:macro))
}
rescue Exceptions::InvalidAttribute => e
field = e.attribute == 'email_recipient' ? 'article.to' : e.attribute
error_response({ field:, message: e.message })
rescue => e
raise e if !e.class.name.starts_with?('Service::Ticket::Update::Validator')

View File

@@ -0,0 +1,9 @@
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
module ApplicationHelper
def icons_url
Auth::RequestCache.fetch_value('icons_url') do
"assets/images/icons.svg?#{Rails.public_path.join('assets/images/icons.svg').mtime.to_i}"
end
end
end

View File

@@ -411,6 +411,8 @@ example
end
def fetch_message_ids(filter)
raise if @imap.capabilities.exclude?('SORT')
{
result: @imap.sort(['DATE'], filter, 'US-ASCII'),
is_fallback: false

View File

@@ -4,14 +4,14 @@ class Checklist < ApplicationModel
include HasDefaultModelUserRelations
include ChecksClientNotification
include HasHistory
include Checklist::SearchIndex
include Checklist::TriggersSubscriptions
include Checklist::Assets
include CanChecklistSortedItems
belongs_to :ticket
has_one :ticket, dependent: :nullify
has_many :items, inverse_of: :checklist, dependent: :destroy
validates :ticket_id, uniqueness: true
validates :name, length: { maximum: 250 }
history_attributes_ignored :sorted_item_ids
@@ -25,7 +25,7 @@ class Checklist < ApplicationModel
def history_log_attributes
{
related_o_id: ticket_id,
related_o_id: ticket.id,
related_history_object: 'Ticket',
}
end
@@ -41,7 +41,7 @@ class Checklist < ApplicationModel
def notify_clients_data_attributes
{
id: id,
ticket_id: ticket_id,
ticket_id: ticket.id,
updated_at: updated_at,
updated_by_id: updated_by_id,
}
@@ -69,12 +69,11 @@ class Checklist < ApplicationModel
# @param target_ticket [Ticket, Integer] target ticket or it's id
# @param user [User] to optionally filter accessible tickets
def self.tickets_referencing(target_ticket, user = nil)
source_ticket_ids = joins(:items)
source_checklist_ids = joins(:items)
.where(items: { ticket: target_ticket })
.group(:id)
.pluck(:ticket_id)
.pluck(:id)
scope = Ticket.where(id: source_ticket_ids)
scope = Ticket.where(checklist_id: source_checklist_ids)
return scope if !user
@@ -90,9 +89,37 @@ class Checklist < ApplicationModel
%w[closed merged].include? state_type.name
end
def self.create_fresh!(ticket)
ActiveRecord::Base.transaction do
Checklist
.create!(ticket:)
.tap { |checklist| checklist.items.create! }
end
end
def self.create_from_template!(ticket, template)
if !template.active
raise Exceptions::UnprocessableEntity, __('Checklist template must be active to use as a checklist starting point.')
end
ActiveRecord::Base.transaction do
Checklist.create!(name: template.name, ticket:)
.tap do |checklist|
sorted_item_ids = template
.items
.map { |elem| checklist.items.create!(text: elem.text, initial_clone: true) }
.pluck(:id)
checklist.update! sorted_item_ids:
end
end
end
private
def update_ticket
return if ticket.destroyed?
ticket.updated_at = Time.current
ticket.save!
end

View File

@@ -10,16 +10,16 @@ class Checklist
if !data[ app_model ]
data[ app_model ] = {}
end
Rails.logger.debug [app_model, id, data[app_model]]
return data if data[ app_model ][ id ]
data[ app_model ][ id ] = attributes_with_association_ids
if ticket && !ticket.authorized_asset?
data[self.class.to_app_model][id]['ticket_inaccessible'] = true
data[app_model][id]['ticket_inaccessible'] = true
end
items.each { |elem| elem.assets(data) }
ticket.assets(data)
data
end

View File

@@ -17,8 +17,8 @@ class Checklist::Item < ApplicationModel
before_validation :detect_ticket_reference, unless: :initial_clone
before_validation :detect_ticket_reference_state
validate :detect_ticket_loop_reference, unless: -> { ticket.blank? || checklist.blank? }
validate :validate_item_count, on: :create, unless: -> { checklist.blank? }
validate :detect_ticket_loop_reference, unless: -> { ticket.blank? }
validate :validate_item_count, on: :create, unless: :initial_clone
# MySQL does not support default value on non-null text columns
# Can be removed after dropping MySQL
@@ -27,7 +27,7 @@ class Checklist::Item < ApplicationModel
after_update :history_update_checked, if: -> { saved_change_to_checked? }
after_destroy :update_checklist_on_destroy
after_destroy :update_referenced_ticket
after_save :update_checklist_on_save
after_save :update_checklist_on_save, unless: :initial_clone
after_save :update_referenced_ticket
@@ -35,7 +35,7 @@ class Checklist::Item < ApplicationModel
def history_log_attributes
{
related_o_id: checklist.ticket_id,
related_o_id: checklist.ticket.id,
related_history_object: 'Ticket',
}
end
@@ -98,7 +98,7 @@ class Checklist::Item < ApplicationModel
end
def detect_ticket_loop_reference
return if ticket.id != checklist.ticket.id
return if checklist_id != ticket.checklist_id
errors.add(:ticket, __('reference must not be the checklist ticket.'))
end

View File

@@ -10,7 +10,6 @@ class Checklist::Item
if !data[ app_model ]
data[ app_model ] = {}
end
Rails.logger.debug [app_model, id, data[app_model]]
return data if data[ app_model ][ id ]
data[ app_model ][ id ] = attributes_with_association_ids

View File

@@ -0,0 +1,17 @@
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
class Checklist
module SearchIndex
extend ActiveSupport::Concern
def search_index_attribute_lookup(include_references: true)
attributes = super
attributes['items'] = items.map do |item|
item.search_index_attribute_lookup(include_references: include_references)
end
attributes
end
end
end

View File

@@ -11,18 +11,20 @@ class ChecklistTemplate < ApplicationModel
validates :name, length: { maximum: 250 }
def create_from_template!(ticket_id:)
raise ActiveRecord::RecordInvalid if !active
new_checklist = Checklist.new(name:, ticket_id:)
# Inherit only the text property from related checklist items.
items.each do |item|
new_checklist.items.build(text: item.text, initial_clone: true)
def replace_items!(new_items)
if new_items.count > 100
raise Exceptions::UnprocessableEntity, __('Checklist Template items are limited to 100 items per checklist.')
end
new_checklist.save!
ActiveRecord::Base.transaction do
items.destroy_all
new_checklist
self.sorted_item_ids = new_items
.compact_blank
.map { |elem| items.create! text: elem.strip }
.map(&:id)
save!
end
end
end

View File

@@ -10,29 +10,8 @@ class ChecklistTemplate::Item < ApplicationModel
# Can be removed after dropping MySQL
before_validation :ensure_text_not_nil, if: -> { ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] == 'mysql2' }
after_create :update_checklist
after_destroy :update_checklist
validate :validate_item_count, on: :create, unless: -> { checklist_template.blank? }
private
def update_checklist
if persisted? && checklist_template.sorted_item_ids.exclude?(id.to_s)
checklist_template.sorted_item_ids << id
end
if !persisted?
checklist_template.sorted_item_ids = checklist_template.sorted_item_ids.reject { |sid| sid.to_s == id.to_s }
end
checklist_template.save!
end
def validate_item_count
return if checklist_template.items.count < 100
errors.add(:base, __('Checklist Template items are limited to 100 items per checklist.'))
end
# MySQL does not support default value on non-null text columns
# Can be removed after dropping MySQL
def ensure_text_not_nil

View File

@@ -80,26 +80,13 @@ returns
end
# get payload based on csv
payload = []
rows.each do |row|
if row.first(2).any?(&:present?)
payload.push(
header.zip(row).to_h
.compact.transform_values(&:strip)
.except(nil).transform_keys(&:to_sym)
.except(*csv_attributes_ignored)
.merge(data[:fixed_params] || {})
)
else
header.zip(row).to_h
payload = rows.map do |row|
header.zip(row).to_h
.compact.transform_values(&:strip)
.except(nil).except('').transform_keys(&:to_sym)
.each do |col, val|
next if val.blank?
payload.last[col] = [*payload.last[col], val]
end
end
.transform_values { |value| (value.include?('~~~') ? value.split('~~~') : value) }
.except(nil).transform_keys(&:to_sym)
.except(*csv_attributes_ignored)
.merge(data[:fixed_params] || {})
end
stats = {
@@ -268,36 +255,17 @@ returns
rows = []
records_attributes_with_association_names.each do |record|
row = []
rows_to_add = []
position = -1
header.each do |key|
position += 1
if record[key].instance_of?(ActiveSupport::TimeWithZone)
row.push record[key].iso8601
next
elsif record[key].instance_of?(Array)
row.push record[key].join('~~~')
else
row.push record[key]
end
if record[key].instance_of?(Array)
entry_count = -2
record[key].each do |entry|
entry_count += 1
next if entry_count == -1
if !rows_to_add[entry_count]
rows_to_add[entry_count] = Array.new(header.count + 1) { '' }
end
rows_to_add[entry_count][position] = entry
end
record[key] = record[key][0]
end
row.push record[key]
end
rows.push row
next if rows_to_add.count.zero?
rows_to_add.each do |item|
rows.push item
end
rows_to_add = []
rows << row
end
require 'csv' # Only load it when it's really needed to save memory.

View File

@@ -6,7 +6,7 @@ class SystemReport::Plugin::PostgreSql < SystemReport::Plugin
def fetch
return if ActiveRecord::Base.connection.adapter_name != 'PostgreSQL'
database_name = Rails.configuration.database_configuration[Rails.env]['database']
database_name = ActiveRecord::Base.connection.current_database
ActiveRecord::Base.connection.execute("
SELECT version() as version,
pg_database_size('#{ActiveRecord::Base.connection.quote_string(database_name)}') as database_size,

View File

@@ -9,6 +9,7 @@ module Taskbar::Validator
def validate_uniqueness
return if local_update
return if %i[user_id app key].none? { |column| will_save_change_to_attribute?(column) }
errors.add(:key, :taken) if taskbar_exist?
end
@@ -23,6 +24,6 @@ module Taskbar::Validator
clause = { user_id: effective_user_id, app:, key: }
record = Taskbar.where(clause)
id.present? ? record.where.not(id:).present? : record.present?
id.present? ? record.where.not(id:).exists? : record.exists?
end
end

View File

@@ -38,6 +38,7 @@ class Ticket::Article < ApplicationModel
belongs_to :origin_by, class_name: 'User', optional: true
before_validation :check_mentions, on: :create
before_validation :check_email_recipient_validity, if: :check_email_recipient_raises_error
before_create :check_subject, :check_body, :check_message_id_md5
before_update :check_subject, :check_body, :check_message_id_md5
after_destroy :store_delete, :update_time_units
@@ -67,7 +68,7 @@ class Ticket::Article < ApplicationModel
:to,
:cc
attr_accessor :should_clone_inline_attachments, :check_mentions_raises_error
attr_accessor :should_clone_inline_attachments, :check_mentions_raises_error, :check_email_recipient_raises_error
alias should_clone_inline_attachments? should_clone_inline_attachments
@@ -384,6 +385,24 @@ returns
end
end
def check_email_recipient_validity
return if Setting.get('import_mode')
# Check if article type is email
email_article_type = Ticket::Article::Type.lookup(name: 'email')
return if type_id != email_article_type.id
# ... and if recipient is valid.
recipient = begin
Mail::Address.new(to).address
rescue Mail::Field::FieldError
# no-op
end
return if EmailAddressValidation.new(recipient).valid?
raise Exceptions::InvalidAttribute.new('email_recipient', __('Sending an email without a valid recipient is not possible.'))
end
def history_log_attributes
{
related_o_id: self['ticket_id'],

View File

@@ -28,7 +28,6 @@ returns
if !data[ app_model ]
data[ app_model ] = {}
end
Rails.logger.debug [app_model, id, data[app_model]]
return data if data[ app_model ][ id ]
data[ app_model ][ id ] = attributes_with_association_ids
@@ -37,16 +36,6 @@ returns
organization&.assets(data)
assets_user(data)
if Setting.get('checklist')
checklist&.assets(data)
referencing_checklists
.includes(:ticket)
.each do |elem|
elem.assets(data)
elem.ticket.assets(data) if elem.ticket.authorized_asset?
end
end
data
end

View File

@@ -6,23 +6,16 @@ module Ticket::Checklists
included do
has_many :referencing_checklist_items, class_name: 'Checklist::Item', dependent: :nullify
has_many :referencing_checklists, class_name: 'Checklist', through: :referencing_checklist_items, source: :checklist
has_one :checklist, dependent: :destroy
belongs_to :checklist, dependent: :destroy, optional: true
after_save :update_referenced_checklist_items
association_attributes_ignored :referencing_checklist_items
end
def attributes_with_association_ids
attributes = super
validates :checklist_id, uniqueness: { allow_nil: true }
return attributes if !Setting.get('checklist')
attributes['checklist_id'] = checklist&.id
attributes['checklist_incomplete'] = checklist&.incomplete
attributes['checklist_total'] = checklist&.total
attributes
validate :ensure_checklist_did_not_exist
end
private
@@ -36,4 +29,12 @@ module Ticket::Checklists
.where(checked: !is_closed)
.each { |elem| elem.update! checked: is_closed }
end
def ensure_checklist_did_not_exist
return if !checklist_id_changed?
# All is good if checklist did not exist before or will not exist afterwards
return if !checklist_id || !checklist_id_was
errors.add :base, __('This ticket already has a checklist.')
end
end

View File

@@ -14,6 +14,11 @@ module Ticket::SearchIndex
# mentions
attributes['mention_user_ids'] = mentions.pluck(:user_id)
# checklists
if checklist
attributes['checklist'] = checklist.search_index_attribute_lookup(include_references: false)
end
# current payload size
total_size_current = 0
@@ -22,18 +27,7 @@ module Ticket::SearchIndex
Ticket::Article.where(ticket_id: id).limit(1000).find_each(batch_size: 50).each do |article|
# lookup attributes of ref. objects (normally name and note)
article_attributes = article.search_index_attribute_lookup(include_references: false)
# remove note needed attributes
ignore = %w[message_id_md5 ticket]
ignore.each do |attribute|
article_attributes.delete(attribute)
end
# index raw text body
if article_attributes['content_type'] && article_attributes['content_type'] == 'text/html' && article_attributes['body']
article_attributes['body'] = article_attributes['body'].html2text
end
article_attributes = search_index_article_attributes(article)
article_attributes_payload_size = article_attributes.to_json.bytes.size
@@ -56,13 +50,7 @@ module Ticket::SearchIndex
# add attachment size to totel payload size
total_size_current += attachment.content.bytes.size
data = {
'size' => attachment.size,
'_name' => attachment.filename,
'_content' => Base64.encode64(attachment.content).delete("\n"),
}
article_attributes['attachment'].push data
article_attributes['attachment'].push search_index_article_attachment_attributes(attachment)
end
attributes['article'].push article_attributes
@@ -109,4 +97,31 @@ module Ticket::SearchIndex
false
end
def search_index_article_attributes(article)
# lookup attributes of ref. objects (normally name and note)
article_attributes = article.search_index_attribute_lookup(include_references: false)
# remove note needed attributes
ignore = %w[message_id_md5 ticket]
ignore.each do |attribute|
article_attributes.delete(attribute)
end
# index raw text body
if article_attributes['content_type'] && article_attributes['content_type'] == 'text/html' && article_attributes['body']
article_attributes['body'] = article_attributes['body'].html2text
end
article_attributes
end
def search_index_article_attachment_attributes(attachment)
{
'size' => attachment.size,
'_name' => attachment.filename,
'_content' => Base64.encode64(attachment.content).delete("\n"),
}
end
end

View File

@@ -33,7 +33,7 @@ create new persistent token
}
)
in case if you use it via an controller, e. g. you can verify via "curl -H "Authorization: Token token=my_token" http://...
in case if you use it via an controller, e. g. you can verify via "curl -H "Authorization: Token token=…" http://…
returns

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