Compare commits
254 Commits
develop
...
stable-7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c04934046 | ||
|
|
936403b2db | ||
|
|
599144afb4 | ||
|
|
cf3425712e | ||
|
|
5e6e966599 | ||
|
|
c2bba6fdc0 | ||
|
|
9dc15acf23 | ||
|
|
e50ef69bea | ||
|
|
e661d00efb | ||
|
|
b225881b9a | ||
|
|
13ccc136e5 | ||
|
|
a04a95b694 | ||
|
|
4a9dd5f8a8 | ||
|
|
3f6e8a5fa3 | ||
|
|
4ccd5d5730 | ||
|
|
c767f32c00 | ||
|
|
b059bd1cac | ||
|
|
1fae97d924 | ||
|
|
61d2c42696 | ||
|
|
7b6c0adec0 | ||
|
|
f059eb2d98 | ||
|
|
02c1ff8dc6 | ||
|
|
3cdf1b08a0 | ||
|
|
0fa7d753b0 | ||
|
|
f87b42752e | ||
|
|
b38ecfe2ef | ||
|
|
4a373e663e | ||
|
|
b2cfa6f350 | ||
|
|
09b2a58786 | ||
|
|
9afb6253ae | ||
|
|
61708c75d0 | ||
|
|
f86350e6f4 | ||
|
|
ce735a391e | ||
|
|
dfb9e49bad | ||
|
|
e917c72c99 | ||
|
|
67cd76bed7 | ||
|
|
ef127b2e18 | ||
|
|
a58794c228 | ||
|
|
52ce6eeb97 | ||
|
|
b009f5eb72 | ||
|
|
e24b963134 | ||
|
|
488feb53ec | ||
|
|
7d174a0e84 | ||
|
|
a770125d42 | ||
|
|
622f6eb0e1 | ||
|
|
6e54559043 | ||
|
|
9f7ffd99d9 | ||
|
|
6c48620951 | ||
|
|
17c9d075e8 | ||
|
|
76f4f9157a | ||
|
|
332bba5afd | ||
|
|
7cd61fee9f | ||
|
|
6e22cbb2cd | ||
|
|
d438e0a9db | ||
|
|
2453ea2642 | ||
|
|
2de9362f50 | ||
|
|
57af4f9573 | ||
|
|
3b45354591 | ||
|
|
9a75d3afc1 | ||
|
|
82dee637e7 | ||
|
|
acbbc70745 | ||
|
|
1f68767300 | ||
|
|
78ad4e429d | ||
|
|
46e5d2e079 | ||
|
|
df77241aa1 | ||
|
|
5e40f9cb70 | ||
|
|
d6167f95f5 | ||
|
|
52a7fc39a9 | ||
|
|
4a8609d41f | ||
|
|
82f35c35f3 | ||
|
|
cbd48bd198 | ||
|
|
e0dddc16e3 | ||
|
|
1b6c4e9523 | ||
|
|
8b800ddb29 | ||
|
|
6b87aa9bbd | ||
|
|
aa2a818162 | ||
|
|
c317f2a8cf | ||
|
|
bacc6a9e81 | ||
|
|
8003e5f178 | ||
|
|
a7e3888aef | ||
|
|
8f0b37bcd5 | ||
|
|
51e6b36ea9 | ||
|
|
8a5307eba4 | ||
|
|
f75a86766c | ||
|
|
ea46e905a9 | ||
|
|
a8a3058794 | ||
|
|
81b38d6a84 | ||
|
|
d6c89953f3 | ||
|
|
6a5ac49588 | ||
|
|
2f46ad76cf | ||
|
|
1e734d1ab6 | ||
|
|
c3cbedea23 | ||
|
|
16a4a2e1be | ||
|
|
f4bacb312e | ||
|
|
c3015bc4f9 | ||
|
|
2ecf1e6812 | ||
|
|
0780538e04 | ||
|
|
a0a4a2b05e | ||
|
|
55467d2977 | ||
|
|
e6ddc624c6 | ||
|
|
ca817321ac | ||
|
|
ee2b8445b7 | ||
|
|
c414d7b2d8 | ||
|
|
8265be04d7 | ||
|
|
d3a75b7a64 | ||
|
|
730cd0c9ca | ||
|
|
2de087fdb2 | ||
|
|
e8a57b6989 | ||
|
|
4251d17d39 | ||
|
|
9b8f2722e6 | ||
|
|
7157e07858 | ||
|
|
59519b71aa | ||
|
|
4c3df3b227 | ||
|
|
d1907eaacf | ||
|
|
a40c879deb | ||
|
|
bc1b294a01 | ||
|
|
dfec1746e3 | ||
|
|
650585fa7a | ||
|
|
f782bb8090 | ||
|
|
6ef1671c0e | ||
|
|
fea86c2a9f | ||
|
|
b82372af1a | ||
|
|
e0d5fd3a3f | ||
|
|
c0c8a40738 | ||
|
|
b35be827b0 | ||
|
|
8a2664c9d1 | ||
|
|
3edeb40f6d | ||
|
|
c598c8095b | ||
|
|
ecead9532f | ||
|
|
bc615144ae | ||
|
|
fd06a330c1 | ||
|
|
02e28071d3 | ||
|
|
276496e637 | ||
|
|
5b45d98881 | ||
|
|
5235f5cb9b | ||
|
|
81e67d9139 | ||
|
|
10facc67f1 | ||
|
|
ac644a7bb3 | ||
|
|
076aa21b3a | ||
|
|
d530e2fdcf | ||
|
|
646ee56b69 | ||
|
|
7910ed7d22 | ||
|
|
6e4cf99565 | ||
|
|
c1a1c897a4 | ||
|
|
67118383b4 | ||
|
|
06e846c184 | ||
|
|
4e6d3d7822 | ||
|
|
113beb30ae | ||
|
|
9143bd74ad | ||
|
|
3c95b7da4e | ||
|
|
71cc9d5f0f | ||
|
|
680557e836 | ||
|
|
be60725a69 | ||
|
|
45b265b015 | ||
|
|
70789f0ec9 | ||
|
|
ab53d4894e | ||
|
|
c4c2445148 | ||
|
|
86e2533e7b | ||
|
|
5c6cef2064 | ||
|
|
6a1d6690ab | ||
|
|
13e12b474c | ||
|
|
ab9d33954e | ||
|
|
5fa48c14e2 | ||
|
|
bc96ad9b3a | ||
|
|
f3228274c4 | ||
|
|
0bfa74834a | ||
|
|
65a5bb918b | ||
|
|
017c272df3 | ||
|
|
d588f05fd5 | ||
|
|
df804e73d5 | ||
|
|
c1d4349bb7 | ||
|
|
bb071d92ad | ||
|
|
db3e381e34 | ||
|
|
df2ce8e456 | ||
|
|
cddddeb79e | ||
|
|
e3ab7eda0a | ||
|
|
d43ff878d9 | ||
|
|
ceae43f736 | ||
|
|
130211021f | ||
|
|
df3660840b | ||
|
|
3aab532cfd | ||
|
|
d0abfc34c0 | ||
|
|
fd3fa7a568 | ||
|
|
c88b481601 | ||
|
|
597f2b939a | ||
|
|
75446f5acf | ||
|
|
cc3f957068 | ||
|
|
7f3c5b1c74 | ||
|
|
9531d33e8c | ||
|
|
8450a87113 | ||
|
|
16244b0d99 | ||
|
|
0b1d7f8850 | ||
|
|
cd10579929 | ||
|
|
f0c09af711 | ||
|
|
77f0f33b05 | ||
|
|
3527c24e91 | ||
|
|
e53d64267d | ||
|
|
2043f46496 | ||
|
|
49457623e3 | ||
|
|
7d6dead38f | ||
|
|
61a37f1d7a | ||
|
|
5e3f78fbbb | ||
|
|
006c5df722 | ||
|
|
67995d28fc | ||
|
|
8e0993ce98 | ||
|
|
352be82348 | ||
|
|
6e657827e5 | ||
|
|
8cdd4e365c | ||
|
|
d71c2cb037 | ||
|
|
7eab068347 | ||
|
|
2f629ed7b1 | ||
|
|
0110b3f429 | ||
|
|
839c97b5fe | ||
|
|
6a4281287a | ||
|
|
b0368f6811 | ||
|
|
f44931d8a0 | ||
|
|
da85f46ca8 | ||
|
|
8284364d66 | ||
|
|
51b5289f53 | ||
|
|
eebdfec5ee | ||
|
|
4629dfade5 | ||
|
|
172fb27655 | ||
|
|
1e5068c795 | ||
|
|
cae36fccbc | ||
|
|
f5120d6dd8 | ||
|
|
7885467e17 | ||
|
|
467a78a1a6 | ||
|
|
0ecd35551e | ||
|
|
c625a55a81 | ||
|
|
27525e1d1d | ||
|
|
31d2be40f6 | ||
|
|
9428c0a32a | ||
|
|
f9eb947699 | ||
|
|
8579ce7a1b | ||
|
|
6e4daffb0a | ||
|
|
2810e507e2 | ||
|
|
4ef08f051c | ||
|
|
55b64ccf2d | ||
|
|
d4ca6c1947 | ||
|
|
f1604c16e1 | ||
|
|
ff5f127fcd | ||
|
|
f2b5556fa1 | ||
|
|
95b2fd48ec | ||
|
|
6a699a0096 | ||
|
|
5ba5dc26d8 | ||
|
|
aecebc6b10 | ||
|
|
f7ee8d7e9c | ||
|
|
89c2f8f238 | ||
|
|
d86ad289d0 | ||
|
|
0776909ad6 | ||
|
|
1767a194c9 | ||
|
|
01ecdd5cdf | ||
|
|
15937a00d1 | ||
|
|
1ad526ef41 |
@@ -1,2 +1,2 @@
|
||||
https://github.com/heroku/heroku-buildpack-nodejs.git#v280
|
||||
https://github.com/heroku/heroku-buildpack-nodejs.git#v340
|
||||
https://github.com/pkgr/heroku-buildpack-ruby.git#v267-1
|
||||
10
.coderabbit.yml
Normal file
10
.coderabbit.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
inheritance: true
|
||||
reviews:
|
||||
review_status: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
base_branches:
|
||||
- 'develop'
|
||||
ignore_usernames:
|
||||
- 'renovatebot'
|
||||
drafts: false
|
||||
@@ -23,7 +23,7 @@ Purpose: Give an AI coding agent a fast, reliable mental model of this repo to s
|
||||
- Legacy desktop-app: CoffeeScript, Spine.js, Sprockets, REST API
|
||||
(see [`../app/assets/`](../app/assets/), [`../coffeelint.json`](../coffeelint.json), QUnit in `../test/`)
|
||||
- New desktop-view and mobile apps: Vue 3, TypeScript, Pinia, Apollo Client (GraphQL), Tailwind CSS, VueUse,
|
||||
Vitest, Testing Library, Cypress, pnpm, vite-plugin-ruby, vite-plugin-pwa, ESLint, Stylelint, Prettier
|
||||
Vitest, Testing Library, Cypress, pnpm, vite-plugin-ruby, vite-plugin-pwa, ESLint, Stylelint, Oxfmt
|
||||
(see [`../package.json`](../package.json), [`../vite.config.mjs`](../vite.config.mjs),
|
||||
[`../tsconfig.base.json`](../tsconfig.base.json), [`../eslint.config.ts`](../eslint.config.ts))
|
||||
- Backend: Ruby on Rails, PostgreSQL, Redis, ActionCable, Delayed Job, GraphQL
|
||||
@@ -86,7 +86,7 @@ Purpose: Give an AI coding agent a fast, reliable mental model of this repo to s
|
||||
- Use path aliases from [`../tsconfig.base.json`](../tsconfig.base.json).
|
||||
- Do not cross-import between desktop/mobile apps (ESLint enforces boundaries).
|
||||
- Use Vitest and Testing Library for unit/component tests ([`../app/frontend/tests/`](../app/frontend/tests/)).
|
||||
- Use Tailwind CSS utilities for styling. Lint with Stylelint and Prettier.
|
||||
- Use Tailwind CSS utilities for styling. Lint with Stylelint and Oxfmt.
|
||||
- For i18n, wrap user-facing strings and see [`../eslint.config.ts`](../eslint.config.ts) for rules.
|
||||
|
||||
## When in doubt
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
version: '3.6'
|
||||
|
||||
---
|
||||
volumes:
|
||||
visual-regression-dependencies-amd64:
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
version: '3.6'
|
||||
|
||||
---
|
||||
volumes:
|
||||
visual-regression-dependencies-arm:
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
module CanCheck51PlusMigrations
|
||||
VERSION_THRESHOLD = 5.1
|
||||
|
||||
# Apply to 7.0 migrations and newer
|
||||
MIGRATION_THRESHOLD = 20250519095403 # rubocop:disable Style/NumericLiterals
|
||||
|
||||
def migration_51_or_newer?(node)
|
||||
return false if old_migration?
|
||||
|
||||
migration?(node) && rails_51_or_newer?(node)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def migration?(node)
|
||||
superclass(node)&.children&.at(0)&.const_name == 'ActiveRecord::Migration'
|
||||
end
|
||||
|
||||
def old_migration?
|
||||
file_timestamp = File.basename(processed_source.file_path)[%r{\A\d+}]
|
||||
|
||||
return false if !file_timestamp
|
||||
|
||||
file_timestamp.to_i < MIGRATION_THRESHOLD
|
||||
end
|
||||
|
||||
def rails_51_or_newer?(node)
|
||||
klass = superclass(node)
|
||||
|
||||
return false if !klass.send_type?
|
||||
|
||||
klass.arguments.first.value.to_f >= VERSION_THRESHOLD
|
||||
end
|
||||
|
||||
def superclass(node)
|
||||
node
|
||||
.each_ancestor(:class)
|
||||
.first
|
||||
&.parent_class
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'concerns/can_check_51_plus_migrations'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class MigrationEnsureIntegerForeignKey < Base
|
||||
extend AutoCorrector
|
||||
include CanCheck51PlusMigrations
|
||||
|
||||
MSG = 'Rails 5.1+ migrations must add type: :integer to references setup.'
|
||||
|
||||
RESTRICT_ON_SEND = %i[
|
||||
references
|
||||
belongs_to
|
||||
add_reference
|
||||
add_references
|
||||
].freeze
|
||||
|
||||
def on_send(node)
|
||||
return if !migration_51_or_newer?(node)
|
||||
|
||||
options = node.arguments.last
|
||||
|
||||
pair_with_type = nil
|
||||
|
||||
if options&.hash_type?
|
||||
options.pairs.each do |pair|
|
||||
next if !(pair.key.sym_type? && pair.key.value == :type)
|
||||
|
||||
pair_with_type = pair
|
||||
next if pair.value.source == ':integer'
|
||||
next if pair.value.source == ':bigint' # allow explicit bigint foreign keys
|
||||
|
||||
add_offense(pair.value) do |corrector|
|
||||
corrector.replace(pair.value, ':integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return if pair_with_type
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.insert_after(node.arguments.last.source_range, ', type: :integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'concerns/can_check_51_plus_migrations'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class MigrationEnsureIntegerPrimaryKey < Base
|
||||
extend AutoCorrector
|
||||
include CanCheck51PlusMigrations
|
||||
|
||||
MSG = 'Rails 5.1+ migrations must add id: :integer to ensure primary key is integer, not bigint.'
|
||||
|
||||
RESTRICT_ON_SEND = %i[create_table].freeze
|
||||
|
||||
def on_send(node)
|
||||
return if !migration_51_or_newer?(node)
|
||||
|
||||
options = node.arguments.last
|
||||
|
||||
pair_with_id = nil
|
||||
|
||||
if options&.hash_type?
|
||||
options.pairs.each do |pair|
|
||||
next if !(pair.key.sym_type? && pair.key.value == :id)
|
||||
|
||||
pair_with_id = pair
|
||||
next if pair.value.source == ':integer'
|
||||
next if pair.value.source == 'false' # allow id: false for ID-less tables
|
||||
next if pair.value.source == ':bigserial' # allow id: :bigserial for explicit bigint primary keys
|
||||
|
||||
add_offense(pair.value) do |corrector|
|
||||
corrector.replace(pair.value, ':integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return if pair_with_id
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.insert_after(node.arguments.first.source_range, ', id: :integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -531,6 +531,16 @@ Zammad/GraphqlCsrfSkipVerification:
|
||||
Include:
|
||||
- "app/graphql/gql/**/*.rb"
|
||||
|
||||
Zammad/MigrationEnsureIntegerPrimaryKey:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "db/migrate/*.rb"
|
||||
|
||||
Zammad/MigrationEnsureIntegerForeignKey:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "db/migrate/*.rb"
|
||||
|
||||
Capybara/NegationMatcherAfterVisit:
|
||||
Description: 'Allow to use negated matcher after visit'
|
||||
Enabled: false
|
||||
|
||||
@@ -30,3 +30,5 @@ require_relative 'cop/zammad/to_forbid_over_not_to_permit'
|
||||
require_relative 'cop/zammad/trigger_from_commit_hooks'
|
||||
require_relative 'cop/zammad/update_copyright'
|
||||
require_relative 'cop/zammad/ensures_time_zone_at_spec_top_level'
|
||||
require_relative 'cop/zammad/migration_ensure_integer_primary_key'
|
||||
require_relative 'cop/zammad/migration_ensure_integer_foreign_key'
|
||||
|
||||
@@ -50,7 +50,6 @@ Metrics/AbcSize:
|
||||
- 'app/models/avatar.rb'
|
||||
- 'app/models/calendar.rb'
|
||||
- 'app/models/channel.rb'
|
||||
- 'app/models/channel/assets.rb'
|
||||
- 'app/models/channel/email_build.rb'
|
||||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/channel/filter/auto_response_check.rb'
|
||||
@@ -260,7 +259,6 @@ Metrics/CyclomaticComplexity:
|
||||
- 'app/models/avatar.rb'
|
||||
- 'app/models/calendar.rb'
|
||||
- 'app/models/channel.rb'
|
||||
- 'app/models/channel/assets.rb'
|
||||
- 'app/models/channel/email_build.rb'
|
||||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/channel/filter/auto_response_check.rb'
|
||||
@@ -383,7 +381,6 @@ Metrics/PerceivedComplexity:
|
||||
- 'app/models/avatar.rb'
|
||||
- 'app/models/calendar.rb'
|
||||
- 'app/models/channel.rb'
|
||||
- 'app/models/channel/assets.rb'
|
||||
- 'app/models/channel/email_build.rb'
|
||||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/channel/filter/auto_response_check.rb'
|
||||
|
||||
@@ -42,7 +42,7 @@ services:
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Expose the ES service for direct access if needed, but avoid conflicts with host services.
|
||||
- 19200:9200
|
||||
- 127.0.0.1:19200:9200
|
||||
volumes:
|
||||
- elasticsearch-data:/usr/share/elasticsearch/data
|
||||
|
||||
@@ -69,4 +69,4 @@ volumes:
|
||||
elasticsearch-data:
|
||||
driver: local
|
||||
postgresql-data:
|
||||
driver: local
|
||||
driver: local
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"dbaeumer.vscode-eslint",
|
||||
"dotjoshjohnson.xml",
|
||||
"eamodio.gitlens",
|
||||
"esbenp.prettier-vscode",
|
||||
"ezforo.copy-relative-path-and-line-numbers",
|
||||
"jock.svg",
|
||||
"kaiwood.endwise",
|
||||
|
||||
83
.devcontainer/with-mailserver/auto_wizard_mailserver.json
Normal file
83
.devcontainer/with-mailserver/auto_wizard_mailserver.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"Users": [
|
||||
{
|
||||
"login": "admin@example.com",
|
||||
"firstname": "Test Admin",
|
||||
"lastname": "Agent",
|
||||
"email": "admin@example.com",
|
||||
"password": "test"
|
||||
},
|
||||
{
|
||||
"login": "agent1@example.com",
|
||||
"firstname": "Agent 1",
|
||||
"lastname": "Test",
|
||||
"email": "agent1@example.com",
|
||||
"password": "test",
|
||||
"roles": ["Agent"]
|
||||
}
|
||||
],
|
||||
"Groups": [
|
||||
{
|
||||
"name": "some group1",
|
||||
"users": ["admin@example.com", "agent1@example.com"]
|
||||
},
|
||||
{
|
||||
"name": "Users",
|
||||
"users": ["admin@example.com", "agent1@example.com"],
|
||||
"signature": "default",
|
||||
"email_address_id": 1
|
||||
}
|
||||
],
|
||||
"Channels": [
|
||||
{
|
||||
"id": 3,
|
||||
"area": "Email::Account",
|
||||
"group": "Users",
|
||||
"active": true,
|
||||
"options": {
|
||||
"inbound": {
|
||||
"adapter": "imap",
|
||||
"options": {
|
||||
"host": "mail.test.local",
|
||||
"port": "993",
|
||||
"ssl": "ssl",
|
||||
"user": "zammad@test.local",
|
||||
"password": "zammad",
|
||||
"ssl_verify": true
|
||||
}
|
||||
},
|
||||
"outbound": {
|
||||
"adapter": "smtp",
|
||||
"options": {
|
||||
"host": "mail.test.local",
|
||||
"port": "465",
|
||||
"user": "zammad@test.local",
|
||||
"password": "zammad",
|
||||
"ssl_verify": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"EmailAddresses": [
|
||||
{
|
||||
"id": 1,
|
||||
"channel_id": 3,
|
||||
"name": "Zammad Helpdesk",
|
||||
"email": "zammad@test.local"
|
||||
}
|
||||
],
|
||||
"Settings": [
|
||||
{
|
||||
"name": "product_name",
|
||||
"value": "Zammad Test System"
|
||||
},
|
||||
{
|
||||
"name": "developer_mode",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"TextModuleLocale": {
|
||||
"Locale": "de-de"
|
||||
}
|
||||
}
|
||||
0
.devcontainer/with-mailserver/certs/.gitkeep
Normal file
0
.devcontainer/with-mailserver/certs/.gitkeep
Normal file
46
.devcontainer/with-mailserver/compose.yaml
Normal file
46
.devcontainer/with-mailserver/compose.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
name: "zammad-devcontainer"
|
||||
|
||||
include: [../default/compose.yaml]
|
||||
|
||||
services:
|
||||
mailserver-init:
|
||||
image: ghcr.io/docker-mailserver/docker-mailserver:latest
|
||||
entrypoint: ["/bin/sh", "-c", "sh /tmp/docker-mailserver/scripts/init-mailserver.sh"]
|
||||
environment:
|
||||
ACCOUNT_PROVISIONER: FILE
|
||||
volumes:
|
||||
- ./scripts:/tmp/docker-mailserver/scripts:ro
|
||||
- ./certs:/tmp/docker-mailserver/certs
|
||||
- mailserver-config:/tmp/docker-mailserver
|
||||
restart: no
|
||||
|
||||
mailserver:
|
||||
image: ghcr.io/docker-mailserver/docker-mailserver:latest
|
||||
hostname: mail.test.local
|
||||
domainname: test.local
|
||||
environment:
|
||||
SSL_TYPE: manual
|
||||
SSL_CERT_PATH: /tmp/docker-mailserver/certs/mail.test.local.crt
|
||||
SSL_KEY_PATH: /tmp/docker-mailserver/certs/mail.test.local.key
|
||||
ENABLE_IMAP: "1"
|
||||
ENABLE_POP3: "1"
|
||||
DMS_DEBUG: "0"
|
||||
ONE_DIR: "1"
|
||||
POSTMASTER_ADDRESS: postmaster@test.local
|
||||
OVERRIDE_HOSTNAME: mail.test.local
|
||||
ACCOUNT_PROVISIONER: FILE
|
||||
depends_on:
|
||||
mailserver-init:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- 127.0.0.1:1465:465 # SMTP (SSL)
|
||||
- 127.0.0.1:1587:587 # SMTP (STARTTLS)
|
||||
- 127.0.0.1:1993:993 # IMAP (SSL)
|
||||
volumes:
|
||||
- ./certs:/tmp/docker-mailserver/certs
|
||||
- mailserver-config:/tmp/docker-mailserver
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mailserver-config:
|
||||
driver: local
|
||||
30
.devcontainer/with-mailserver/devcontainer.json
Normal file
30
.devcontainer/with-mailserver/devcontainer.json
Normal file
@@ -0,0 +1,30 @@
|
||||
// For format details, see https://containers.dev/implementors/json_reference/.
|
||||
// For config options, see the README at: https://github.com/devcontainers/templates/tree/main/src/ruby
|
||||
{
|
||||
"name": "Zammad (with Mailserver)",
|
||||
"dockerComposeFile": "compose.yaml",
|
||||
"service": "devcontainer",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"features": {
|
||||
"../features/zammad-deps": {},
|
||||
"../features/add-bin-path": {
|
||||
"addBinPath": "/workspaces/${localWorkspaceFolderBasename}/bin"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
// Avoid conflicts with host-installed node_modules with a different platform/architecture.
|
||||
"source": "node-modules",
|
||||
"target": "/workspaces/${localWorkspaceFolderBasename}/node_modules",
|
||||
"type": "volume"
|
||||
}
|
||||
],
|
||||
"containerEnv": {
|
||||
// ZAMMAD_MAIL_TO_FILE is for debugging outgoing mails.
|
||||
// It allows to easily inspect contents of the outgoing messages that use `sendmail` adapter,
|
||||
// by writing them to files instead of sending them. Check the `tmp/mails` directory for the generated files.
|
||||
"ZAMMAD_MAIL_TO_FILE": "1"
|
||||
},
|
||||
"forwardPorts": [3000, 3001, 3036, 6042],
|
||||
"postCreateCommand": "/workspaces/${localWorkspaceFolderBasename}/.devcontainer/with-mailserver/scripts/post_create.sh"
|
||||
}
|
||||
42
.devcontainer/with-mailserver/scripts/init-mailserver.sh
Normal file
42
.devcontainer/with-mailserver/scripts/init-mailserver.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
CERT_DIR=/tmp/docker-mailserver/certs
|
||||
CONFIG_DIR=/tmp/docker-mailserver
|
||||
CERT_FILE="$CERT_DIR/mail.test.local.crt"
|
||||
KEY_FILE="$CERT_DIR/mail.test.local.key"
|
||||
POSTFIX_ACCOUNTS_FILE="$CONFIG_DIR/postfix-accounts.cf"
|
||||
POSTFIX_VIRTUAL_FILE="$CONFIG_DIR/postfix-virtual.cf"
|
||||
POSTFIX_MAIN_FILE="$CONFIG_DIR/postfix-main.cf"
|
||||
|
||||
mkdir -p "$CERT_DIR"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ]; then
|
||||
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
|
||||
-keyout "$KEY_FILE" \
|
||||
-out "$CERT_FILE" \
|
||||
-subj "/CN=mail.test.local" \
|
||||
-addext "subjectAltName=DNS:mail.test.local,DNS:localhost"
|
||||
fi
|
||||
|
||||
if [ ! -f "$POSTFIX_ACCOUNTS_FILE" ]; then
|
||||
touch "$POSTFIX_ACCOUNTS_FILE"
|
||||
setup email add zammad@test.local zammad
|
||||
elif ! setup email list | grep -q 'zammad@test.local$'; then
|
||||
setup email add zammad@test.local zammad
|
||||
fi
|
||||
|
||||
if [ ! -f "$POSTFIX_VIRTUAL_FILE" ]; then
|
||||
touch "$POSTFIX_VIRTUAL_FILE"
|
||||
setup alias add zammad@localhost zammad@test.local
|
||||
elif ! setup alias list | grep -q 'zammad@localhost'; then
|
||||
setup alias add zammad@localhost zammad@test.local
|
||||
fi
|
||||
|
||||
cat > "$POSTFIX_MAIN_FILE" <<'EOF'
|
||||
smtpd_recipient_restrictions = permit_sasl_authenticated,reject
|
||||
relay_transport = discard:
|
||||
default_transport = discard:
|
||||
EOF
|
||||
21
.devcontainer/with-mailserver/scripts/post_create.sh
Executable file
21
.devcontainer/with-mailserver/scripts/post_create.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
env
|
||||
|
||||
# Ensure proper permissions on root-mounted volume.
|
||||
sudo chown -R "${USER}" node_modules
|
||||
|
||||
bin/setup --skip-server
|
||||
|
||||
bundle exec bootsnap precompile --gemfile app/ lib/
|
||||
|
||||
bundle exec rails r "Setting.set('es_url', 'http://elasticsearch:9200')"
|
||||
|
||||
bundle exec rails zammad:setup:auto_wizard[.devcontainer/with-mailserver/auto_wizard_mailserver.json]
|
||||
|
||||
CERT_PATH=.devcontainer/with-mailserver/certs/mail.test.local.crt
|
||||
|
||||
# Import self-signed certificate for mailserver.
|
||||
if [ -f "$CERT_PATH" ]; then
|
||||
bundle exec rails r "SSLCertificate.create!(certificate: Rails.root.join('$CERT_PATH').read)"
|
||||
fi
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
ollama:
|
||||
image: docker.io/ollama/ollama:latest
|
||||
ports:
|
||||
- 11434:11434
|
||||
- 127.0.0.1:11434:11434
|
||||
volumes:
|
||||
- ollama-data:/root/.ollama
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
SE_NODE_OVERRIDE_MAX_SESSIONS: 'true'
|
||||
SE_VNC_NO_PASSWORD: 'true'
|
||||
ports:
|
||||
- 4444:4444
|
||||
- 127.0.0.1:4444:4444
|
||||
restart: unless-stopped
|
||||
|
||||
selenium-firefox:
|
||||
@@ -23,5 +23,5 @@ services:
|
||||
SE_OPTS: '--port 4445'
|
||||
SE_VNC_NO_PASSWORD: 'true'
|
||||
ports:
|
||||
- 4445:4445
|
||||
- 127.0.0.1:4445:4445
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -23,7 +23,7 @@ const year = new Date().getFullYear()
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parser: require('vue-eslint-parser'), // required to parse .vue files properly otherwise js-parser will be used
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
ruleTester.run('zammad-copyright', rule, {
|
||||
|
||||
@@ -56,8 +56,7 @@ ruleTester.run('zammad-detect-translatable-string', rule, {
|
||||
code: `'String that should be translatable'`,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'This string looks like it should be marked as translatable via __(...)',
|
||||
message: 'This string looks like it should be marked as translatable via __(...)',
|
||||
},
|
||||
],
|
||||
output: `__('String that should be translatable')`,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
|
||||
/**
|
||||
* @fileoverview Enforce kebab-case for Symbol descriptors
|
||||
* @author Benjamin Scharf
|
||||
@@ -21,7 +20,7 @@ const rule = require('../../../lib/rules/zammad-symbol-description.js')
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parser: require('vue-eslint-parser'), // required to parse .vue files properly otherwise js-parser will be used
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
ruleTester.run('zammad-symbol-descriptor', rule, {
|
||||
|
||||
@@ -17,8 +17,7 @@ const rule = require('../../../lib/rules/zammad-tailwind-ltr.js')
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const error =
|
||||
'When positioning classes are used, they must be prefixed with ltr/rtl.'
|
||||
const error = 'When positioning classes are used, they must be prefixed with ltr/rtl.'
|
||||
|
||||
const ruleTester = new RuleTester()
|
||||
ruleTester.run('zammad-tailwind-ltr', rule, {
|
||||
|
||||
4
.github/workflows/docker-release.yaml
vendored
4
.github/workflows/docker-release.yaml
vendored
@@ -137,7 +137,9 @@ jobs:
|
||||
type=sha,suffix=-arm64
|
||||
|
||||
- name: Get incrementing Zammad version from the most recent tag
|
||||
run: echo ZAMMAD_VERSION="$(git describe --tags | sed -e 's/-[a-z0-9]\{8,\}.*//g')" >> $GITHUB_ENV
|
||||
# Make sure the number is always present (via --long), and pad it to four digits
|
||||
# for proper alphabetical sorting in tools like updatecli (e.g. 7.0.0-0000, 7.1.0-0215).
|
||||
run: echo ZAMMAD_VERSION="$(git describe --tags --long | awk -F'-' '{printf "%s-%04d\n", $1, $(NF-1)}')" >> $GITHUB_ENV
|
||||
|
||||
- name: Get Zammad major.minor version from the most recent tag
|
||||
run: echo ZAMMAD_VERSION_MAJOR_MINOR="$(git describe --tags | cut -f1-2 -d.)" >> $GITHUB_ENV
|
||||
|
||||
11
.github/workflows/packager.io.yaml
vendored
11
.github/workflows/packager.io.yaml
vendored
@@ -3,11 +3,16 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- stable*
|
||||
# Test builds for public branches based on develop. Don't forget to clean up test branches in go.packager.io after merging.
|
||||
- develop-*
|
||||
schedule:
|
||||
# Build for develop only once per workday to save space in packagehall.
|
||||
- cron: '20 6 * * 1-5'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'zammad/zammad'
|
||||
@@ -34,7 +39,7 @@ jobs:
|
||||
VERSION=$(cat VERSION)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
- name: Package
|
||||
uses: pkgr/action/package@main
|
||||
uses: pkgr/action/package@v1
|
||||
id: package
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
@@ -45,10 +50,10 @@ jobs:
|
||||
DATABASE_URL=postgresql://zammad:/zammad
|
||||
ZAMMAD_SAFE_MODE=1
|
||||
- name: Publish
|
||||
uses: pkgr/action/publish@main
|
||||
uses: pkgr/action/publish@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
token: ${{ secrets.PACKAGER_PUBLISH_TOKEN }}
|
||||
repository: zammad/zammad
|
||||
channel: ${{ github.ref_name }}
|
||||
file: ${{ steps.package.outputs.package_path }}
|
||||
file: ${{ steps.package.outputs.package_path }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -122,3 +122,7 @@ schema.graphql
|
||||
|
||||
# Packages
|
||||
*.zpm
|
||||
|
||||
# Devcontainer
|
||||
/.devcontainer/with-mailserver/certs/*
|
||||
!/.devcontainer/with-mailserver/certs/.gitkeep
|
||||
|
||||
@@ -103,6 +103,8 @@
|
||||
- pnpm lint:ts
|
||||
- echo "ESLint check…"
|
||||
- pnpm lint:js
|
||||
- echo "Oxfmt check…"
|
||||
- pnpm format:check
|
||||
- pnpm analyse:vite-bundle -t list -o ./tmp/vite-bundle-stats.yml
|
||||
- .gitlab/verify_vite_bundle_size.rb
|
||||
# Artifacts are stored for failed jobs
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
curl https://raw.githubusercontent.com/zammad/zammad-docker-compose/master/docker-compose.yml > docker-compose.yml
|
||||
cat - <<YML_FILE > docker-compose.review-app.yml
|
||||
---
|
||||
version: '3.8'
|
||||
services:
|
||||
zammad-init:
|
||||
environment:
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
set -eu
|
||||
|
||||
# Workaround: postgresql fails on rhel-9 when it gets installed together with Zammad.
|
||||
dnf install -y postgresql-server
|
||||
|
||||
dnf install -y zammad
|
||||
|
||||
curl --retry 30 --retry-delay 1 --retry-connrefused http://localhost:3000 | grep "Zammad Helpdesk"
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
"pinia",
|
||||
"/^@vue/",
|
||||
"/^vue/",
|
||||
"!@vue/eslint-config-prettier",
|
||||
"!@vue/eslint-config-typescript",
|
||||
"!@vuepic/vue-datepicker"
|
||||
],
|
||||
@@ -97,7 +96,7 @@
|
||||
"groupSlug": "vue"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["@vue/eslint-config-prettier", "@vue/eslint-config-typescript"],
|
||||
"matchPackageNames": ["@vue/eslint-config-typescript"],
|
||||
"groupName": "Vue ESLint Configs",
|
||||
"groupSlug": "vue-eslint-configs"
|
||||
},
|
||||
|
||||
@@ -95,6 +95,17 @@ PreCommit:
|
||||
include:
|
||||
- 'app/frontend/**/*'
|
||||
- '.eslint-plugin-zammad/**/*'
|
||||
Oxfmt:
|
||||
required_executable: 'pnpm'
|
||||
enabled: true
|
||||
command: ['pnpm', 'format:check:cmd']
|
||||
flags: ['app/frontend/', '.eslint-plugin-zammad/']
|
||||
include:
|
||||
- 'app/frontend/**/*.js'
|
||||
- 'app/frontend/**/*.ts'
|
||||
- 'app/frontend/**/*.vue'
|
||||
- '.eslint-plugin-zammad/**/*.js'
|
||||
- '.eslint-plugin-zammad/**/*.ts'
|
||||
Markdownlint:
|
||||
required_executable: 'pnpm'
|
||||
enabled: true
|
||||
|
||||
30
.oxfmtrc.json
Normal file
30
.oxfmtrc.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"sortTailwindcss": {
|
||||
"stylesheet": "./app/frontend/apps/desktop/styles/main.css"
|
||||
},
|
||||
"sortPackageJson": false,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["./app/frontend/apps/desktop/**"],
|
||||
"options": {
|
||||
"sortTailwindcss": { "stylesheet": "./app/frontend/apps/desktop/styles/main.css" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["./app/frontend/apps/mobile/**"],
|
||||
"options": {
|
||||
"sortTailwindcss": { "stylesheet": "./app/frontend/apps/mobile/styles/main.css" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"app/frontend/**/graphql/**/*.ts",
|
||||
"!app/frontend/tests/graphql/**/*.ts",
|
||||
"public/**/*",
|
||||
"tmp/**/*",
|
||||
".dev/rubocop/**/*.yml"
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,6 @@ targets:
|
||||
- elasticsearch
|
||||
- nginx
|
||||
- postgresql-server
|
||||
- which
|
||||
- epel-release
|
||||
- imlib2
|
||||
- shared-mime-info
|
||||
@@ -99,7 +98,6 @@ targets:
|
||||
- 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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
**/graphql/**/*.api.ts
|
||||
!**/graphql/**/mocks/*.ts
|
||||
!**/graphql/**/*.spec.ts
|
||||
!**/tests/graphql/**
|
||||
public/**/*
|
||||
tmp/**/*
|
||||
.dev/rubocop/**/*.yml
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-scss",
|
||||
"stylelint-config-recommended-vue",
|
||||
"stylelint-prettier/recommended"
|
||||
"stylelint-config-recommended-vue"
|
||||
],
|
||||
"rules": {
|
||||
"alpha-value-notation": null,
|
||||
|
||||
@@ -55,6 +55,31 @@ state after the update.
|
||||
User records cannot have the same organization set as both primary and secondary anymore, and trying to
|
||||
do this will now result in an API error.
|
||||
|
||||
### Catalan locale change
|
||||
|
||||
The previously available Catalan locale used a wrong internal locale code and was deprecated. There is now a
|
||||
new Catalan locale with the correct code "ca". The deprecated locale will be removed in a future release of Zammad.
|
||||
An automatic update will switch the language preference of all Catalan user profiles to the new 'Catalan (Català)'
|
||||
locale during the migration.
|
||||
|
||||
The following applies only in case you are already using Knowledge Base with Catalan language. Please note the
|
||||
deprecated Catalan locale will not be updated automatically. Changing the locale of a Knowledge Base causes a change in
|
||||
the public URLs. You can migrate Knowledge Base at your own pace by running the following command:
|
||||
|
||||
```ruby
|
||||
zammad run rails r "KnowledgeBase::Locale.find_by(system_locale: Locale.find_by(locale: 'es-ca'))&.update!(system_locale: Locale.find_by(locale: 'ca'))"
|
||||
```
|
||||
|
||||
### Slack integration removed
|
||||
|
||||
The slack integration was remved from the codebase. It is recommended that you
|
||||
[switch to pre-built webhooks instead](https://admin-docs.zammad.org/en/latest/manage/webhook/examples/slack-notifications.html).
|
||||
Existing Slack integrations should be migrated manually, if not already done.
|
||||
|
||||
### Twitter integration removed
|
||||
|
||||
The Twitter integration was removed due to problems with API licensing. There is no replacement available.
|
||||
|
||||
## 6.5.2
|
||||
|
||||
The following breaking changes occurred due to a security fix.
|
||||
@@ -101,7 +126,7 @@ The structure of the **full search** (e.g. `/ticket/search?full=true`) remains t
|
||||
|
||||
Some objects used an object-related hash key, such as `ticket_ids`. This is now always `record_ids`.
|
||||
|
||||
The **count search** (e.g. `/ticket/search?only_total_count=true`) is a new feature.
|
||||
The **count search** (e.g. `/ticket/search?only_total_count=true`) is a new feature.
|
||||
|
||||
### API performance optimization of asset return data
|
||||
|
||||
@@ -244,8 +269,8 @@ now it is forbidden.
|
||||
On existing systems, the group names that contain the now reserved delimiter will be renamed, with sets of double colons
|
||||
being replaced by a dash (`-`) during the migration process.
|
||||
|
||||
Additionally, existing custom group object attributes named _name\_last_ and _parent\_id_ will be renamed too, by adding
|
||||
an underscore in front (_\_name\_last_ and _\_parent\_id_). This is due to these attributes now being part of the group
|
||||
Additionally, existing custom group object attributes named _name_last_ and _parent_id_ will be renamed too, by adding
|
||||
an underscore in front (_\_name_last_ and _\_parent_id_). This is due to these attributes now being part of the group
|
||||
model, requiring dedicated table columns under the reserved names.
|
||||
|
||||
### Disallowed URL Values in User's Name Attributes
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Change Log for Zammad 7.0.x
|
||||
# Change Log for Zammad 7.0.3
|
||||
|
||||
- [Release notes](https://zammad.com/en/releases/7-0-x)
|
||||
- [Release notes](https://zammad.com/en/product/releases/7-1-1-7-0-3)
|
||||
- [Breaking changes](BREAKING_CHANGES.md#70)
|
||||
- [Implemented enhancements](https://github.com/zammad/zammad/issues?q=is%3Aclosed+milestone%3A7.0+(-type%3ABug+AND+-label%3Abug))
|
||||
- [Closed bugs](https://github.com/zammad/zammad/issues?q=is%3Aclosed+milestone%3A7.0+(type%3ABug+OR+label%3Abug))
|
||||
- [Full commit log](https://github.com/zammad/zammad/compare/6.5.1...7.0.x)
|
||||
- [File tree](https://github.com/zammad/zammad/tree/7.0.x)
|
||||
- [Full commit log](https://github.com/zammad/zammad/compare/7.1.1...7.0.3)
|
||||
- [File tree](https://github.com/zammad/zammad/tree/7.0.3)
|
||||
|
||||
@@ -17,11 +17,14 @@ ENV RAILS_ENV="production" \
|
||||
RAILS_LOG_TO_STDOUT="true"
|
||||
|
||||
# Install base packages
|
||||
# Add official PostgreSQL apt repository to not depend on Debian's version. https://www.postgresql.org/download/linux/debian/ \
|
||||
# Add official PostgreSQL apt repository to not depend on Debian's version.
|
||||
# https://www.postgresql.org/download/linux/debian/
|
||||
# Use `postgresql-client` meta-package to have the latest `pg_dump` that works even with the latest PostgreSQL versions.
|
||||
# https://github.com/zammad/zammad/issues/6009
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y postgresql-common && \
|
||||
/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && \
|
||||
apt-get install --no-install-recommends -y curl libimlib2 libpq5 nginx gnupg postgresql-client-17 && \
|
||||
apt-get install --no-install-recommends -y curl libimlib2 libpq5 nginx gnupg postgresql-client && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Throw-away stage to get the node binary
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -265,7 +265,7 @@ group :development, :test do
|
||||
gem 'localhost'
|
||||
|
||||
# Keycloak admin tool for setting up SAML auth tests
|
||||
gem 'ruby-keycloak-admin', git: 'https://github.com/fliebe92/ruby-keycloak-admin'
|
||||
gem 'ruby-keycloak-admin'
|
||||
|
||||
# Debugging and profiling
|
||||
gem 'pry-doc' # This gem is very large, so don't include it in production.
|
||||
|
||||
190
Gemfile.lock
190
Gemfile.lock
@@ -1,11 +1,3 @@
|
||||
GIT
|
||||
remote: https://github.com/fliebe92/ruby-keycloak-admin
|
||||
revision: ef2cf3a24f98fdfad3aa1a4872bc63f52761a29e
|
||||
specs:
|
||||
ruby-keycloak-admin (26.0.8)
|
||||
hashie (>= 5.0.0)
|
||||
httparty (>= 0.21, < 0.25)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/zammad-deps/autodiscover
|
||||
revision: ee9b53dfa797ce6d4f970b82beea7fbdd2df56bb
|
||||
@@ -40,29 +32,29 @@ GEM
|
||||
simple_po_parser (~> 1.1.6)
|
||||
aasm (5.5.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
actioncable (8.0.4)
|
||||
actionpack (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
actioncable (8.0.5)
|
||||
actionpack (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.4)
|
||||
actionpack (= 8.0.4)
|
||||
activejob (= 8.0.4)
|
||||
activerecord (= 8.0.4)
|
||||
activestorage (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
actionmailbox (8.0.5)
|
||||
actionpack (= 8.0.5)
|
||||
activejob (= 8.0.5)
|
||||
activerecord (= 8.0.5)
|
||||
activestorage (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.4)
|
||||
actionpack (= 8.0.4)
|
||||
actionview (= 8.0.4)
|
||||
activejob (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
actionmailer (8.0.5)
|
||||
actionpack (= 8.0.5)
|
||||
actionview (= 8.0.5)
|
||||
activejob (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.4)
|
||||
actionview (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
actionpack (8.0.5)
|
||||
actionview (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -70,27 +62,27 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.4)
|
||||
actionpack (= 8.0.4)
|
||||
activerecord (= 8.0.4)
|
||||
activestorage (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
actiontext (8.0.5)
|
||||
actionpack (= 8.0.5)
|
||||
activerecord (= 8.0.5)
|
||||
activestorage (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
actionview (8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
activejob (8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
activerecord (8.0.4)
|
||||
activemodel (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
activemodel (8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
activerecord (8.0.5)
|
||||
activemodel (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
timeout (>= 0.4.0)
|
||||
activerecord-import (2.2.0)
|
||||
activerecord (>= 4.2)
|
||||
@@ -100,13 +92,13 @@ GEM
|
||||
cgi (>= 0.3.6)
|
||||
rack (>= 2.0.8, < 4)
|
||||
railties (>= 7.0)
|
||||
activestorage (8.0.4)
|
||||
actionpack (= 8.0.4)
|
||||
activejob (= 8.0.4)
|
||||
activerecord (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
activestorage (8.0.5)
|
||||
actionpack (= 8.0.5)
|
||||
activejob (= 8.0.5)
|
||||
activerecord (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.4)
|
||||
activesupport (8.0.5)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
@@ -122,8 +114,8 @@ GEM
|
||||
acts_as_list (1.2.6)
|
||||
activerecord (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
android_key_attestation (0.3.0)
|
||||
argon2 (2.3.3)
|
||||
@@ -131,6 +123,8 @@ GEM
|
||||
ffi-compiler (~> 1.0)
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
auth-sanitizer (0.2.1)
|
||||
version_gem (~> 1.1, >= 1.1.10)
|
||||
autoprefixer-rails (10.4.21.0)
|
||||
execjs (~> 2)
|
||||
aws-eventstream (1.4.0)
|
||||
@@ -154,7 +148,7 @@ GEM
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (3.3.1)
|
||||
bigdecimal (4.0.1)
|
||||
bindata (2.5.1)
|
||||
binding_of_caller (1.0.1)
|
||||
debug_inspector (>= 1.2.0)
|
||||
@@ -199,7 +193,7 @@ GEM
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.12.2)
|
||||
concurrent-ruby (1.3.6)
|
||||
concurrent-ruby (1.3.7)
|
||||
connection_pool (2.5.5)
|
||||
cose (1.3.1)
|
||||
cbor (~> 0.5.9)
|
||||
@@ -237,8 +231,8 @@ GEM
|
||||
dry-types (~> 1.8, >= 1.8.2)
|
||||
ice_nine (~> 0.11)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-types (1.8.3)
|
||||
bigdecimal (~> 3.0)
|
||||
dry-types (1.9.1)
|
||||
bigdecimal (>= 3.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-inflector (~> 1.0)
|
||||
@@ -266,7 +260,7 @@ GEM
|
||||
simpleidn
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erb (5.0.3)
|
||||
erb (6.0.4)
|
||||
erubi (1.13.1)
|
||||
eventmachine (1.2.7)
|
||||
execjs (2.10.0)
|
||||
@@ -277,7 +271,7 @@ GEM
|
||||
railties (>= 6.1.0)
|
||||
faker (3.6.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.14.0)
|
||||
faraday (2.14.3)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
@@ -288,7 +282,7 @@ GEM
|
||||
hashie
|
||||
faraday-multipart (1.1.1)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (3.4.2)
|
||||
faraday-net_http (3.4.4)
|
||||
net-http (~> 0.5)
|
||||
ffi (1.17.3)
|
||||
ffi-compiler (1.3.2)
|
||||
@@ -299,7 +293,7 @@ GEM
|
||||
ostruct
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
graphql (2.5.19)
|
||||
graphql (2.5.26)
|
||||
base64
|
||||
fiber-storage
|
||||
logger
|
||||
@@ -309,12 +303,13 @@ GEM
|
||||
graphql-fragment_cache (1.22.2)
|
||||
graphql (>= 2.1.4)
|
||||
hashdiff (1.2.1)
|
||||
hashie (5.0.0)
|
||||
hashie (5.1.0)
|
||||
logger
|
||||
hiredis-client (0.26.4)
|
||||
redis-client (= 0.26.4)
|
||||
htmlentities (4.4.2)
|
||||
http_parser.rb (0.8.0)
|
||||
httparty (0.24.0)
|
||||
httparty (0.24.2)
|
||||
csv
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
@@ -322,7 +317,7 @@ GEM
|
||||
mutex_m
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
icalendar (2.12.1)
|
||||
icalendar (2.12.2)
|
||||
base64
|
||||
ice_cube (~> 0.16)
|
||||
logger
|
||||
@@ -342,7 +337,7 @@ GEM
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.18.1)
|
||||
json (2.19.9)
|
||||
json-jwt (1.17.0)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
@@ -350,7 +345,7 @@ GEM
|
||||
bindata
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
jwt (2.10.2)
|
||||
jwt (2.10.3)
|
||||
base64
|
||||
koala (3.7.0)
|
||||
addressable
|
||||
@@ -404,7 +399,7 @@ GEM
|
||||
nestful (1.1.4)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.2)
|
||||
net-imap (0.6.4.1)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.20.0)
|
||||
@@ -418,25 +413,31 @@ GEM
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nkf (0.2.0)
|
||||
nokogiri (1.19.0)
|
||||
nokogiri (1.19.4)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nori (2.7.1)
|
||||
bigdecimal
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
oauth-tty (1.0.5)
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
oauth2 (2.0.18)
|
||||
oauth (1.1.6)
|
||||
auth-sanitizer (~> 0.2, >= 0.2.1)
|
||||
base64 (~> 0.1)
|
||||
cgi
|
||||
oauth-tty (~> 1.0, >= 1.0.8)
|
||||
snaky_hash (~> 2.0, >= 2.0.5)
|
||||
version_gem (~> 1.1, >= 1.1.11)
|
||||
oauth-tty (1.0.8)
|
||||
auth-sanitizer (~> 0.1, >= 0.1.3)
|
||||
cgi
|
||||
version_gem (~> 1.1, >= 1.1.9)
|
||||
oauth2 (2.0.22)
|
||||
auth-sanitizer (~> 0.2, >= 0.2.1)
|
||||
faraday (>= 0.17.3, < 4.0)
|
||||
jwt (>= 1.0, < 4.0)
|
||||
logger (~> 1.2)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0, >= 2.0.3)
|
||||
version_gem (~> 1.1, >= 1.1.9)
|
||||
snaky_hash (~> 2.0, >= 2.0.5)
|
||||
version_gem (~> 1.1, >= 1.1.11)
|
||||
omniauth (2.1.4)
|
||||
hashie (>= 3.4.6)
|
||||
logger
|
||||
@@ -542,7 +543,7 @@ GEM
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
puma (7.2.0)
|
||||
puma (7.2.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.5.2)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -552,7 +553,7 @@ GEM
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
racc (1.8.1)
|
||||
rack (2.2.21)
|
||||
rack (2.2.23)
|
||||
rack-attack (6.8.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-oauth2 (2.2.1)
|
||||
@@ -574,20 +575,20 @@ GEM
|
||||
rackup (1.0.1)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (8.0.4)
|
||||
actioncable (= 8.0.4)
|
||||
actionmailbox (= 8.0.4)
|
||||
actionmailer (= 8.0.4)
|
||||
actionpack (= 8.0.4)
|
||||
actiontext (= 8.0.4)
|
||||
actionview (= 8.0.4)
|
||||
activejob (= 8.0.4)
|
||||
activemodel (= 8.0.4)
|
||||
activerecord (= 8.0.4)
|
||||
activestorage (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
rails (8.0.5)
|
||||
actioncable (= 8.0.5)
|
||||
actionmailbox (= 8.0.5)
|
||||
actionmailer (= 8.0.5)
|
||||
actionpack (= 8.0.5)
|
||||
actiontext (= 8.0.5)
|
||||
actionview (= 8.0.5)
|
||||
activejob (= 8.0.5)
|
||||
activemodel (= 8.0.5)
|
||||
activerecord (= 8.0.5)
|
||||
activestorage (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.4)
|
||||
railties (= 8.0.5)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
@@ -599,9 +600,9 @@ GEM
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (8.0.4)
|
||||
actionpack (= 8.0.4)
|
||||
activesupport (= 8.0.4)
|
||||
railties (8.0.5)
|
||||
actionpack (= 8.0.5)
|
||||
activesupport (= 8.0.5)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
@@ -697,6 +698,9 @@ GEM
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rspec (~> 3.5)
|
||||
ruby-keycloak-admin (26.5.2)
|
||||
hashie (>= 5.0.0)
|
||||
httparty (>= 0.21, < 0.25)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-saml (1.18.1)
|
||||
nokogiri (>= 1.13.10)
|
||||
@@ -736,7 +740,7 @@ GEM
|
||||
hashie
|
||||
logger
|
||||
slop (3.6.0)
|
||||
snaky_hash (2.0.3)
|
||||
snaky_hash (2.0.5)
|
||||
hashie (>= 0.1.0, < 6)
|
||||
version_gem (>= 1.1.8, < 3)
|
||||
sprockets (3.7.5)
|
||||
@@ -797,7 +801,7 @@ GEM
|
||||
activemodel (>= 3.0.0)
|
||||
public_suffix
|
||||
vcr (6.4.0)
|
||||
version_gem (1.1.9)
|
||||
version_gem (1.1.11)
|
||||
viewpoint (1.2.0)
|
||||
httpclient
|
||||
logging
|
||||
@@ -845,7 +849,7 @@ GEM
|
||||
rubyzip (>= 2.4.0, < 4.0)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yard (0.9.38)
|
||||
yard (0.9.43)
|
||||
zeitwerk (2.7.4)
|
||||
zendesk_api (3.1.1)
|
||||
faraday (> 2.0.0)
|
||||
@@ -962,7 +966,7 @@ DEPENDENCIES
|
||||
rubocop-rake
|
||||
rubocop-rspec
|
||||
rubocop-rspec_rails
|
||||
ruby-keycloak-admin!
|
||||
ruby-keycloak-admin
|
||||
sass-rails (~> 5)
|
||||
selenium-webdriver
|
||||
shoulda-matchers
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
class ChannelAiProvider extends App.ControllerTabs
|
||||
@requiredPermission: 'admin.ai_provider'
|
||||
header: __('Provider')
|
||||
headerSwitchName: 'ai_provider'
|
||||
|
||||
events:
|
||||
'change .js-header-switch input': 'didChangeHeaderSwitch'
|
||||
|
||||
elements:
|
||||
'.js-header-switch input': 'aiProviderSetting'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@@ -20,6 +27,44 @@ class ChannelAiProvider extends App.ControllerTabs
|
||||
|
||||
@render()
|
||||
|
||||
@controllerBind('config_update', @aiProviderConfigHasChanged)
|
||||
|
||||
aiProviderConfigHasChanged: (config) =>
|
||||
return if config.name isnt 'ai_provider'
|
||||
|
||||
@renderHeader(config.value)
|
||||
|
||||
render: =>
|
||||
super
|
||||
@renderHeader(App.Config.get('ai_provider'))
|
||||
|
||||
renderHeader: (value) =>
|
||||
@aiProviderSetting.prop('checked', value)
|
||||
|
||||
didChangeHeaderSwitch: ->
|
||||
value = @aiProviderSetting.prop('checked')
|
||||
|
||||
App.Setting.set('ai_provider', value, done: =>
|
||||
# If the provider is being enabled but not yet configured, disable it again and show a warning message.
|
||||
if value and not @controllerList[0].form?.current_provider
|
||||
App.Setting.set('ai_provider', false, done: =>
|
||||
@renderHeader(false)
|
||||
@notify(
|
||||
type: 'warning'
|
||||
msg: __('Please set up the provider before proceeding under the settings tab.')
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
@notify(
|
||||
type: 'success'
|
||||
msg: if value
|
||||
__('AI provider enabled successfully.')
|
||||
else
|
||||
__('AI provider disabled successfully.')
|
||||
)
|
||||
)
|
||||
|
||||
class AiProviderSettings extends App.Controller
|
||||
@requiredPermission: 'admin.ai_provider'
|
||||
description : __('This service allows you to connect Zammad with an AI provider.')
|
||||
@@ -32,11 +77,17 @@ class AiProviderSettings extends App.Controller
|
||||
release: =>
|
||||
App.Setting.unsubscribe(@subscribeId)
|
||||
|
||||
render: =>
|
||||
render: (_changedItems, localOrServer) =>
|
||||
return if localOrServer and localOrServer isnt 'refresh'
|
||||
return if _.isEqual(@aiProviderConfigAtRender, App.Setting.get('ai_provider_config'))
|
||||
|
||||
@aiProviderConfigAtRender = _.clone(App.Setting.get('ai_provider_config'))
|
||||
|
||||
@html App.view('ai/provider')(
|
||||
description: @description,
|
||||
)
|
||||
new ProviderForm()
|
||||
|
||||
@form = new ProviderForm()
|
||||
|
||||
class AiProviderFeedbackAndLogs extends App.Controller
|
||||
@requiredPermission: 'admin.ai_provider'
|
||||
@@ -103,11 +154,7 @@ class AiProviderFeedbackAndLogs extends App.Controller
|
||||
@httpLog = null
|
||||
super
|
||||
|
||||
|
||||
class ProviderForm extends App.Controller
|
||||
events:
|
||||
'.js-provider-submit': 'update'
|
||||
|
||||
constructor: (content) ->
|
||||
super
|
||||
|
||||
@@ -116,7 +163,6 @@ class ProviderForm extends App.Controller
|
||||
|
||||
@render(content)
|
||||
|
||||
|
||||
getSortedProviderOptions: ->
|
||||
Object
|
||||
.entries(@providers)
|
||||
@@ -135,7 +181,7 @@ class ProviderForm extends App.Controller
|
||||
type: 'password'
|
||||
single: true
|
||||
null: not _.contains(provider.required, 'token')
|
||||
autocomplete: 'off'
|
||||
autocomplete: 'new-password'
|
||||
value: params.token
|
||||
}
|
||||
model: {
|
||||
@@ -244,9 +290,9 @@ class ProviderForm extends App.Controller
|
||||
|
||||
render: (provider) ->
|
||||
config = App.Setting.get('ai_provider_config') || {}
|
||||
current_provider = if provider isnt undefined then provider else config['provider']
|
||||
@current_provider = if provider isnt undefined then provider else config['provider']
|
||||
|
||||
configure_attributes = @providerConfiguration(current_provider, config)
|
||||
configure_attributes = @providerConfiguration(@current_provider, config)
|
||||
|
||||
@providerSettingsForm?.releaseController()
|
||||
@providerSettingsForm = new App.ControllerForm(
|
||||
@@ -259,6 +305,7 @@ class ProviderForm extends App.Controller
|
||||
)
|
||||
|
||||
$('.js-provider-submit').off('click.provider').on('click.provider', @update)
|
||||
|
||||
$('select[name=provider]').off('change.provider').on('change.provider', (e) =>
|
||||
@render($(e.target).val())
|
||||
)
|
||||
@@ -279,6 +326,8 @@ class ProviderForm extends App.Controller
|
||||
@validateAndSave(params)
|
||||
|
||||
validateAndSave: (params) ->
|
||||
App.ControllerForm.disable(@providerSettingsForm.form)
|
||||
|
||||
has_provider = not _.isEmpty(params.provider)
|
||||
|
||||
if not has_provider
|
||||
@@ -294,8 +343,31 @@ class ProviderForm extends App.Controller
|
||||
if has_provider && !params.hasOwnProperty('token') && savedProviderConfig.provider == params.provider && savedProviderConfig.token
|
||||
params.token = savedProviderConfig.token
|
||||
|
||||
App.Setting.set('ai_provider_config', params, done: ->
|
||||
App.Setting.set('ai_provider', has_provider, notify: true)
|
||||
App.Setting.set(
|
||||
'ai_provider_config',
|
||||
params,
|
||||
done: =>
|
||||
App.ControllerForm.enable(@providerSettingsForm.form)
|
||||
|
||||
# If the provider configuration is being updated, do not touch the provider switch.
|
||||
if has_provider
|
||||
App.Event.trigger 'notify', {
|
||||
type: 'success'
|
||||
msg: __('Update successful.')
|
||||
timeout: 2000
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
# Turn off the provider switch when the provider configuration is emptied.
|
||||
App.Setting.set('ai_provider', false, done: =>
|
||||
@notify(
|
||||
type: 'success'
|
||||
msg: __('AI provider disabled successfully.')
|
||||
)
|
||||
)
|
||||
fail: =>
|
||||
App.ControllerForm.enable(@providerSettingsForm.form)
|
||||
)
|
||||
|
||||
App.Config.set('Provider', { prio: 1000, name: __('Provider'), parent: '#ai', target: '#ai/provider', controller: ChannelAiProvider, permission: ['admin.ai_provider'] }, 'NavBarAdmin')
|
||||
|
||||
@@ -54,6 +54,7 @@ class App.TicketSummary extends App.ControllerAIFeatureBase
|
||||
value: 'on_ticket_summary_sidebar_activation'
|
||||
},
|
||||
]
|
||||
translate: true
|
||||
)
|
||||
|
||||
content.find('.js-ticketSummaryGenerationConfigSelect').html(select)
|
||||
|
||||
@@ -73,10 +73,14 @@ class App.GenericHistory extends App.ControllerModal
|
||||
created_by: App.User.find( item.created_by_id )
|
||||
sources: []
|
||||
|
||||
recordsSource = _.findWhere(newItem.sources, { sourceable_id: item.sourceable_id })
|
||||
recordsSource = _.findWhere(newItem.sources, {
|
||||
sourceable_id: item.sourceable_id,
|
||||
sourceable_type_raw: item.sourceable_type
|
||||
})
|
||||
if !recordsSource
|
||||
recordsSource = {
|
||||
sourceable_id: item.sourceable_id,
|
||||
sourceable_type_raw: item.sourceable_type,
|
||||
sourceable_type: @sourceableTypeDisplayName(item.sourceable_type),
|
||||
sourceable_name: item.sourceable_name,
|
||||
users: []
|
||||
|
||||
@@ -161,7 +161,7 @@ class App.ControllerForm extends App.Controller
|
||||
container = $("<div class='form-buttons #{@fullFormButtonsContainerClass}'>")
|
||||
|
||||
for buttonConfig in @fullFormAdditionalButtons
|
||||
btn = $("<button class='btn #{buttonConfig.className}'>").text(buttonConfig.text)
|
||||
btn = $("<button class='btn #{buttonConfig.className}'>").text(App.i18n.translateContent(buttonConfig.text))
|
||||
if buttonConfig.disabled
|
||||
btn.prop('disabled', true)
|
||||
container.append(btn)
|
||||
|
||||
@@ -693,7 +693,7 @@ class App.ControllerTable extends App.Controller
|
||||
true
|
||||
callback: (id) =>
|
||||
item = @model.find(id)
|
||||
item.name = "Clone: #{item.name}"
|
||||
item.name = App.i18n.translatePlain('Clone: %s', item.name)
|
||||
|
||||
if @cloneCallback
|
||||
@cloneCallback(item)
|
||||
|
||||
@@ -252,7 +252,7 @@ class ChannelChat extends App.ControllerSubContent
|
||||
renderError: (data) =>
|
||||
errorMessage = data?.error || __('An error occurred while fetching the website preview.')
|
||||
|
||||
@browserHelp.text(App.i18n.translateInline(errorMessage))
|
||||
@browserHelp.text(App.i18n.translatePlain(errorMessage))
|
||||
.show()
|
||||
|
||||
@urlInput.removeClass('is-loading')
|
||||
|
||||
@@ -326,9 +326,6 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
|
||||
'click .js-goToSlide': 'goToSlide'
|
||||
'click .js-expert': 'probeBasedOnIntro'
|
||||
'click .js-close': 'hide'
|
||||
inboundPassword: ''
|
||||
outboundPassword: ''
|
||||
passwordPlaceholder: '{{{{{{{{{{{{SECRTE_PASSWORD}}}}}}}}}}}}'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@@ -349,14 +346,6 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
|
||||
outbound: clone(@channel.options.outbound)
|
||||
meta: {}
|
||||
|
||||
# remember passwords, do not show in ui
|
||||
if @account.inbound.options && @account.inbound.options.password
|
||||
@inboundPassword = @account.inbound.options.password
|
||||
@account.inbound.options.password = @passwordPlaceholder
|
||||
if @account.outbound.options && @account.outbound.options.password
|
||||
@outboundPassword = @account.outbound.options.password
|
||||
@account.outbound.options.password = @passwordPlaceholder
|
||||
|
||||
@render()
|
||||
|
||||
if @channel
|
||||
@@ -645,9 +634,6 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
|
||||
@formValidate(form: e.target, errors: errors)
|
||||
return false
|
||||
|
||||
if params.options && params.options.password is @passwordPlaceholder
|
||||
params.options.password = @inboundPassword
|
||||
|
||||
# Update meta as the one from AttributesBase could be outdated
|
||||
@account.meta.realname = params.options.realname
|
||||
@account.meta.email = params.options.email
|
||||
@@ -809,9 +795,6 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
|
||||
params = @formParam(e.target)
|
||||
params['email'] = @account['meta']['email']
|
||||
|
||||
if params.options && params.options.password is @passwordPlaceholder
|
||||
params.options.password = @outboundPassword
|
||||
|
||||
if !params['email'] && @channel
|
||||
email_addresses = App.EmailAddress.search(filter: { channel_id: @channel.id })
|
||||
if email_addresses && email_addresses[0]
|
||||
@@ -883,11 +866,6 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
|
||||
if email_addresses && email_addresses[0]
|
||||
params.email = email_addresses[0].email
|
||||
|
||||
if params.inbound?.options?.password is @passwordPlaceholder
|
||||
params.inbound.options.password = @inboundPassword
|
||||
if params.outbound?.options?.password is @passwordPlaceholder
|
||||
params.outbound.options.password = @outboundPassword
|
||||
|
||||
@ajax(
|
||||
id: 'email_verify'
|
||||
type: 'POST'
|
||||
@@ -938,9 +916,6 @@ class ChannelEmailNotificationWizard extends App.ControllerWizardModal
|
||||
'change .js-outbound [name=adapter]': 'toggleOutboundAdapter'
|
||||
'submit .js-outbound': 'probleOutbound'
|
||||
'click .js-close': 'hide'
|
||||
inboundPassword: ''
|
||||
outboundPassword: ''
|
||||
passwordPlaceholder: '{{{{{{{{{{{{SECRTE_PASSWORD}}}}}}}}}}}}'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@@ -960,14 +935,6 @@ class ChannelEmailNotificationWizard extends App.ControllerWizardModal
|
||||
inbound: clone(@channel.options.inbound)
|
||||
outbound: clone(@channel.options.outbound)
|
||||
|
||||
# remember passwords, do not show in ui
|
||||
if @account.inbound && @account.inbound.options && @account.inbound.options.password
|
||||
@inboundPassword = @account.inbound.options.password
|
||||
@account.inbound.options.password = @passwordPlaceholder
|
||||
if @account.outbound && @account.outbound.options && @account.outbound.options.password
|
||||
@outboundPassword = @account.outbound.options.password
|
||||
@account.outbound.options.password = @passwordPlaceholder
|
||||
|
||||
@render()
|
||||
@toggleSslVerifyAlert(target: @el.find('[name="options::ssl_verify"]'))
|
||||
|
||||
@@ -1059,9 +1026,6 @@ class ChannelEmailNotificationWizard extends App.ControllerWizardModal
|
||||
# get params
|
||||
params = @formParam(e.target)
|
||||
|
||||
if params.options && params.options.password is @passwordPlaceholder
|
||||
params.options.password = @outboundPassword
|
||||
|
||||
# let backend know about the channel
|
||||
params.channel_id = @channel.id
|
||||
|
||||
|
||||
@@ -131,6 +131,9 @@ class WhatsappAccountCloudAPIModal extends App.ControllerModal
|
||||
|
||||
params = if @params then _.extend(@params, @formParams()) else @formParams()
|
||||
|
||||
if @channel
|
||||
params.channel_id = @channel.id
|
||||
|
||||
@ajax(
|
||||
id: 'whatsapp_initial'
|
||||
type: 'POST'
|
||||
|
||||
@@ -61,7 +61,7 @@ class Form extends App.Controller
|
||||
if !@config
|
||||
@config = @currentConfig()
|
||||
settings = [
|
||||
{ name: 'api_key', display: __('API Key'), tag: 'input', type: 'text', limit: 100, null: false, placeholder: '...', note: __('Your API key.') },
|
||||
{ name: 'api_key', display: __('API Key'), tag: 'input', type: 'text', input_type: 'password', limit: 100, null: false, placeholder: '…', note: __('Your API key.') },
|
||||
{ name: 'organization_autocreate', display: __('Auto create'), tag: 'boolean', type: 'boolean', null: false, note: __('Create organizations automatically if record has one.') },
|
||||
{ name: 'organization_shared', display: __('Shared'), tag: 'boolean', type: 'boolean', null: false, note: __('New organizations are shared.') },
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ class Ldap extends App.ControllerIntegrationBase
|
||||
featureIntegration: 'ldap_integration'
|
||||
featureName: __('LDAP')
|
||||
description: [
|
||||
[__('Use this switch to start synchronization of your ldap sources.')]
|
||||
[__('Use this switch to start synchronization of your LDAP sources.')]
|
||||
[__('If a user is found in two (or more) configured LDAP sources, the last synchronisation will win.')]
|
||||
[__('In order to be able to influence the desired behavior in this regard, you can influence the order of the LDAP sources via drag & drop.')]
|
||||
]
|
||||
@@ -182,7 +182,7 @@ class Form extends App.Controller
|
||||
setConfig: (value) =>
|
||||
@item.name = value.name
|
||||
@item.active = value.active
|
||||
@item.preferences = _.omit(value, ['id', 'name', 'active'])
|
||||
@item.preferences = _.omit(value, ['id', 'name', 'active', 'ldap_source_id'])
|
||||
@item.save(
|
||||
done: =>
|
||||
@showIndex()
|
||||
@@ -521,6 +521,7 @@ class ConnectionWizard extends App.ControllerWizardModal
|
||||
params.host = @wizardConfig.host
|
||||
params.ssl = @wizardConfig.ssl
|
||||
params.ssl_verify = @wizardConfig.ssl_verify
|
||||
params.ldap_source_id = @wizardConfig.id if @wizardConfig.id
|
||||
@ajax(
|
||||
id: 'ldap_bind'
|
||||
type: 'POST'
|
||||
@@ -820,7 +821,7 @@ class LdapSourceIndex extends App.ControllerGenericIndex
|
||||
item = new App.LdapSource(
|
||||
name: config.name
|
||||
active: config.active
|
||||
preferences: _.omit(config, ['id', 'name', 'active'])
|
||||
preferences: _.omit(config, ['id', 'name', 'active', 'ldap_source_id'])
|
||||
)
|
||||
item.save(
|
||||
done: ->
|
||||
|
||||
@@ -26,6 +26,13 @@ class App.ManageKnowledgeBase extends App.ControllerTabs
|
||||
@render()
|
||||
@fetchAndRender()
|
||||
|
||||
@controllerBind('config_update_local', @kbActiveConfigChanged)
|
||||
|
||||
kbActiveConfigChanged: (data) =>
|
||||
return if data.name isnt 'kb_active'
|
||||
return if !@knowledge_base_id
|
||||
@headerSwitchInput.prop('checked', data.value)
|
||||
|
||||
fetchAndRender: =>
|
||||
@startLoading()
|
||||
|
||||
|
||||
@@ -5,18 +5,23 @@ class Maintenance extends App.Controller
|
||||
@controllerBind(
|
||||
'maintenance'
|
||||
(data) =>
|
||||
if data.type is 'message'
|
||||
@showMessage(data)
|
||||
if data.type is 'mode'
|
||||
@maintanaceMode(data)
|
||||
if data.type is 'app_version'
|
||||
@maintanaceAppVersion(data)
|
||||
if data.type is 'config_changed'
|
||||
@maintanaceConfigChanged(data)
|
||||
if data.type is 'restart_auto'
|
||||
@maintanaceRestartAuto(data)
|
||||
if data.type is 'restart_manual'
|
||||
@maintanaceRestartManual(data)
|
||||
switch data.type
|
||||
when 'message'
|
||||
@showMessage(data)
|
||||
when 'mode'
|
||||
@maintenanceMode(data)
|
||||
when 'app_version'
|
||||
@maintenanceAppVersion(data)
|
||||
when 'config_changed'
|
||||
@maintenanceConfigChanged(data)
|
||||
when 'invalid_csrf_token'
|
||||
@maintenanceInvalidCSRFToken(data)
|
||||
when 'restart_auto'
|
||||
@maintenanceRestartAuto(data)
|
||||
when 'restart_manual'
|
||||
@maintenanceRestartManual(data)
|
||||
when 'force_refresh'
|
||||
@maintenanceForceRefresh(data)
|
||||
)
|
||||
|
||||
showMessage: (message = {}) =>
|
||||
@@ -40,13 +45,13 @@ class Maintenance extends App.Controller
|
||||
forceReload: message.reload
|
||||
)
|
||||
|
||||
maintanaceMode: (data = {}) =>
|
||||
maintenanceMode: (data = {}) =>
|
||||
return if data.on isnt true
|
||||
return if !@authenticateCheck()
|
||||
@navigate '#logout'
|
||||
|
||||
#App.Event.trigger('maintenance', {type:'restart_auto'})
|
||||
maintanaceRestartAuto: (data) =>
|
||||
maintenanceRestartAuto: (data) =>
|
||||
return if @messageRestartAuto
|
||||
|
||||
App.SessionStorage.clear()
|
||||
@@ -65,7 +70,7 @@ class Maintenance extends App.Controller
|
||||
@checkAvailability()
|
||||
|
||||
#App.Event.trigger('maintenance', {type:'restart_manual'})
|
||||
maintanaceRestartManual: (data) =>
|
||||
maintenanceRestartManual: (data) =>
|
||||
return if @messageRestartManual
|
||||
|
||||
App.SessionStorage.clear()
|
||||
@@ -83,7 +88,13 @@ class Maintenance extends App.Controller
|
||||
@disconnectClient()
|
||||
@checkAvailability()
|
||||
|
||||
maintanaceConfigChanged: (data) =>
|
||||
maintenanceForceRefresh: (data) =>
|
||||
return if @forceRefreshed
|
||||
@forceRefreshed = true
|
||||
|
||||
window.location.reload()
|
||||
|
||||
maintenanceConfigChanged: (data) =>
|
||||
return if @messageConfigChanged
|
||||
|
||||
App.SessionStorage.clear()
|
||||
@@ -98,7 +109,23 @@ class Maintenance extends App.Controller
|
||||
forceReload: true
|
||||
)
|
||||
|
||||
maintanaceAppVersion: (data) =>
|
||||
maintenanceInvalidCSRFToken: (data) =>
|
||||
return if @messageConfigChanged
|
||||
|
||||
App.SessionStorage.clear()
|
||||
|
||||
@messageConfigChanged = new App.SessionMessage(
|
||||
head: __('Security token verification failed')
|
||||
message: __('The included security token is invalid. This may be just a temporary error, please reload your browser, and try again.')
|
||||
keyboard: false
|
||||
backdrop: true
|
||||
buttonClose: false
|
||||
buttonSubmit: __('Reload')
|
||||
forceReload: true
|
||||
reloadTimeout: 10000
|
||||
)
|
||||
|
||||
maintenanceAppVersion: (data) =>
|
||||
return if @messageAppVersion
|
||||
return if @appVersion is data.app_version
|
||||
if !@appVersion
|
||||
|
||||
@@ -182,7 +182,7 @@ class Navigation extends App.Controller
|
||||
|
||||
new App.ControllerConfirm(
|
||||
head: __('Help')
|
||||
message: __('You can switch between the old and the New BETA UI at any moment in the Profile > New BETA UI section.')
|
||||
message: __('You can switch between the old and the New BETA UI at any moment in the Profile settings > New BETA UI section.')
|
||||
buttonClass: 'btn--success'
|
||||
buttonCancel: false
|
||||
buttonSubmit: __('Got it')
|
||||
@@ -190,8 +190,9 @@ class Navigation extends App.Controller
|
||||
)
|
||||
|
||||
content.find('.js-switchControl').replaceWith App.UiElement.switch.render(
|
||||
name: 'desktop_beta_switch'
|
||||
name: 'desktop_beta_switch'
|
||||
display: __('Try New BETA UI')
|
||||
theme: 'light' # force light mode on the switch, due to bad contrast in dark mode
|
||||
)
|
||||
|
||||
content.find('input[name="desktop_beta_switch"]')
|
||||
|
||||
@@ -36,9 +36,18 @@ class App.SettingsAreaSwitch extends App.Controller
|
||||
subSetting: @subSetting
|
||||
)
|
||||
for localSetting in @subSetting
|
||||
directValue = 0
|
||||
for item in localSetting.options['form']
|
||||
directValue += 1
|
||||
|
||||
if directValue > 1
|
||||
params = localSetting.state_current.value
|
||||
else
|
||||
params = {}
|
||||
params[localSetting.options['form'][0].name] = localSetting.state_current.value
|
||||
new App.ControllerForm(
|
||||
el: @$('.form-item')
|
||||
params: localSetting.state_current.value
|
||||
params: params
|
||||
model: { configure_attributes: localSetting.options['form'], className: '' }
|
||||
autofocus: false
|
||||
)
|
||||
|
||||
@@ -14,9 +14,10 @@ class App.UiElement.autocompletion_ajax
|
||||
name: attribute.name
|
||||
id: params.organization_id || attribute.id
|
||||
placeholder: App.i18n.translateInline('Search…')
|
||||
limit: 40
|
||||
relation: attribute.relation
|
||||
ajax: true
|
||||
multiple: attribute.multiple
|
||||
limit: 40
|
||||
relation: attribute.relation
|
||||
ajax: true
|
||||
multiple: attribute.multiple
|
||||
allowInactive: attribute.allow_inactive
|
||||
)
|
||||
searchableAjaxSelectObject.element()
|
||||
|
||||
@@ -269,17 +269,18 @@ class App.UiElement.object_attribute_options_context extends Spine.Module
|
||||
flatOptions = []
|
||||
|
||||
options.forEach (option) ->
|
||||
label = if isTree
|
||||
App.TokenHelper.computeNameValue(option.value, isTranslated)
|
||||
else if isTranslated
|
||||
App.i18n.translateInline(option.name or option.value)
|
||||
else
|
||||
option.name or option.value
|
||||
if !option.disabled
|
||||
label = if isTree
|
||||
App.TokenHelper.computeNameValue(option.value, isTranslated)
|
||||
else if isTranslated
|
||||
App.i18n.translateInline(option.name or option.value)
|
||||
else
|
||||
option.name or option.value
|
||||
|
||||
flatOptions.push(
|
||||
value: option.value
|
||||
label: label
|
||||
)
|
||||
flatOptions.push(
|
||||
value: option.value
|
||||
label: label
|
||||
)
|
||||
|
||||
if option.children
|
||||
flatOptions.push App.UiElement.object_attribute_options_context.buildFlatOptions(
|
||||
|
||||
@@ -3,38 +3,28 @@ class TicketCreateFormHandlerSignature
|
||||
@run: (params, attribute, attributes, classname, form, ui) ->
|
||||
return if !attribute
|
||||
return if attribute.name isnt 'group_id'
|
||||
signature = undefined
|
||||
if params['group_id']
|
||||
group = App.Group.find(params['group_id'])
|
||||
if group && group.signature_id
|
||||
signature = App.Signature.find(group.signature_id)
|
||||
|
||||
result = App.SignatureHelper.findForGroup(params['group_id'])
|
||||
|
||||
# check if signature needs to be added
|
||||
type = ui.el.closest('.content').find('[name="formSenderType"]').val()
|
||||
if signature && signature.active && signature.body && type is 'email-out'
|
||||
signatureFinished = App.Utils.replaceTags(signature.body,
|
||||
user: App.Session.get()
|
||||
config: App.Config.all()
|
||||
|
||||
# Fake a ticket object, if a group is present (#4448).
|
||||
ticket:
|
||||
group: group
|
||||
)
|
||||
|
||||
if result && type is 'email-out'
|
||||
currentBody = ui.el.closest('.content').find('[data-name=body]')
|
||||
if !_.isEmpty(currentBody)
|
||||
# Always remove any existing signature first (from old or new editor format)
|
||||
# before checking and potentially adding a new one
|
||||
ui.el.closest('.content').find('[data-signature="true"]').remove()
|
||||
return if _.isEmpty(currentBody)
|
||||
|
||||
# Refresh currentBody reference after removal
|
||||
currentBody = ui.el.closest('.content').find('[data-name=body]')
|
||||
|
||||
if App.Utils.signatureCheck(currentBody.html() || '', signatureFinished)
|
||||
signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>")
|
||||
App.Utils.htmlStrip(signature)
|
||||
currentBody.append(signature)
|
||||
ui.el.closest('.content').find('[data-name=body]').replaceWith(currentBody)
|
||||
# skip re-applying the signature if it is already present (e.g. after autosave restore)
|
||||
return if currentBody.find("[data-signature-id=\"#{result.signature.id}\"]").length
|
||||
|
||||
# remove existing signature and re-add it
|
||||
# https://github.com/zammad/zammad/issues/2319
|
||||
App.SignatureHelper.removeTopLevel(currentBody)
|
||||
|
||||
# Fake a ticket object, if a group is present (#4448).
|
||||
signatureFinished = App.SignatureHelper.render(result.signature.body, { group: result.group })
|
||||
|
||||
sigEl = App.SignatureHelper.buildElement(result.signature.id, signatureFinished)
|
||||
App.SignatureHelper.appendToBottom(currentBody, sigEl)
|
||||
ui.el.closest('.content').find('[data-name=body]').replaceWith(currentBody)
|
||||
|
||||
# remove old signature
|
||||
else
|
||||
|
||||
@@ -35,7 +35,7 @@ class Success extends App.ControllerAppContent
|
||||
detail: __('Woo hoo! Your email address has been verified!')
|
||||
)
|
||||
delay = =>
|
||||
@navigate '#'
|
||||
App.Auth.loginCheck(=> @navigate '#')
|
||||
@delay(delay, 2000)
|
||||
|
||||
class Fail extends App.ControllerAppContent
|
||||
|
||||
@@ -151,19 +151,18 @@ class ImportFreshdesk extends App.ControllerWizardFullScreen
|
||||
url: "#{@apiPath}/import/freshdesk/import_status"
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
|
||||
if _.isEmpty(data.result) && @updateMigrationDisplayLoop > 16
|
||||
@$('.js-error').removeClass('hide')
|
||||
@$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.'))
|
||||
return
|
||||
|
||||
if !_.isEmpty(data.result['error'])
|
||||
if !_.isEmpty(data.result) && !_.isEmpty(data.result['error'])
|
||||
@$('.js-error').removeClass('hide')
|
||||
@$('.js-error').html(App.i18n.translateContent(data.result['error']))
|
||||
else
|
||||
@$('.js-error').addClass('hide')
|
||||
|
||||
if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error'])
|
||||
if data.setup_done is true
|
||||
@redirectToLogin()
|
||||
return
|
||||
|
||||
|
||||
@@ -158,13 +158,13 @@ class ImportKayako extends App.ControllerWizardFullScreen
|
||||
@$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.'))
|
||||
return
|
||||
|
||||
if !_.isEmpty(data.result['error'])
|
||||
if !_.isEmpty(data.result) && !_.isEmpty(data.result['error'])
|
||||
@$('.js-error').removeClass('hide')
|
||||
@$('.js-error').html(App.i18n.translateContent(data.result['error']))
|
||||
else
|
||||
@$('.js-error').addClass('hide')
|
||||
|
||||
if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error'])
|
||||
if data.setup_done is true
|
||||
@redirectToLogin()
|
||||
return
|
||||
|
||||
|
||||
@@ -107,11 +107,12 @@ class ImportOtrs extends App.ControllerWizardFullScreen
|
||||
url: "#{@apiPath}/import/otrs/import_check"
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
if data.result is 'ok'
|
||||
if data.result? and data.result is 'ok'
|
||||
@startMigration()
|
||||
return
|
||||
for issue in data.issues
|
||||
@$(".js-#{issue}").removeClass('hide')
|
||||
if data.issues?
|
||||
for issue in data.issues
|
||||
@$(".js-#{issue}").removeClass('hide')
|
||||
@showImportNotice()
|
||||
)
|
||||
|
||||
@@ -139,7 +140,7 @@ class ImportOtrs extends App.ControllerWizardFullScreen
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
|
||||
if data.result is 'import_done'
|
||||
if data.setup_done is true
|
||||
window.location.reload()
|
||||
return
|
||||
|
||||
|
||||
@@ -156,13 +156,13 @@ class ImportZendesk extends App.ControllerWizardFullScreen
|
||||
@$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.'))
|
||||
return
|
||||
|
||||
if !_.isEmpty(data.result['error'])
|
||||
if !_.isEmpty(data.result) && !_.isEmpty(data.result['error'])
|
||||
@$('.js-error').removeClass('hide')
|
||||
@$('.js-error').html(App.i18n.translateContent(data.result['error']))
|
||||
else
|
||||
@$('.js-error').addClass('hide')
|
||||
|
||||
if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error'])
|
||||
if data.setup_done is true
|
||||
@redirectToLogin()
|
||||
return
|
||||
|
||||
|
||||
@@ -64,7 +64,11 @@ class App.KnowledgeBaseReaderListContainer.Categories extends App.KnowledgeBaseR
|
||||
#
|
||||
|
||||
if !@isEditor
|
||||
items = items.filter (elem) => elem.visibleInternally(@kb_locale)
|
||||
items = items.filter (elem) =>
|
||||
if elem.access(@kb_locale) == 'editor'
|
||||
true
|
||||
else
|
||||
elem.visibleInternally(@kb_locale)
|
||||
|
||||
items
|
||||
|
||||
|
||||
@@ -124,15 +124,7 @@ class App.TextToolsModal extends App.ControllerModal
|
||||
)
|
||||
|
||||
onSubmit: =>
|
||||
if @feedbackWidget
|
||||
@feedbackWidget.recordUsage(context: { approved: true }, null, =>
|
||||
@notify(
|
||||
type: 'error'
|
||||
msg: __('Your AI result usage could not be recorded.')
|
||||
)
|
||||
false
|
||||
)
|
||||
|
||||
@feedbackWidget.recordUsage(context: { approved: true }, null, -> false) if @feedbackWidget
|
||||
@approve(@result)
|
||||
@close()
|
||||
|
||||
|
||||
@@ -141,12 +141,10 @@ class App.TicketZoom extends App.Controller
|
||||
)
|
||||
)
|
||||
|
||||
load: (data, local = false, newTicketRaw = undefined) =>
|
||||
load: (data, local = false) =>
|
||||
@previousTicketRaw = @currentTicketRaw
|
||||
@currentTicketRaw = data.assets.Ticket[@ticket_id]
|
||||
|
||||
return if newTicketRaw
|
||||
|
||||
view = @ticket?.currentView()
|
||||
readable = @ticket?.userGroupAccess('read')
|
||||
changeable = @ticket?.userGroupAccess('change')
|
||||
@@ -1176,7 +1174,7 @@ class App.TicketZoom extends App.Controller
|
||||
# reset form after save
|
||||
@reset()
|
||||
|
||||
@load(data, false, true)
|
||||
@load(data, false)
|
||||
|
||||
if @sidebarWidget
|
||||
@sidebarWidget.commit()
|
||||
|
||||
@@ -149,7 +149,8 @@ class EmailReply extends App.Controller
|
||||
if selected
|
||||
quote_header = @replyQuoteHeader(article)
|
||||
|
||||
selected = "<div><br><br/></div><div><blockquote type=\'cite\'>#{quote_header}#{selected}<br></blockquote></div><div><br></div>"
|
||||
marker = if signaturePosition is 'top' then ' data-marker=\'signature-before\'' else ''
|
||||
selected = "<div><br><br/></div><div><blockquote type=\'cite\'#{marker}>#{quote_header}#{selected}<br></blockquote></div><div><br></div>"
|
||||
|
||||
# add selected text to body
|
||||
body = selected + body
|
||||
@@ -258,7 +259,7 @@ class EmailReply extends App.Controller
|
||||
|
||||
quote_header = App.FullQuoteHeader.fullQuoteHeaderForward(article)
|
||||
|
||||
body = "<br/><div>---#{App.i18n.translateInline('Begin forwarded message')}:---<br/><br/></div><div><blockquote type=\"cite\">#{quote_header}#{body}</blockquote></div><div><br></div>"
|
||||
body = "<br/><div>---#{App.i18n.translateInline('Begin forwarded message')}:---<br/><br/></div><div><blockquote type=\"cite\" data-marker=\"signature-before\">#{quote_header}#{body}</blockquote></div><div><br></div>"
|
||||
|
||||
articleNew = {}
|
||||
articleNew.body = body
|
||||
@@ -332,10 +333,8 @@ class EmailReply extends App.Controller
|
||||
task = App.TaskManager.get(ui.taskKey)
|
||||
if task && task.state && task.state.ticket && task.state.ticket.group_id
|
||||
group_id = task.state.ticket.group_id
|
||||
group = App.Group.find(group_id)
|
||||
signature = undefined
|
||||
if group && group.signature_id
|
||||
signature = App.Signature.find(group.signature_id)
|
||||
|
||||
result = App.SignatureHelper.findForGroup(group_id)
|
||||
|
||||
# remove signature if it was added but type is no longer email
|
||||
# https://github.com/zammad/zammad/issues/4453
|
||||
@@ -344,34 +343,39 @@ class EmailReply extends App.Controller
|
||||
return
|
||||
|
||||
# add/replace signature
|
||||
if signature && signature.active && signature.body
|
||||
if result
|
||||
|
||||
# if signature has changed, remove it but skip signatures in quoted messages
|
||||
# https://github.com/zammad/zammad/issues/5634
|
||||
signature_selector = ui.$('[data-signature=true]').not('blockquote [data-signature=true]')
|
||||
signature_id = signature_selector.data('signature-id')
|
||||
if signature_id && signature_id.toString() isnt signature.id.toString()
|
||||
signature_selector.remove()
|
||||
existingTopLevelSignature = ui.$('[data-signature=true]').not('blockquote [data-signature=true]').first()
|
||||
if existingTopLevelSignature.length > 0 && existingTopLevelSignature.attr('data-signature-id') is "#{result.signature.id}"
|
||||
unless signaturePosition is 'top'
|
||||
App.Utils.htmlImage2DataUrlAsyncInline(ui.$('[contenteditable=true]'))
|
||||
return
|
||||
|
||||
# remove existing top-level signature (skip signatures in quoted messages)
|
||||
# https://github.com/zammad/zammad/issues/5634, https://github.com/zammad/zammad/issues/2319
|
||||
App.SignatureHelper.removeTopLevel(ui.$('[data-name=body]'))
|
||||
|
||||
# apply new signature
|
||||
signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), ticket: ticketCurrent, config: App.Config.all() })
|
||||
signatureFinished = App.SignatureHelper.render(result.signature.body, ticketCurrent)
|
||||
|
||||
body = ui.$('[data-name=body]')
|
||||
if App.Utils.signatureCheck(body.html() || '', signatureFinished)
|
||||
if !App.Utils.htmlLastLineEmpty(body)
|
||||
body.append('<br><br>')
|
||||
signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>")
|
||||
App.Utils.htmlStrip(signature)
|
||||
sigEl = App.SignatureHelper.buildElement(result.signature.id, signatureFinished)
|
||||
|
||||
placeholder = body.find('[data-signature-placeholder]')
|
||||
if placeholder.length > 0
|
||||
placeholder[0].replaceWith(signature[0])
|
||||
else if signaturePosition is 'top'
|
||||
body.prepend(signature)
|
||||
placeholder = body.find('[data-signature-placeholder]')
|
||||
if placeholder.length > 0
|
||||
placeholder[0].replaceWith(sigEl[0])
|
||||
else
|
||||
# detect full-quote layout on autosave restore: check data-marker first, then leading BR fingerprint
|
||||
effectiveSigPosition = signaturePosition
|
||||
if not signaturePosition? && (body.find('blockquote[type=cite][data-marker=signature-before]').length > 0 || EmailReply.hasFullQuoteLayout(body))
|
||||
effectiveSigPosition = 'top'
|
||||
|
||||
if effectiveSigPosition is 'top'
|
||||
body.prepend(sigEl)
|
||||
body.prepend('<br><br>')
|
||||
else
|
||||
body.append(signature)
|
||||
ui.$('[data-name=body]').replaceWith(body)
|
||||
App.SignatureHelper.appendToBottom(body, sigEl)
|
||||
ui.$('[data-name=body]').replaceWith(body)
|
||||
else
|
||||
body = ui.$('[data-name=body]')
|
||||
|
||||
@@ -394,6 +398,61 @@ class EmailReply extends App.Controller
|
||||
# convert remote images into data urls
|
||||
App.Utils.htmlImage2DataUrlAsyncInline(ui.$('[contenteditable=true]'))
|
||||
|
||||
# returns true when the body starts with a bare BR before a blockquote (full-quote fingerprint)
|
||||
@hasFullQuoteLayout: (body) ->
|
||||
return false unless body.find('blockquote[type=cite]').length > 0
|
||||
firstChild = body.get(0)?.firstChild
|
||||
firstChild = firstChild.nextSibling while firstChild?.nodeType is 3
|
||||
firstChild?.nodeName is 'BR'
|
||||
|
||||
@updateSignatureByGroup: (type, ticket, ui, newGroupId) ->
|
||||
return if type isnt 'email'
|
||||
|
||||
ticketCurrent = App.Ticket.fullLocal(ticket.id)
|
||||
result = App.SignatureHelper.findForGroup(newGroupId)
|
||||
|
||||
body = ui.$('[data-name=body]')
|
||||
|
||||
if result
|
||||
|
||||
# apply new signature
|
||||
signatureFinished = App.SignatureHelper.render(result.signature.body, ticketCurrent)
|
||||
newSig = App.SignatureHelper.buildElement(result.signature.id, signatureFinished)
|
||||
|
||||
existingSignature = body.find('[data-signature=true]').not('blockquote [data-signature=true]').first()
|
||||
if existingSignature.length > 0
|
||||
# preserve signature in place if it already matches the new group (e.g. no actual change)
|
||||
if existingSignature.attr('data-signature-id') is "#{result.signature.id}"
|
||||
App.Utils.htmlImage2DataUrlAsyncInline(ui.$('[contenteditable=true]'))
|
||||
return
|
||||
# replace in-place to preserve the signature's position in the body
|
||||
existingSignature.replaceWith(newSig[0])
|
||||
else
|
||||
# place signature before the full-quote block if marked, append otherwise
|
||||
markedBlockquote = body.find('blockquote[type=cite][data-marker=signature-before]').first()
|
||||
if markedBlockquote.length > 0
|
||||
# place before the full-quote's container
|
||||
parents = markedBlockquote.parentsUntil(body)
|
||||
quoteContainer = if parents.length > 0 then parents.last() else markedBlockquote
|
||||
prevElement = quoteContainer.prev()
|
||||
if prevElement.length > 0
|
||||
prevElement.before(newSig)
|
||||
else
|
||||
quoteContainer.before(newSig)
|
||||
# add blank lines above signature if not already present
|
||||
unless newSig[0].previousSibling?.nodeName is 'BR'
|
||||
newSig.before('<br><br>')
|
||||
else
|
||||
App.SignatureHelper.appendToBottom(body, newSig)
|
||||
else
|
||||
# no signature for the new group – remove the existing one
|
||||
App.SignatureHelper.removeTopLevel(body)
|
||||
|
||||
ui.$('[data-name=body]').replaceWith(body)
|
||||
|
||||
# convert remote images into data urls
|
||||
App.Utils.htmlImage2DataUrlAsyncInline(ui.$('[contenteditable=true]'))
|
||||
|
||||
@validation: (type, params, ui) ->
|
||||
return true if type isnt 'email'
|
||||
|
||||
|
||||
@@ -123,6 +123,13 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||
@updateSecurityOptions()
|
||||
)
|
||||
|
||||
# update signature when group changes
|
||||
@controllerBind('ui::ticket::updateSignature', (data) =>
|
||||
return if data.taskKey isnt @taskKey
|
||||
|
||||
@updateSignatureByGroup(data.newGroupId)
|
||||
)
|
||||
|
||||
# Listen to security setting changes.
|
||||
@controllerBind('config_update', (data) =>
|
||||
return if not /^(pgp|smime)_integration$/.test(data.name)
|
||||
@@ -558,13 +565,18 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||
|
||||
@updateSecurityTypeToolbar()
|
||||
|
||||
setArticleTypePost: (type, signaturePosition = 'bottom') =>
|
||||
setArticleTypePost: (type, signaturePosition = undefined) =>
|
||||
for localConfig in @actions()
|
||||
if localConfig && localConfig.setArticleTypePost
|
||||
localConfig.setArticleTypePost(@type, @ticket, @, signaturePosition)
|
||||
|
||||
@evaluateAttachmentsList()
|
||||
|
||||
updateSignatureByGroup: (newGroupId) =>
|
||||
for localConfig in @actions()
|
||||
if localConfig && localConfig.updateSignatureByGroup
|
||||
localConfig.updateSignatureByGroup(@type, @ticket, @, newGroupId)
|
||||
|
||||
isScrolledToBottom: ->
|
||||
return @el.scrollParent().scrollTop() + @el.scrollParent().height() is @el.scrollParent().prop('scrollHeight')
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
class TicketZoomFormHandlerSignature
|
||||
|
||||
# central method, is getting called on every ticket form change
|
||||
# but only trigger event for group_id changes
|
||||
@run: (params, attribute, attributes, classname, form, ui) ->
|
||||
|
||||
return if attribute.name isnt 'group_id'
|
||||
|
||||
App.Event.trigger('ui::ticket::updateSignature', { taskKey: ui.taskKey, newGroupId: params.group_id })
|
||||
|
||||
App.Config.set('130-ticketFormSignature', TicketZoomFormHandlerSignature, 'TicketZoomFormHandler')
|
||||
@@ -132,10 +132,6 @@ class App.SidebarTicketSummary extends App.Controller
|
||||
@badgeRenderLocal()
|
||||
@feedbackWidget?.recordUsage({}, null, =>
|
||||
@hasUsage = false
|
||||
@notify(
|
||||
type: 'error'
|
||||
msg: __('Your AI result usage could not be recorded.')
|
||||
)
|
||||
false
|
||||
)
|
||||
@hasUsage = true
|
||||
@@ -177,6 +173,8 @@ class App.SidebarTicketSummary extends App.Controller
|
||||
|
||||
renderSummarization: (data) =>
|
||||
@summaryData = data if data
|
||||
@isPreparingData = (_.isNull(@summaryData?.result) or @summaryData?.error)
|
||||
|
||||
@badgeRenderLocal()
|
||||
|
||||
return if !@elSidebar
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
class App.SessionMessage extends App.ControllerModal
|
||||
FORCE_RELOAD_TIMEOUT: 1000
|
||||
showTrySupport: true
|
||||
|
||||
constructor: (params) ->
|
||||
@initForcedReloadInterval(params)
|
||||
|
||||
super
|
||||
|
||||
initForcedReloadInterval: (params) =>
|
||||
return if not params.forceReload or not params.reloadTimeout
|
||||
|
||||
buttonSubmit = params.buttonSubmit || __('Reload')
|
||||
|
||||
# Override default button label to show initial remaining time.
|
||||
params.buttonSubmit = @buttonSubmitWithReload(buttonSubmit, params.reloadTimeout)
|
||||
|
||||
@totalDisplayTime = -1000 # account for try delay from modal subclass
|
||||
|
||||
callback = =>
|
||||
@totalDisplayTime += @FORCE_RELOAD_TIMEOUT
|
||||
|
||||
timeLeft = params.reloadTimeout - @totalDisplayTime
|
||||
$('.js-submit').text(@buttonSubmitWithReload(buttonSubmit, timeLeft))
|
||||
|
||||
@windowReload() if @totalDisplayTime >= params.reloadTimeout
|
||||
|
||||
@interval(callback, @FORCE_RELOAD_TIMEOUT, 'session_message_force_reload')
|
||||
|
||||
buttonSubmitWithReload: (buttonSubmit, timeLeft) ->
|
||||
suffix = if timeLeft >= 0 then " (#{Math.ceil(timeLeft / 1000)})" else ''
|
||||
App.i18n.translateInline(buttonSubmit) + suffix
|
||||
|
||||
onCancel: (e) =>
|
||||
if @forceReload
|
||||
@windowReload(e)
|
||||
|
||||
@@ -78,7 +78,7 @@ class App.WidgetTextTools extends App.Controller
|
||||
@contextData: (ce) ->
|
||||
$(ce.element).data().plugin_texttools?.searchCondition or {}
|
||||
|
||||
@showModal: (e, ce, selection, el) ->
|
||||
@showModal: (e, ce, selection, el, callback) ->
|
||||
textToolId = $(e.target).data('id')
|
||||
|
||||
new App.TextToolsModal(
|
||||
@@ -86,10 +86,12 @@ class App.WidgetTextTools extends App.Controller
|
||||
contextData: @contextData(ce)
|
||||
textTool: App.AITextTool.find(textToolId)
|
||||
selectedText: selection.content
|
||||
approve: (result) -> ce.replaceSelection(selection.ranges, result)
|
||||
approve: (result) ->
|
||||
ce.replaceSelection(selection.ranges, result)
|
||||
callback() if callback
|
||||
)
|
||||
|
||||
@renderDropdown: (e, ce, selection, el) ->
|
||||
@renderDropdown: (e, ce, selection, el, callback) ->
|
||||
bubbleMenuElement = $(e.target).closest('.js-bubbleMenu')
|
||||
popupContainerElement = bubbleMenuElement.find('.dropup-container')
|
||||
|
||||
@@ -107,7 +109,7 @@ class App.WidgetTextTools extends App.Controller
|
||||
|
||||
textToolsDropdown.off('mousedown.text-tools-dropdown').on 'mousedown.text-tools-dropdown', '.js-action', (e) ->
|
||||
e.preventDefault()
|
||||
App.WidgetTextTools.showModal(e, ce, selection, el)
|
||||
App.WidgetTextTools.showModal(e, ce, selection, el, callback)
|
||||
textToolsDropdown.removeClass('open')
|
||||
popupContainerElement.parent().removeClass('show-dropdown')
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ App.Config.set('anthropic', {
|
||||
prio: 4000
|
||||
fields: ['token', 'model', 'ocr_active', 'ocr_model']
|
||||
required: ['token']
|
||||
default_model: 'claude-3-7-sonnet-latest'
|
||||
default_model: 'claude-sonnet-4-6'
|
||||
}, 'AIProviders')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
App.Config.set('azure', {
|
||||
key: 'azure'
|
||||
label: __('Azure AI')
|
||||
label: __('Azure AI (legacy deployment-based endpoints)')
|
||||
prio: 5000
|
||||
fields: ['token', 'url_completions', 'ocr_active', 'url_ocr'] # TODO: Add url_embeddings when needed.
|
||||
required: ['token', 'url_completions']
|
||||
|
||||
@@ -5,5 +5,5 @@ App.Config.set('mistral', {
|
||||
prio: 6000
|
||||
fields: ['token', 'model', 'ocr_active', 'ocr_model']
|
||||
required: ['token']
|
||||
default_model: 'mistral-medium-latest'
|
||||
default_model: 'mistral-large-2512'
|
||||
}, 'AIProviders')
|
||||
|
||||
@@ -6,5 +6,5 @@ App.Config.set('ollama', {
|
||||
url_placeholder: 'http://localhost:11434'
|
||||
fields: ['url', 'model', 'ocr_active', 'ocr_model']
|
||||
required: ['url']
|
||||
default_model: 'llama3.2'
|
||||
default_model: 'mistral-small3.2'
|
||||
}, 'AIProviders')
|
||||
|
||||
@@ -17,7 +17,7 @@ class App.Auth
|
||||
params.success(data, status, xhr)
|
||||
|
||||
error: (xhr, statusText, error) =>
|
||||
@_loginError()
|
||||
@_loginError(xhr)
|
||||
params.error(xhr, statusText, error)
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ class App.Auth
|
||||
callback()
|
||||
|
||||
error: (xhr, statusText, error) =>
|
||||
@_loginError()
|
||||
@_loginError(xhr)
|
||||
)
|
||||
|
||||
@logout: (rerender = true, callback) ->
|
||||
@@ -71,7 +71,7 @@ class App.Auth
|
||||
@_logout(rerender, callback)
|
||||
|
||||
error: (xhr, statusText, error) =>
|
||||
@_loginError()
|
||||
@_loginError(xhr)
|
||||
)
|
||||
Spine.Ajax.queue(performLogut)
|
||||
|
||||
@@ -195,7 +195,10 @@ class App.Auth
|
||||
if callback
|
||||
callback()
|
||||
|
||||
@_loginError: ->
|
||||
@_loginError: (data) ->
|
||||
details = data.responseJSON || {}
|
||||
App.Event.trigger('maintenance', type: 'invalid_csrf_token') if details.invalid_csrf_token
|
||||
|
||||
App.Log.debug 'Auth', '_loginError:error'
|
||||
|
||||
# empty session
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class App.SignatureHelper
|
||||
|
||||
# Look up active signature for a group. Returns { signature, group } or null.
|
||||
@findForGroup: (groupId) ->
|
||||
return null unless groupId
|
||||
group = App.Group.find(groupId)
|
||||
return null unless group?.signature_id
|
||||
signature = App.Signature.find(group.signature_id)
|
||||
return null unless signature?.active && signature.body
|
||||
{ signature, group }
|
||||
|
||||
# Render signature body with tag replacement. Ticket data is passed in, session/config are resolved internally.
|
||||
@render: (signatureBody, ticket) ->
|
||||
App.Utils.replaceTags(signatureBody, { user: App.Session.get(), ticket: ticket, config: App.Config.all() })
|
||||
|
||||
# Build signature DOM element with data attributes (returns jQuery object).
|
||||
@buildElement: (signatureId, renderedBody) ->
|
||||
el = $("<div data-signature=\"true\" data-signature-id=\"#{signatureId}\">#{renderedBody}</div>")
|
||||
App.Utils.htmlStrip(el)
|
||||
el
|
||||
|
||||
# Remove top-level signatures from body (preserves signatures inside blockquotes).
|
||||
@removeTopLevel: (body) ->
|
||||
body.find('[data-signature=true]').not('blockquote [data-signature=true]').remove()
|
||||
|
||||
# Append signature at the bottom of body, with spacing if needed.
|
||||
@appendToBottom: (body, signatureEl) ->
|
||||
if !App.Utils.htmlLastLineEmpty(body)
|
||||
body.append('<br><br>')
|
||||
body.append(signatureEl)
|
||||
@@ -553,27 +553,6 @@ class App.Utils
|
||||
if origHref != fixedHref then $(@).attr('href', fixedHref)
|
||||
)
|
||||
|
||||
# signatureNeeded = App.Utils.signatureCheck(message, signature)
|
||||
@signatureCheck: (message, signature) ->
|
||||
messageText = message.replace(/<[^>]+>/g, ' ')
|
||||
messageText = $('<div>' + messageText + '</div>').text().trim()
|
||||
# New UI editor treats new lines differently.
|
||||
# Thus the only way to match signature between new & old is to remove any white space characters.
|
||||
messageText = messageText.replace(/\s+/g, ' ')
|
||||
signatureText = signature.replace(/<[^>]+>/g, ' ')
|
||||
signatureText = $('<div>' + signatureText + '</div>').text().trim()
|
||||
signatureText = signatureText.replace(/\s+/g, ' ')
|
||||
|
||||
quote = (str) ->
|
||||
(str + '').replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&")
|
||||
|
||||
#console.log('SC', messageText, signatureText, quote(signatureText))
|
||||
regex = new RegExp(quote(signatureText), 'mi')
|
||||
if messageText.match(regex)
|
||||
false
|
||||
else
|
||||
true
|
||||
|
||||
# messageWithMarker = App.Utils.signatureIdentifyByPlaintext(message, false)
|
||||
@signatureIdentifyByPlaintext: (message, test = false, internal = false) ->
|
||||
textToSearch = @html2text(message)
|
||||
@@ -1697,4 +1676,4 @@ class App.Utils
|
||||
return
|
||||
|
||||
filename = @resolveFilename(xhr) || option.fallbackFilename
|
||||
@downloadBlob(blob, filename)
|
||||
@downloadBlob(blob, filename)
|
||||
@@ -319,28 +319,27 @@ class _webSocketSingleton extends App.Controller
|
||||
)
|
||||
|
||||
_ajaxReceive: =>
|
||||
return if !@client_id
|
||||
return if @_ajaxReceiveWorking is true
|
||||
@_ajaxReceiveWorking = true
|
||||
App.Ajax.request(
|
||||
id: 'message_receive',
|
||||
type: 'POST'
|
||||
url: @Config.get('api_path') + '/message_receive'
|
||||
data: JSON.stringify(client_id: @client_id)
|
||||
processData: false
|
||||
success: (data) =>
|
||||
@log 'debug', 'ajax:onmessage', data
|
||||
@_receiveMessage(data)
|
||||
if data && data.error
|
||||
App.Delay.set(=>
|
||||
return if !@client_id
|
||||
|
||||
App.Ajax.request(
|
||||
id: 'message_receive',
|
||||
type: 'POST'
|
||||
url: @Config.get('api_path') + '/message_receive'
|
||||
data: JSON.stringify(client_id: @client_id)
|
||||
processData: false
|
||||
success: (data) =>
|
||||
@log 'debug', 'ajax:onmessage', data
|
||||
@_receiveMessage(data)
|
||||
if data && data.error
|
||||
@client_id = undefined
|
||||
@_ajaxInit(force: true)
|
||||
@_ajaxReceive()
|
||||
error: (data) =>
|
||||
@client_id = undefined
|
||||
@_ajaxInit(force: true)
|
||||
@_ajaxReceiveWorking = false
|
||||
@_ajaxReceive()
|
||||
error: (data) =>
|
||||
@client_id = undefined
|
||||
@_ajaxInit(force: true)
|
||||
@_ajaxReceiveWorking = false
|
||||
)
|
||||
)
|
||||
, 2000, '_ajaxReceive', 'ws')
|
||||
|
||||
class Modal extends App.ControllerModal
|
||||
buttonClose: false
|
||||
|
||||
@@ -133,7 +133,7 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
|
||||
{
|
||||
name: name
|
||||
value: object.id
|
||||
inactive: object.active == false
|
||||
inactive: !@options.attribute.allowInactive && object.active == false
|
||||
}
|
||||
|
||||
showLoader: =>
|
||||
|
||||
@@ -38,6 +38,7 @@ class App.ExternalDataSourceAjaxSelect extends App.SearchableAjaxSelect
|
||||
@attribute.valueRaw = JSON.stringify(@attribute.value)
|
||||
|
||||
@renderElement()
|
||||
@toggleClear()
|
||||
|
||||
renderOptions: (options) ->
|
||||
# We need to transform the value that we don't lose the needed type information.
|
||||
@@ -130,6 +131,7 @@ class App.ExternalDataSourceAjaxSelect extends App.SearchableAjaxSelect
|
||||
return
|
||||
|
||||
@setShadowValue(value: key, label: value)
|
||||
@toggleClear()
|
||||
|
||||
onKeyUp: =>
|
||||
return if @input.val().trim() isnt '' || @attribute.multiple
|
||||
|
||||
@@ -675,7 +675,8 @@
|
||||
'index': 'KnowledgeBase::Answer::Translation',
|
||||
'url_type': 'agent',
|
||||
'highlight_enabled': false,
|
||||
'include_locale': true,
|
||||
'include_locale': true,
|
||||
'include_subtitle': true,
|
||||
}),
|
||||
processData: true,
|
||||
success: function(data, status, xhr) {
|
||||
|
||||
@@ -43,7 +43,7 @@ App.RichtextBubbleMenu =
|
||||
actionKey = actionNode.dataset.key
|
||||
activeAction = _.find(@allActions(), (item) -> item.key is actionKey)
|
||||
|
||||
activeAction.action(e, ce, selection, el) if activeAction
|
||||
activeAction.action(e, ce, selection, el, closeDropdown) if activeAction
|
||||
|
||||
return false
|
||||
)
|
||||
@@ -61,8 +61,8 @@ App.RichtextBubbleMenu =
|
||||
|
||||
setTimeout ->
|
||||
$(window).off('click.bubble-menu, keyup.bubble-menu').on 'click.bubble-menu, keyup.bubble-menu', (e) ->
|
||||
return if e.type is 'keyup' and e.key.startsWith('Shift')
|
||||
return if e.target.closest('.bubble-menu') # keep the bubble menu open if click is on the menu itself
|
||||
return if not window.getSelection().isCollapsed # keep the bubble menu open if there is still a selection
|
||||
return if e.target.closest('.bubble-menu') # keep the bubble menu open if the click is on the menu itself
|
||||
|
||||
closeDropdown()
|
||||
$(window).off('click.bubble-menu, keyup.bubble-menu')
|
||||
|
||||
@@ -106,15 +106,10 @@ App.ViewHelpers =
|
||||
# define mask helper
|
||||
# mask an value like 'a***********yz'
|
||||
M: (item, start = 1, end = 2) ->
|
||||
return '' if !item
|
||||
string = ''
|
||||
end = item.length - end - 1
|
||||
for n in [0..item.length-1]
|
||||
if start <= n && end >= n
|
||||
string += '*'
|
||||
else
|
||||
string += item[n]
|
||||
string
|
||||
if item
|
||||
'**********'
|
||||
else
|
||||
''
|
||||
|
||||
# define translation helper
|
||||
T: (item, args...) ->
|
||||
|
||||
@@ -16,7 +16,9 @@ class App.Model extends Spine.Model
|
||||
@constructor.className
|
||||
|
||||
displayName: ->
|
||||
return @name || '-'
|
||||
return App.i18n.translatePlain(@name) or '-' if @translate()
|
||||
|
||||
@name or '-'
|
||||
|
||||
# shows the icon representing the object itself (e. g. the organization icon in organization profile or ticket sidebar)
|
||||
icon: (user) ->
|
||||
@@ -112,12 +114,14 @@ class App.Model extends Spine.Model
|
||||
# return no errors
|
||||
return
|
||||
|
||||
# keep in sync with ChecksCoreWorkflow::EMPTY_VALUES
|
||||
@_validate_is_empty: (value) ->
|
||||
return true if value is ''
|
||||
return true if value is null
|
||||
return true if value is undefined
|
||||
return true if _.isArray(value) is true && value.length is 0
|
||||
return true if _.isArray(value) is true && value.length is 1 && value[0] is ''
|
||||
return true if _.isObject(value) is true && _.isEmpty(value)
|
||||
false
|
||||
|
||||
###
|
||||
|
||||
@@ -3,7 +3,7 @@ class App.DataPrivacyTask extends App.Model
|
||||
@extend Spine.Model.Ajax
|
||||
@url: @apiPath + '/data_privacy_tasks'
|
||||
@configure_attributes = [
|
||||
{ name: 'deletable_id', display: __('User'), tag: 'autocompletion_ajax', relation: 'User', do_not_log: true },
|
||||
{ name: 'deletable_id', display: __('User'), tag: 'autocompletion_ajax', relation: 'User', allow_inactive: true, do_not_log: true },
|
||||
{ name: 'state', display: __('State'), tag: 'input', readonly: 1 },
|
||||
{ name: 'created_by_id', display: __('Created by'), relation: 'User', readonly: 1 },
|
||||
{ name: 'created_at', display: __('Created'), tag: 'datetime', readonly: 1 },
|
||||
|
||||
@@ -4,8 +4,8 @@ class App.PublicLink extends App.Model
|
||||
@url: @apiPath + '/public_links'
|
||||
@configure_attributes = [
|
||||
{ name: 'link', display: __('Link'), tag: 'input', type: 'text', limit: 500, 'null': false, placeholder: 'https://link' },
|
||||
{ name: 'title', display: __('Title'), tag: 'input', type: 'text', limit: 200, 'null': false },
|
||||
{ name: 'description', display: __('Description (shown as title tag for screen readers)'), tag: 'input', type: 'text', limit: 200, 'null': true },
|
||||
{ name: 'title', display: __('Title'), tag: 'input', type: 'text', limit: 200, 'null': false, translate: true },
|
||||
{ name: 'description', display: __('Description (shown as title tag for screen readers)'), tag: 'input', type: 'text', limit: 200, 'null': true, translate: true },
|
||||
{ name: 'screen', display: __('Context'), tag: 'multiselect', options: { login: __('Login Screen'), signup: __('Signup Screen'), password_reset: __('Forgot Password Screen') }, default: ['login'], 'null': false, multiple: true, translate: true },
|
||||
{ name: 'new_tab', display: __('Open in new tab'), tag: 'select', options: { true: __('yes'), false: __('no'), }, default: true, 'null': false, translate: true },
|
||||
{ name: 'prio', display: __('Prio'), readonly: 1 },
|
||||
|
||||
@@ -10,9 +10,9 @@ class App.Webhook extends App.Model
|
||||
{ name: 'ssl_verify', display: __('SSL verification'), tag: 'boolean', null: true, translate: true, options: { true: 'yes', false: 'no' }, default: true },
|
||||
{ name: 'auth_type', display: __('Authentication'), tag: 'select', null: true, translate: true, nulloption: true, options: { 'basic_auth': __('HTTP Basic Authentication'), 'bearer_token': __('Bearer Token') } },
|
||||
{ name: 'basic_auth_username', display: __('Username'), tag: 'input', type: 'text', limit: 250, null: true, item_class: 'formGroup--halfSize' },
|
||||
{ name: 'basic_auth_password', display: __('Password'), tag: 'input', type: 'text', limit: 250, null: true, item_class: 'formGroup--halfSize' },
|
||||
{ name: 'basic_auth_password', display: __('Password'), tag: 'input', type: 'text', input_type: 'password', limit: 250, null: true, item_class: 'formGroup--halfSize' },
|
||||
{ name: 'bearer_token', display: __('Bearer Token'), tag: 'input', type: 'text', input_type: 'password', limit: 250, null: true },
|
||||
{ name: 'signature_token', display: __('HMAC SHA1 Signature Token'), tag: 'input', type: 'text', limit: 100, null: true },
|
||||
{ name: 'signature_token', display: __('HMAC SHA1 Signature Token'), tag: 'input', type: 'text', input_type: 'password', limit: 200, null: true },
|
||||
{ name: 'customized_payload', display: __('Custom Payload'), tag: 'switch', null: true, label_class: 'hidden' },
|
||||
{ name: 'custom_payload', display: __('Custom Payload'), tag: 'code_editor', null: true, collapsible: true, label_class: 'hidden', hint: __('To revert back to the default payload, simply turn off the "Custom Payload" switch above.') },
|
||||
{ name: 'note', display: __('Note'), tag: 'richtext', null: true, note: '', limit: 250 },
|
||||
@@ -36,4 +36,4 @@ You can use webhooks in Zammad to send ticket, article, and attachment data when
|
||||
return @name if !@endpoint
|
||||
if @active is false
|
||||
return "#{@name} (#{@endpoint}) (#{App.i18n.translateInline('inactive')})"
|
||||
"#{@name} (#{@endpoint})"
|
||||
"#{@name} (#{@endpoint})"
|
||||
@@ -1,3 +1,3 @@
|
||||
<div class="js-missingProviderAlert alert alert--warning <% if !@visible: %>hide<% end %>">
|
||||
<%- @T('The provider configuration is missing. Please set up the provider before proceeding in |AI > Providers|.') %>
|
||||
<%- @T('The provider configuration is disabled. Please set up the provider before proceeding in |AI > Providers|.') %>
|
||||
</div>
|
||||
@@ -215,7 +215,7 @@ $(function() {
|
||||
<li><%- @T('The chat is turned off.') %>
|
||||
<li><%- @T('There are too many people in the chat queue.') %>
|
||||
</ol>
|
||||
<%- @T('When you turn on debugging by setting the option §debug§ to §true§ the reason gets printed to the javascript console.') %>
|
||||
<%- @T('When you turn on debugging by setting the option §debug§ to §true§ the reason gets printed to the JavaScript console.') %>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<p><%- @T("Zammad Forms requires jQuery. If you don't already use it on your website, you can add it like this:") %></p>
|
||||
<pre><code class="language-html js-code"><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script></code></pre>
|
||||
|
||||
<p><%- @T('You need to add the following Javascript code snippet to your web page:') %></p>
|
||||
<p><%- @T('You need to add the following JavaScript code snippet to your web page:') %></p>
|
||||
|
||||
<pre class="js-modal"><code class="language-html js-code js-paramsBlock"><button id="zammad-feedback-form">Feedback</button>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<label for="application_secret"><%- @T('Facebook App Secret') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="application_secret" type="text" name="application_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.application_secret %><% end %>" class="form-control" required autocomplete="off">
|
||||
<input id="application_secret" type="password" name="application_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.application_secret %><% end %>" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<h2><%- @T('Your callback URL') %></h2>
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
<% for id, day of @days: %>
|
||||
<% if @hours[id].timeframes[slot]: %>
|
||||
<td data-day="<%- id %>" data-slot="<%- slot %>" class="form-group day-time<%- ' is-active' if @hours[id].active %>">
|
||||
<label for="<%- id %>_start_time"><%- @T('From') %></label>
|
||||
<label for="<%- id %>_start_time" class="sr-only"><%- @T('Start time') %></label>
|
||||
<input type="text" id="<%- id %>_start_time" name="<%= @attribute.name %>::<%- id %>::start" value="<%- @hours[id].timeframes[slot][0] %>" data-day="<%- id %>" data-slot="<%- slot %>" data-i="0" class="form-control form-control--small time js-time">
|
||||
<label for="<%- id %>_end_time"><%- @T('Till') %></label>
|
||||
<div class="day-time-separator">–</div>
|
||||
<label for="<%- id %>_end_time" class="sr-only"><%- @T('End time') %></label>
|
||||
<input type="text" id="<%- id %>_end_time" name="<%= @attribute.name %>::<%- id %>::end" value="<%- @hours[id].timeframes[slot][1] %>" data-day="<%- id %>" data-slot="<%- slot %>" data-i="1" class="form-control form-control--small time js-time">
|
||||
<% else: %>
|
||||
<td class="empty-cell">
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<% end %>
|
||||
<div class="event-row">
|
||||
<div>
|
||||
<h4><%- @T('From') %></h4>
|
||||
<h4><%- @T('Start date') %></h4>
|
||||
<p><%- @datetime(event.start_date) %></p>
|
||||
</div>
|
||||
<div>
|
||||
<h4><%- @T('Till') %></h4>
|
||||
<h4><%- @T('End date') %></h4>
|
||||
<p><%- @datetime(event.end_date) %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="<%= @attribute.class %> horizontal-filters-switch horizontal-filters-switch--align-start">
|
||||
<div class="<%= @attribute.class %> horizontal-filters-switch horizontal-filters-switch--align-start"<% if @attribute.theme: %> data-theme="<%= @attribute.theme %>"<% end %>>
|
||||
<label>
|
||||
<%- @T(@attribute.display) %>
|
||||
<div class="zammad-switch zammad-switch--small js-switch">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||
<span class="label-text"><%- @Ti(role.displayName()) %></span>
|
||||
<% if role.note: %><span class="help-text"><%- @T(role.note) %></span><% end %>
|
||||
<% if role.note: %><span class="help-text"><%- @T(App.Utils.html2text(role.note)) %></span><% end %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user