Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f09f838a2 | ||
|
|
762d7688b0 | ||
|
|
8e26723b73 | ||
|
|
8623b79379 | ||
|
|
34c3085115 | ||
|
|
b1a28c201e | ||
|
|
89b6b3f5c0 | ||
|
|
8d794975d4 | ||
|
|
3c53eddaf4 | ||
|
|
fd2b8c3bce | ||
|
|
65810f7ba0 | ||
|
|
366231aaaf | ||
|
|
93a85c0357 | ||
|
|
8db459ce53 | ||
|
|
84133a875f | ||
|
|
4df1e55b18 | ||
|
|
2bda998d6f | ||
|
|
5e2004aa1d | ||
|
|
34e874626a | ||
|
|
5cf2bc4273 | ||
|
|
ca361ad8c2 | ||
|
|
684d8fe3e7 | ||
|
|
b639db89e3 | ||
|
|
cf922c0ce7 | ||
|
|
534391ecf5 | ||
|
|
ada583d0f8 | ||
|
|
5c460457ad | ||
|
|
e22b0f5c6e | ||
|
|
b2946c81dc | ||
|
|
f6fd7a48ad | ||
|
|
74c0d4cf51 | ||
|
|
fdb8ecddca | ||
|
|
3f31b555f1 | ||
|
|
3d46cab28d | ||
|
|
00a4b00194 | ||
|
|
13e4f810d5 | ||
|
|
9622724696 | ||
|
|
d8c0c889c0 | ||
|
|
17803e445f | ||
|
|
0d42c95552 | ||
|
|
9147c3be88 | ||
|
|
ed48331cec | ||
|
|
1d542d1b31 | ||
|
|
40f32aa102 | ||
|
|
3974df9a2d | ||
|
|
238b03fba7 | ||
|
|
f5b7299c2b | ||
|
|
95c6b69d64 | ||
|
|
e7be8d42a0 | ||
|
|
71f992a7fa | ||
|
|
b54fda8ac2 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
61
.gitlab/ci/test/packager.io.yml
Normal file
61
.gitlab/ci/test/packager.io.yml
Normal 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'
|
||||
40
.gitlab/ci/test/performance.yml
Normal file
40
.gitlab/ci/test/performance.yml
Normal 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
15
.gitlab/packager.io/cleanup.sh
Executable 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
|
||||
16
.gitlab/packager.io/docker-compose.yml
Normal file
16
.gitlab/packager.io/docker-compose.yml
Normal 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"]
|
||||
9
.gitlab/packager.io/dockerfiles/Dockerfile.debian
Normal file
9
.gitlab/packager.io/dockerfiles/Dockerfile.debian
Normal 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" ]
|
||||
9
.gitlab/packager.io/dockerfiles/Dockerfile.rhel
Normal file
9
.gitlab/packager.io/dockerfiles/Dockerfile.rhel
Normal 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" ]
|
||||
9
.gitlab/packager.io/dockerfiles/Dockerfile.sles
Normal file
9
.gitlab/packager.io/dockerfiles/Dockerfile.sles
Normal 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" ]
|
||||
9
.gitlab/packager.io/dockerfiles/Dockerfile.ubuntu
Normal file
9
.gitlab/packager.io/dockerfiles/Dockerfile.ubuntu
Normal 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" ]
|
||||
12
.gitlab/packager.io/dockerfiles/bootstrap.debian.sh
Executable file
12
.gitlab/packager.io/dockerfiles/bootstrap.debian.sh
Executable 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
|
||||
26
.gitlab/packager.io/dockerfiles/bootstrap.rhel.sh
Executable file
26
.gitlab/packager.io/dockerfiles/bootstrap.rhel.sh
Executable 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
|
||||
26
.gitlab/packager.io/dockerfiles/bootstrap.sles.sh
Executable file
26
.gitlab/packager.io/dockerfiles/bootstrap.sles.sh
Executable 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
|
||||
12
.gitlab/packager.io/dockerfiles/bootstrap.ubuntu.sh
Normal file
12
.gitlab/packager.io/dockerfiles/bootstrap.ubuntu.sh
Normal 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
|
||||
18
.gitlab/packager.io/dockerfiles/shared.deb.sh
Executable file
18
.gitlab/packager.io/dockerfiles/shared.deb.sh
Executable 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
|
||||
26
.gitlab/packager.io/run_scenarios.sh
Executable file
26
.gitlab/packager.io/run_scenarios.sh
Executable 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
|
||||
7
.gitlab/packager.io/scenarios/internal-services/debian.sh
Executable file
7
.gitlab/packager.io/scenarios/internal-services/debian.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
cd "$(dirname $0)"
|
||||
|
||||
bash ./shared.deb.sh
|
||||
11
.gitlab/packager.io/scenarios/internal-services/rhel.sh
Executable file
11
.gitlab/packager.io/scenarios/internal-services/rhel.sh
Executable 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"
|
||||
13
.gitlab/packager.io/scenarios/internal-services/shared.deb.sh
Executable file
13
.gitlab/packager.io/scenarios/internal-services/shared.deb.sh
Executable 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"
|
||||
11
.gitlab/packager.io/scenarios/internal-services/sles.sh
Executable file
11
.gitlab/packager.io/scenarios/internal-services/sles.sh
Executable 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"
|
||||
7
.gitlab/packager.io/scenarios/internal-services/ubuntu.sh
Executable file
7
.gitlab/packager.io/scenarios/internal-services/ubuntu.sh
Executable 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
110
.gitlab/performance/run_tests.rb
Executable 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
|
||||
@@ -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
|
||||
|
||||
160
CHANGELOG.md
160
CHANGELOG.md
@@ -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)]
|
||||
|
||||
114
Gemfile.lock
114
Gemfile.lock
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ You're going to love Zammad!
|
||||
|
||||
## Status
|
||||
|
||||
- Toolchain:   [](https://docs.zammad.org)
|
||||
- Toolchain: [](https://github.com/zammad/zammad/actions/workflows/ci.yaml) [](https://github.com/zammad/zammad/actions/workflows/docker-release.yaml) [](https://docs.zammad.org)
|
||||
- Docker container images: [](https://hub.docker.com/r/zammad/zammad-docker-compose) [](https://hub.docker.com/r/zammad/zammad-docker-compose/)
|
||||
- Helm chart for Kubernetes: [](https://artifacthub.io/packages/helm/zammad/zammad) [](https://github.com/zammad/zammad-helm/releases)
|
||||
- Download DEB/RPM: [](https://packager.io/gh/zammad/zammad/refs/stable) [](https://packager.io/gh/zammad/zammad/refs/develop)
|
||||
- Docker container images: [](https://hub.docker.com/r/zammad/zammad-docker-compose)
|
||||
- Helm chart for Kubernetes: [](https://artifacthub.io/packages/helm/zammad/zammad)
|
||||
- License: [](https://github.com/zammad/zammad/blob/develop/LICENSE)
|
||||
|
||||
## Installing & Getting Started
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: ->
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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) =>
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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 = '') ->
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %>
|
||||
@@ -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>
|
||||
@@ -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(',')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>'
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,4 +15,5 @@ class ApplicationController < ActionController::Base
|
||||
include ApplicationController::LogsHttpAccess
|
||||
include ApplicationController::Authorizes
|
||||
include ApplicationController::Klass
|
||||
include ApplicationHelper
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -326,6 +326,7 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
config['core_workflow_config'] = CoreWorkflow.config
|
||||
config['icons_url'] = icons_url
|
||||
|
||||
config
|
||||
end
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
9
app/helpers/application_helper.rb
Normal file
9
app/helpers/application_helper.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
17
app/models/checklist/search_index.rb
Normal file
17
app/models/checklist/search_index.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user