Fixes #6216 - Packages> Integrate API-based package management into core.
Approved-by: Dusan Vuckovic <dv@zammad.com> Co-authored-by: Dusan Vuckovic <dv@zammad.com>
This commit is contained in:
@@ -89,8 +89,11 @@ RUN apt-get update -qq && \
|
||||
apt-get upgrade -y && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# ZAMMAD_DOCKER is used by Zammad to take decisions for containerized environments,
|
||||
# e.g. hiding the package management interface because installed packages do not persist.
|
||||
# Application variables with defaults matching the Zammad docker stack.
|
||||
ENV POSTGRESQL_DB=zammad_production \
|
||||
ENV ZAMMAD_DOCKER=true \
|
||||
POSTGRESQL_DB=zammad_production \
|
||||
POSTGRESQL_HOST=zammad-postgresql \
|
||||
POSTGRESQL_PORT=5432 \
|
||||
POSTGRESQL_USER=zammad \
|
||||
|
||||
@@ -8,7 +8,10 @@ class Package extends App.ControllerSubContent
|
||||
|
||||
events:
|
||||
'change .js-fileUpload': 'selectFile'
|
||||
'click .package-action[data-type="uninstall"]': 'action'
|
||||
'click .js-packageSettings': 'packageSettings'
|
||||
'click .js-packageAction[data-table-action="uninstall"]': 'uninstall'
|
||||
'click .js-packageAction[data-table-action="update_api"]': 'update_api'
|
||||
'click .js-packageAction[data-table-action="install_api"]': 'install_api'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@@ -24,6 +27,9 @@ class Package extends App.ControllerSubContent
|
||||
@packages = data.packages
|
||||
@package_installation = data.package_installation
|
||||
@local_gemfiles = data.local_gemfiles
|
||||
@token_setting_id = data.token_setting_id
|
||||
@token_present = data.token_present
|
||||
@api_package_metas = data.api_package_metas || {}
|
||||
@render()
|
||||
)
|
||||
|
||||
@@ -38,15 +44,28 @@ class Package extends App.ControllerSubContent
|
||||
item.action = ['install']
|
||||
else if item.state == 'deactivate'
|
||||
item.action = ['uninstall', 'activate']
|
||||
item.action = ['uninstall']
|
||||
if @api_package_metas[item.name] && item.vendor is @api_package_metas[item.name].vendor
|
||||
if @isNewerVersion(item.version, @api_package_metas[item.name].version)
|
||||
item.action.unshift('update_api')
|
||||
|
||||
api_packages_installable = []
|
||||
for name, data of @api_package_metas
|
||||
continue if _.find(@packages, (row) -> row.name is name)
|
||||
|
||||
api_packages_installable.push(name)
|
||||
|
||||
@html App.view('package')(
|
||||
head: __('Dashboard')
|
||||
packages: @packages
|
||||
package_installation: @package_installation
|
||||
local_gemfiles: @local_gemfiles
|
||||
token_present: @token_present
|
||||
api_package_metas: @api_package_metas
|
||||
api_packages_installable: api_packages_installable
|
||||
)
|
||||
|
||||
action: (e) ->
|
||||
uninstall: (e) ->
|
||||
e.preventDefault()
|
||||
id = $(e.target).parents('[data-id]').data('id')
|
||||
|
||||
@@ -66,10 +85,126 @@ class Package extends App.ControllerSubContent
|
||||
)
|
||||
)
|
||||
|
||||
isNewerVersion: (local, remote) ->
|
||||
local_parts = local.split('.').map((n) -> parseInt(n))
|
||||
remote_parts = remote.split('.').map((n) -> parseInt(n))
|
||||
local_parts[0] * 1000000 + local_parts[1] * 1000 + local_parts[2] < remote_parts[0] * 1000000 + remote_parts[1] * 1000 + remote_parts[2]
|
||||
|
||||
selectFile: (e) ->
|
||||
if !_.isEmpty(@fileUpload.val())
|
||||
@packageSubmit.prop('disabled', false)
|
||||
else
|
||||
@packageSubmit.prop('disabled', true)
|
||||
|
||||
App.Config.set('Packages', { prio: 3700, name: __('Packages'), parent: '#system', target: '#system/package', controller: Package, permission: ['admin.package'] }, 'NavBarAdmin')
|
||||
update_api: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
id = $(e.target).parents('[data-id]').data('id')
|
||||
@ajax(
|
||||
id: 'packages'
|
||||
type: 'PUT'
|
||||
url: "#{@apiPath}/packages/api",
|
||||
data: JSON.stringify(id: id)
|
||||
processData: false
|
||||
success: =>
|
||||
@notify(
|
||||
type: 'success'
|
||||
msg: __('Package updated successfully.')
|
||||
removeAll: true
|
||||
)
|
||||
@load()
|
||||
)
|
||||
|
||||
install_api: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
id = $(e.target).parents('[data-id]').data('id')
|
||||
@ajax(
|
||||
id: 'packages'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/packages/api",
|
||||
data: JSON.stringify(id: id)
|
||||
processData: false
|
||||
success: =>
|
||||
@notify(
|
||||
type: 'success'
|
||||
msg: __('Package installed successfully.')
|
||||
removeAll: true
|
||||
)
|
||||
@load()
|
||||
)
|
||||
|
||||
packageSettings: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
new PackageSettingsModal(
|
||||
token_setting_id: @token_setting_id
|
||||
token_present: @token_present
|
||||
parent: @
|
||||
)
|
||||
|
||||
class PackageSettingsModal extends App.ControllerModal
|
||||
buttonClose: true
|
||||
buttonCancel: true
|
||||
buttonSubmit: true
|
||||
head: __('Configure')
|
||||
events:
|
||||
'click .js-reset': 'resetToDefault'
|
||||
|
||||
constructor: (params) ->
|
||||
if params.token_present
|
||||
@centerButtons = [
|
||||
{
|
||||
text: __('Reset to default settings')
|
||||
className: 'btn--danger js-reset'
|
||||
}
|
||||
]
|
||||
|
||||
super
|
||||
|
||||
content: ->
|
||||
token_value = ''
|
||||
token_value = 'present' if @token_present
|
||||
|
||||
configureAttributes = [
|
||||
{ name: 'token', display: __('API Token'), tag: 'input', type: 'password', autocomplete: 'one-time-code', single: true, null: true, value: token_value, help: __('Please enter the API token you received from Zammad.') },
|
||||
]
|
||||
@controller = new App.ControllerForm(
|
||||
model:
|
||||
configure_attributes: configureAttributes
|
||||
className: ''
|
||||
autofocus: true
|
||||
)
|
||||
@controller.form
|
||||
|
||||
resetToDefault: =>
|
||||
return if !@token_present
|
||||
|
||||
target = @controller.form[0]
|
||||
|
||||
$(target).find('[name="token"]').val('')
|
||||
|
||||
@onSubmit(new Event('submit', { cancelable: true, bubbles: true, target: target }))
|
||||
|
||||
onSubmit: (e) =>
|
||||
params = @formParam(e.target)
|
||||
return @close() if params.token is 'present'
|
||||
|
||||
@ajax(
|
||||
type: 'PUT'
|
||||
url: "#{@apiPath}/settings/#{@token_setting_id}"
|
||||
data: JSON.stringify({ state_current: { value: params.token } }),
|
||||
success: (data, status, xhr) =>
|
||||
@notify(
|
||||
type: 'success'
|
||||
msg: __('Update successful.')
|
||||
removeAll: true
|
||||
)
|
||||
@close()
|
||||
)
|
||||
|
||||
close: =>
|
||||
super
|
||||
@parent.load()
|
||||
|
||||
App.Config.set('Packages', { prio: 3700, name: __('Packages'), parent: '#system', target: '#system/package', controller: Package, permission: (controller) -> App.Config.get('admin_packages') && controller.permissionCheck('admin.package') }, 'NavBarAdmin')
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<div class="page-header-title">
|
||||
<h1><%- @T( 'Package' ) %> <small><%- @T('Management') %></small></h1>
|
||||
<div class="page-header">
|
||||
<div class="page-header-title">
|
||||
<h1><%- @T( 'Package' ) %> <small><%- @T('Management') %></small></h1>
|
||||
</div>
|
||||
<div class="page-header-meta">
|
||||
<a data-type="settings" class="btn js-packageSettings" href="#"><%- @T('Configure') %></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
@@ -41,26 +46,111 @@
|
||||
</form>
|
||||
</p>
|
||||
|
||||
<table class="table table-striped table-hover">
|
||||
<h2 class="package-table-heading"><%- @T('Installed packages') %></h2>
|
||||
|
||||
<% if _.isEmpty(@packages): %>
|
||||
<table class="table table--placeholder">
|
||||
<thead><tr><th><%- @T('No Entries') %>
|
||||
</table>
|
||||
<% else: %>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%- @T('Name') %></th>
|
||||
<th><%- @T('Version') %></th>
|
||||
<th><%- @T('Vendor') %></th>
|
||||
<th><%- @T('State') %></th>
|
||||
<th><%- @T('Action') %></th>
|
||||
<th class="package-action-header"><%- @T('Action') %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for item in @packages: %>
|
||||
<tr data-id="<%= item.id %>">
|
||||
<td><%= item.name %></td>
|
||||
<td><%= item.version %></td>
|
||||
<% meta = @api_package_metas[item.name]; lastLog = meta && meta.change_log && meta.change_log.length && meta.change_log[meta.change_log.length - 1] %>
|
||||
<td><%= item.version %><% if _.contains(item.action, 'update_api'): %> <span class="badge badge--closed"<% if lastLog: %> title="<%= lastLog.content %> (<%- @Ttimestamp(lastLog.date) %>)"<% end %>><%- @T('Update to %s', meta.version) %></span><% end %></td>
|
||||
<td><% if item.url: %><a href="<%= item.url %>" target="_blank"><% end %><%= item.vendor %><% if item.url: %></a><% end %></td>
|
||||
<td><%- @T(item.state) %></td>
|
||||
<td><% for action in item.action: %><a href="#" class="package-action btn btn--action<%= ' btn--danger' if action == 'uninstall' %>" data-type="<%= action %>"><%- @T(action) %></a><br/><% end %></td>
|
||||
<td class="package-action-cell no-padding">
|
||||
<div class="dropdown dropdown--actions">
|
||||
<div class="btn btn--table btn--text btn--secondary js-action package-action-toggle" data-toggle="dropdown">
|
||||
<%- @Icon('overflow-button') %>
|
||||
</div>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<% for action in item.action: %>
|
||||
<li role="presentation" class="js-packageAction" data-table-action="<%= action %>">
|
||||
<a role="menuitem" tabindex="-1">
|
||||
<% if action is 'update_api': %>
|
||||
<span class="dropdown-iconSpacer">
|
||||
<%- @Icon('reload') %>
|
||||
</span>
|
||||
<%- @T('Update package') %>
|
||||
<% else if action is 'uninstall': %>
|
||||
<span class="dropdown-iconSpacer">
|
||||
<%- @Icon('trash') %>
|
||||
</span>
|
||||
<%- @T('Uninstall package') %>
|
||||
<% else: %>
|
||||
<%- @T(action) %>
|
||||
<% end %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
|
||||
<% if @token_present: %>
|
||||
<h2 class="package-table-heading"><%- @T('Available packages') %></h2>
|
||||
<% if _.isEmpty(@api_packages_installable): %>
|
||||
<table class="table table--placeholder">
|
||||
<thead><tr><th><%- @T('No Entries') %>
|
||||
</table>
|
||||
<% else: %>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%- @T('Name') %></th>
|
||||
<th><%- @T('Version') %></th>
|
||||
<th><%- @T('Vendor') %></th>
|
||||
<th><%- @T('State') %></th>
|
||||
<th class="package-action-header"><%- @T('Action') %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for name in @api_packages_installable: %>
|
||||
<% item = @api_package_metas[name] %>
|
||||
<tr data-id="<%= item.name %>">
|
||||
<td><%= item.name %></td>
|
||||
<td><%= item.version %></td>
|
||||
<td><% if item.url: %><a href="<%= item.url %>" target="_blank"><% end %><%= item.vendor %><% if item.url: %></a><% end %></td>
|
||||
<td><%- @T('not installed') %></td>
|
||||
<td class="package-action-cell no-padding">
|
||||
<div class="dropdown dropdown--actions">
|
||||
<div class="btn btn--table btn--text btn--secondary js-action package-action-toggle" data-toggle="dropdown">
|
||||
<%- @Icon('overflow-button') %>
|
||||
</div>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li role="presentation" class="js-packageAction" data-table-action="install_api">
|
||||
<a role="menuitem" tabindex="-1">
|
||||
<span class="dropdown-iconSpacer">
|
||||
<%- @Icon('package') %>
|
||||
</span>
|
||||
<%- @T('Install package') %>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -16201,3 +16201,23 @@ div.accounted-time-value {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.package {
|
||||
&-table-heading {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
&-action-header {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
&-action-cell {
|
||||
overflow: visible !important;
|
||||
text-align: right;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
&-action-toggle {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ class PackagesController < ApplicationController
|
||||
packages: Package.reorder('name'),
|
||||
package_installation: Package.app_package_installation?,
|
||||
local_gemfiles: Package.gem_files?,
|
||||
token_setting_id: Setting.find_by(name: 'packages_token').id,
|
||||
token_present: Setting.get('packages_token').present?,
|
||||
api_package_metas: api_package_metas,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -27,4 +30,40 @@ class PackagesController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
# POST /api/v1/packages/api
|
||||
def install_api
|
||||
api_package = api_packages[params[:id]]
|
||||
raise "Can not find package '#{params[:id]}'!" if api_package.blank?
|
||||
|
||||
Package.install(string: api_package.to_json)
|
||||
render json: {
|
||||
success: true
|
||||
}
|
||||
end
|
||||
|
||||
# PUT /api/v1/packages/api
|
||||
def update_api
|
||||
package = Package.find(params[:id])
|
||||
|
||||
api_package = api_packages[package.name]
|
||||
raise "Can not find package '#{package.name}'!" if api_package.blank?
|
||||
|
||||
Package.install(string: api_package.to_json)
|
||||
render json: {
|
||||
success: true
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_packages
|
||||
@api_packages ||= Package.api_packages_hash({ version_name: Package.api_version_name })
|
||||
end
|
||||
|
||||
def api_package_metas
|
||||
api_packages.transform_values do |value|
|
||||
value.slice('name', 'version', 'vendor', 'license', 'url', 'change_log', 'description')
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -336,6 +336,9 @@ class SessionsController < ApplicationController
|
||||
# Announce Elasticsearch availability to the front end.
|
||||
config['es_enabled'] = SearchIndexBackend.enabled?
|
||||
|
||||
# Hide the admin packages interface in environments like docker, where installed packages do not persist.
|
||||
config['admin_packages'] = ENV['ZAMMAD_DOCKER'].blank?
|
||||
|
||||
# remember if we can switch back to user
|
||||
if session[:switched_from_user_id]
|
||||
config['switch_back_to_possible'] = true
|
||||
|
||||
@@ -357,6 +357,60 @@ subsequently in a separate step.
|
||||
File.exist?('/usr/bin/zammad')
|
||||
end
|
||||
|
||||
def self.api_token
|
||||
ENV['PACKAGES_TOKEN'] || Setting.get('packages_token')
|
||||
end
|
||||
|
||||
def self.api_version_name
|
||||
Rails.root.join('VERSION').read.chomp.split('.').tap { |row| row[2] = 'x' }[0..2].join('.')
|
||||
end
|
||||
|
||||
def self.api_packages(params)
|
||||
return [] if api_token.blank?
|
||||
|
||||
cache_key = "PackagesController/api_packages/#{api_token}/#{params.to_json}"
|
||||
cache = Rails.cache.read(cache_key)
|
||||
return cache if !cache.nil?
|
||||
|
||||
zip_file = api_zip_packages(params)
|
||||
return [] if zip_file.blank?
|
||||
|
||||
result = []
|
||||
begin
|
||||
Zip::File.open(zip_file.path) do |zip|
|
||||
zip.sort.each do |entry|
|
||||
next if !entry.name.end_with?('.zpm')
|
||||
|
||||
content = entry.get_input_stream.read
|
||||
data = JSON.parse(content)
|
||||
result << data
|
||||
end
|
||||
end
|
||||
ensure
|
||||
zip_file.close!
|
||||
end
|
||||
|
||||
Rails.cache.write(cache_key, result, expires_in: 1.hour)
|
||||
result
|
||||
end
|
||||
|
||||
def self.api_packages_hash(params)
|
||||
api_packages(params).index_by { |row| row['name'] }
|
||||
end
|
||||
|
||||
def self.api_zip_packages(params)
|
||||
require 'zip'
|
||||
|
||||
response = UserAgent.get('https://support.zammad.com/api/v1/addon_releases/download/organization', params, { bearer_token: api_token })
|
||||
return if !response.code.starts_with?('2')
|
||||
|
||||
zip_file = Tempfile.new
|
||||
zip_file.binmode
|
||||
zip_file.write(response.body)
|
||||
zip_file.rewind
|
||||
zip_file
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
reinstall package
|
||||
|
||||
@@ -7,5 +7,7 @@ Zammad::Application.routes.draw do
|
||||
match api_path + '/packages', to: 'packages#index', via: :get
|
||||
match api_path + '/packages', to: 'packages#install', via: :post
|
||||
match api_path + '/packages', to: 'packages#uninstall', via: :delete
|
||||
match api_path + '/packages/api', to: 'packages#update_api', via: :put
|
||||
match api_path + '/packages/api', to: 'packages#install_api', via: :post
|
||||
|
||||
end
|
||||
|
||||
20
db/migrate/20260622000000_add_packages_token_setting.rb
Normal file
20
db/migrate/20260622000000_add_packages_token_setting.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class AddPackagesTokenSetting < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
# return if it's a new setup
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Packages Token',
|
||||
name: 'packages_token',
|
||||
area: 'Core',
|
||||
description: 'This setting defines the token to access the support.zammad.com instance for package remote commands.',
|
||||
options: {},
|
||||
state: '',
|
||||
preferences: { online_service_disable: true, permission: ['admin.package'] },
|
||||
frontend: false
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -6393,3 +6393,14 @@ Setting.create_if_not_exists(
|
||||
},
|
||||
frontend: true,
|
||||
)
|
||||
|
||||
Setting.create_if_not_exists(
|
||||
title: __('Packages Token'),
|
||||
name: 'packages_token',
|
||||
area: 'Core',
|
||||
description: __('This setting defines the token to access the support.zammad.com instance for package remote commands.'),
|
||||
options: {},
|
||||
state: '',
|
||||
preferences: { online_service_disable: true, permission: ['admin.package'] },
|
||||
frontend: false,
|
||||
)
|
||||
|
||||
126
i18n/zammad.pot
126
i18n/zammad.pot
@@ -839,6 +839,7 @@ msgstr ""
|
||||
msgid "API Password Access"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:170
|
||||
#: app/assets/javascripts/app/views/integration/placetel.jst.eco:38
|
||||
msgid "API Token"
|
||||
msgstr ""
|
||||
@@ -1027,7 +1028,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco:10
|
||||
#: app/assets/javascripts/app/views/object_manager/attribute/tree_select.jst.eco:9
|
||||
#: app/assets/javascripts/app/views/object_manager/index.jst.eco:59
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:51
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:63
|
||||
#: app/assets/javascripts/app/views/profile/password.jst.eco:32
|
||||
#: app/assets/javascripts/app/views/session.jst.eco:13
|
||||
#: app/assets/javascripts/app/views/tag/table.jst.eco:6
|
||||
@@ -1042,7 +1043,7 @@ msgstr ""
|
||||
msgid "Action execution"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/CommonActionMenu/CommonActionMenu.vue:129
|
||||
#: app/frontend/apps/desktop/components/CommonActionMenu/CommonActionMenu.vue:131
|
||||
msgid "Action menu button"
|
||||
msgstr ""
|
||||
|
||||
@@ -1495,7 +1496,7 @@ msgstr ""
|
||||
msgid "After approval, please re-connect your Microsoft365 mailbox."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:12
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:17
|
||||
msgid "After installing, updating, or uninstalling packages the following commands need to be executed on the server:"
|
||||
msgstr ""
|
||||
|
||||
@@ -2457,6 +2458,10 @@ msgstr ""
|
||||
msgid "Available methods"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:109
|
||||
msgid "Available packages"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/ai/agent/type/ticket_tagger.rb:119
|
||||
msgid "Available tags are limited to existing in the manage tags section. AI agent won't be able to generate new ones. %l"
|
||||
msgstr ""
|
||||
@@ -3736,8 +3741,10 @@ msgstr ""
|
||||
msgid "Configuration of performance settings via environment variables."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:150
|
||||
#: app/assets/javascripts/app/views/integration/exchange.jst.eco:25
|
||||
#: app/assets/javascripts/app/views/integration/ldap.jst.eco:6
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:6
|
||||
msgid "Configure"
|
||||
msgstr ""
|
||||
|
||||
@@ -4020,6 +4027,7 @@ msgstr ""
|
||||
msgid "Copy current object number (e. g. Ticket#) to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailTopBar/TopBarHeaderCompact.vue:34
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailTopBar/TopBarHeaderFull.vue:47
|
||||
msgid "Copy organization display name"
|
||||
msgstr ""
|
||||
@@ -4032,8 +4040,8 @@ msgstr ""
|
||||
msgid "Copy text"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/TopBarHeaderCompact.vue:56
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/TopBarHeaderFull.vue:73
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/TopBarHeaderCompact.vue:34
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/TopBarHeaderFull.vue:54
|
||||
msgid "Copy ticket number"
|
||||
msgstr ""
|
||||
|
||||
@@ -4057,7 +4065,6 @@ msgstr ""
|
||||
msgid "Copy token"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailTopBar/TopBarHeaderCompact.vue:34
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailTopBar/TopBarHeaderCompact.vue:34
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailTopBar/TopBarHeaderFull.vue:43
|
||||
msgid "Copy user display name"
|
||||
@@ -4612,7 +4619,7 @@ msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_plugin/keyboard_shortcuts.coffee:147
|
||||
#: app/assets/javascripts/app/controllers/dashboard.coffee:28
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:43
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:59
|
||||
#: app/frontend/apps/desktop/pages/dashboard/routes.ts:25
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
@@ -5539,7 +5546,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/controllers/data_privacy.coffee:188
|
||||
#: app/assets/javascripts/app/controllers/knowledge_base/base_form.coffee:78
|
||||
#: app/assets/javascripts/app/controllers/knowledge_base/delete_action.coffee:9
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:54
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:73
|
||||
#: app/assets/javascripts/app/controllers/user.coffee:102
|
||||
#: app/assets/javascripts/app/controllers/user_profile/action_row.coffee:90
|
||||
#: app/assets/javascripts/app/lib/mixins/knowledge_base_actions.coffee:26
|
||||
@@ -6326,8 +6333,8 @@ msgstr ""
|
||||
msgid "Edit security keys"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/TopBarHeaderCompact.vue:117
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/TopBarHeaderFull.vue:161
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/TopBarHeaderCompact.vue:95
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/TopBarHeaderFull.vue:142
|
||||
msgid "Edit ticket title"
|
||||
msgstr ""
|
||||
|
||||
@@ -8454,7 +8461,7 @@ msgstr ""
|
||||
msgid "Highlight icon"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:96
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:100
|
||||
msgid "Highlight options"
|
||||
msgstr ""
|
||||
|
||||
@@ -8466,15 +8473,15 @@ msgstr ""
|
||||
msgid "Highlighted text"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:89
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:93
|
||||
msgid "Highlighter color: %s"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:157
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:161
|
||||
msgid "Highlighting is active. Select content in the ticket article to apply."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:159
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:163
|
||||
msgid "Highlighting is inactive."
|
||||
msgstr ""
|
||||
|
||||
@@ -9104,14 +9111,22 @@ msgstr ""
|
||||
msgid "Install App"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:40
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:45
|
||||
msgid "Install Package"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:32
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:144
|
||||
msgid "Install package"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:37
|
||||
msgid "Installed"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:49
|
||||
msgid "Installed packages"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/ai/agent/type/custom_ai_agent.rb:34
|
||||
msgid "Instruction"
|
||||
msgstr ""
|
||||
@@ -10461,7 +10476,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/views/api.jst.eco:2
|
||||
#: app/assets/javascripts/app/views/calendar/index.jst.eco:3
|
||||
#: app/assets/javascripts/app/views/data_privacy/index.jst.eco:3
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:2
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:3
|
||||
#: app/assets/javascripts/app/views/sla/index.jst.eco:3
|
||||
#: app/assets/javascripts/app/views/translation/index.jst.eco:4
|
||||
#: app/assets/javascripts/app/views/user.jst.eco:3
|
||||
@@ -10632,7 +10647,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/views/popover/organization.jst.eco:6
|
||||
#: app/assets/javascripts/app/views/widget/organization.jst.eco:29
|
||||
#: app/frontend/apps/desktop/components/Organization/OrganizationPopoverWithTrigger/OrganizationPopover.vue:79
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailViewContent.vue:142
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailViewContent.vue:145
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarOrganization/TicketSidebarOrganizationContent.vue:71
|
||||
#: app/frontend/apps/mobile/components/Organization/OrganizationMembersList.vue:36
|
||||
msgid "Members"
|
||||
@@ -11061,7 +11076,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/views/integration/pgp_list.jst.eco:11
|
||||
#: app/assets/javascripts/app/views/knowledge_base_video_servers/index.jst.eco:10
|
||||
#: app/assets/javascripts/app/views/object_manager/index.jst.eco:56
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:47
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:59
|
||||
#: app/assets/javascripts/app/views/profile/devices.jst.eco:12
|
||||
#: app/assets/javascripts/app/views/profile/password.jst.eco:30
|
||||
#: app/assets/javascripts/app/views/profile/token_access.jst.eco:16
|
||||
@@ -11464,6 +11479,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/views/integration/pgp_list.jst.eco:6
|
||||
#: app/assets/javascripts/app/views/integration/smime_group.jst.eco:12
|
||||
#: app/assets/javascripts/app/views/integration/smime_list.jst.eco:6
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:53
|
||||
#: app/assets/javascripts/app/views/ssl_certificates_list.jst.eco:6
|
||||
#: app/assets/javascripts/app/views/time_accounting/by_activity.jst.eco:3
|
||||
#: app/assets/javascripts/app/views/time_accounting/by_customer.jst.eco:3
|
||||
@@ -12206,7 +12222,7 @@ msgstr ""
|
||||
msgid "Only available for users with shared organizations"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:9
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:14
|
||||
msgid "Only packages from known, trusted, and verified sources should be installed."
|
||||
msgstr ""
|
||||
|
||||
@@ -12439,7 +12455,7 @@ msgstr ""
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailTopBar/TopBarHeaderFull.vue:28
|
||||
#: app/frontend/apps/desktop/pages/organization/routes.ts:9
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/plugins/organization.ts:13
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailViewContent.vue:83
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailViewContent.vue:84
|
||||
#: app/frontend/apps/mobile/pages/organization/routes.ts:8
|
||||
#: app/frontend/apps/mobile/pages/organization/views/OrganizationDetailView.vue:30
|
||||
#: app/frontend/apps/mobile/pages/ticket/views/TicketInformation/plugins/organization.ts:6
|
||||
@@ -12484,7 +12500,7 @@ msgstr ""
|
||||
msgid "Organization support"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailViewContent.vue:191
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailViewContent.vue:194
|
||||
#: app/frontend/apps/mobile/pages/user/views/UserDetailView.vue:79
|
||||
msgid "Organization tickets"
|
||||
msgstr ""
|
||||
@@ -12717,15 +12733,27 @@ msgstr ""
|
||||
msgid "POP3"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:2
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:3
|
||||
msgid "Package"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:131
|
||||
msgid "Package installed successfully."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:112
|
||||
msgid "Package updated successfully."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:3
|
||||
#: db/seeds/permissions.rb:326
|
||||
msgid "Packages"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6390
|
||||
msgid "Packages Token"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/generic/table_pager.jst.eco:3
|
||||
msgid "Page %s"
|
||||
msgstr ""
|
||||
@@ -13081,6 +13109,10 @@ msgstr ""
|
||||
msgid "Please enter a valid email address."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:170
|
||||
msgid "Please enter the API token you received from Zammad."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/integration/exchange_wizard.jst.eco:14
|
||||
msgid "Please enter the endpoint for accessing Exchange Web Services."
|
||||
msgstr ""
|
||||
@@ -13758,7 +13790,7 @@ msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/link/ticket/list.jst.eco:1
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarInformation/TicketSidebarInformationContent.vue:142
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailViewContent.vue:177
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailViewContent.vue:180
|
||||
msgid "Related tickets"
|
||||
msgstr ""
|
||||
|
||||
@@ -13873,7 +13905,7 @@ msgstr ""
|
||||
msgid "Remove formatting"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:89
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:93
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/composables/useHighlightMenuState.ts:21
|
||||
msgid "Remove highlight"
|
||||
msgstr ""
|
||||
@@ -14130,6 +14162,7 @@ msgstr ""
|
||||
msgid "Reset to default"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:158
|
||||
#: app/assets/javascripts/app/views/profile/notification.jst.eco:75
|
||||
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingNotifications.vue:223
|
||||
msgid "Reset to default settings"
|
||||
@@ -14660,7 +14693,7 @@ msgstr ""
|
||||
msgid "Search URL is unsafe."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailViewContent.vue:172
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationDetailViewContent.vue:175
|
||||
#: app/frontend/apps/desktop/pages/organization/components/OrganizationRelatedTickets/OrganizationTicketList.vue:94
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserRelatedCustomerTickets/CustomerTicketList.vue:100
|
||||
msgid "Search all"
|
||||
@@ -14851,7 +14884,7 @@ msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/User/UserPopoverWithTrigger/UserPopover.vue:80
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarCustomer/TicketSidebarCustomerContent.vue:112
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailViewContent.vue:146
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailViewContent.vue:149
|
||||
#: app/frontend/apps/mobile/pages/ticket/views/TicketInformation/TicketInformationCustomer.vue:94
|
||||
#: app/frontend/apps/mobile/pages/user/views/UserDetailView.vue:133
|
||||
#: db/seeds/object_manager_attributes.rb:1187
|
||||
@@ -15073,7 +15106,7 @@ msgstr ""
|
||||
msgid "Selected conditions"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:155
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/components/HighlightMenu.vue:159
|
||||
msgid "Selected highlight color: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -16077,7 +16110,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/models/ticket.coffee:12
|
||||
#: app/assets/javascripts/app/views/agent_ticket_view/detail.jst.eco:23
|
||||
#: app/assets/javascripts/app/views/data_privacy/tasks.jst.eco:31
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:50
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:62
|
||||
#: app/graphql/gql/types/overview_type.rb:97
|
||||
#: app/views/mailer/ticket_create/fr.html.erb:10
|
||||
#: app/views/mailer/ticket_create/zh-cn.html.erb:10
|
||||
@@ -16164,7 +16197,7 @@ msgstr ""
|
||||
msgid "Storage Method"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:33
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:38
|
||||
msgid "Store"
|
||||
msgstr ""
|
||||
|
||||
@@ -17213,7 +17246,7 @@ msgstr ""
|
||||
msgid "The included security token is invalid. This may be just a temporary error, please reload your browser, and try again."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:7
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:12
|
||||
msgid "The installation of packages comes with security implications, because arbitrary code will be executed in the context of the Zammad application."
|
||||
msgstr ""
|
||||
|
||||
@@ -17967,7 +18000,7 @@ msgstr ""
|
||||
msgid "There is no rollback of this deletion possible. If you are sure that you wish to proceed, please type \"%s\" into the input."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:54
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:73
|
||||
msgid "There is no rollback of this deletion. If you are sure that you wish to proceed, please type \"%s\" into the input. All related data to this package will be lost."
|
||||
msgstr ""
|
||||
|
||||
@@ -18416,6 +18449,10 @@ msgstr ""
|
||||
msgid "This service shows you contacts of incoming calls and a caller list in realtime."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6393
|
||||
msgid "This setting defines the token to access the support.zammad.com instance for package remote commands."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/models/group.coffee:13
|
||||
msgid "This signature is inactive, it won't be included in the reply."
|
||||
msgstr ""
|
||||
@@ -19578,6 +19615,10 @@ msgstr ""
|
||||
msgid "Uncheck item"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:92
|
||||
msgid "Uninstall package"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/time_accounting/settings.jst.eco:12
|
||||
#: app/frontend/apps/desktop/components/Form/fields/FieldFilterSelector/operators/within-last-relative.ts:45
|
||||
msgid "Unit"
|
||||
@@ -19755,6 +19796,10 @@ msgstr ""
|
||||
msgid "Update existing records with the attributes specified in the import data."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:87
|
||||
msgid "Update package"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSharedDraftStart/TicketSidebarSharedDraftStartContent.vue:204
|
||||
msgid "Update shared draft"
|
||||
msgstr ""
|
||||
@@ -19771,6 +19816,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee:123
|
||||
#: app/assets/javascripts/app/controllers/_settings/form.coffee:81
|
||||
#: app/assets/javascripts/app/controllers/maintenance.coffee:74
|
||||
#: app/assets/javascripts/app/controllers/package.coffee:200
|
||||
#: app/assets/javascripts/app/models/setting.coffee:20
|
||||
msgid "Update successful."
|
||||
msgstr ""
|
||||
@@ -19784,6 +19830,10 @@ msgstr ""
|
||||
msgid "Update time"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:71
|
||||
msgid "Update to %s"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/models/ai_agent.coffee:11
|
||||
#: app/assets/javascripts/app/models/application.coffee:11
|
||||
#: app/assets/javascripts/app/models/calendar.coffee:14
|
||||
@@ -20043,7 +20093,7 @@ msgstr ""
|
||||
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundForm.ts:117
|
||||
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailOutboundForm.ts:73
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailTopBar/TopBarHeaderFull.vue:24
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailViewContent.vue:41
|
||||
#: app/frontend/apps/desktop/pages/user/components/UserDetailViewContent.vue:42
|
||||
#: app/frontend/apps/desktop/pages/user/routes.ts:9
|
||||
#: app/frontend/apps/mobile/pages/user/routes.ts:7
|
||||
#: app/frontend/apps/mobile/pages/user/views/UserDetailView.vue:32
|
||||
@@ -20296,7 +20346,7 @@ msgstr ""
|
||||
msgid "Vector DB knowledge base chunking strategy"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:49
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:61
|
||||
msgid "Vendor"
|
||||
msgstr ""
|
||||
|
||||
@@ -20351,7 +20401,7 @@ msgid "Verifying…"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/version.coffee:3
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:48
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:60
|
||||
#: app/assets/javascripts/app/views/version.jst.eco:2
|
||||
#: app/frontend/apps/mobile/pages/account/views/AccountOverview.vue:165
|
||||
#: lib/excel_sheet/ai/analytics/errors.rb:25
|
||||
@@ -22381,6 +22431,10 @@ msgstr ""
|
||||
msgid "none"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/views/package.jst.eco:132
|
||||
msgid "not installed"
|
||||
msgstr ""
|
||||
|
||||
#: app/helpers/knowledge_base_visibility_note_helper.rb:27
|
||||
msgid "not published"
|
||||
msgstr ""
|
||||
@@ -22449,7 +22503,7 @@ msgstr ""
|
||||
msgid "or sign in using"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:101
|
||||
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:109
|
||||
msgid "or use the reply actions on articles."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/install.rb'
|
||||
require 'tasks/zammad/package/install'
|
||||
Tasks::Zammad::Package::Install.register_rake_task
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class Install < Tasks::Zammad::Command
|
||||
class Install < Tasks::Zammad::PackageCommand
|
||||
|
||||
def self.usage
|
||||
"#{super} /path/to/package.zpm"
|
||||
|
||||
4
lib/tasks/zammad/package/install_api.rake
Normal file
4
lib/tasks/zammad/package/install_api.rake
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/package/install_api'
|
||||
Tasks::Zammad::Package::InstallApi.register_rake_task
|
||||
55
lib/tasks/zammad/package/install_api.rb
Normal file
55
lib/tasks/zammad/package/install_api.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class InstallApi < Tasks::Zammad::PackageCommand
|
||||
def self.usage
|
||||
"#{super} Zammad-Packages 7.0.x dry\n#{super} Zammad-Packages 7.0.x prod"
|
||||
end
|
||||
|
||||
def self.description
|
||||
'Install packages remotely via API to a certain version.'
|
||||
end
|
||||
|
||||
ARGUMENT_COUNT = 3
|
||||
|
||||
def self.task_handler
|
||||
package_name = ArgvHelper.argv[1]
|
||||
version_name = ArgvHelper.argv[2]
|
||||
prod_mode = ArgvHelper.argv[3] == 'prod'
|
||||
|
||||
validate_args!(version_name, ArgvHelper.argv[3])
|
||||
|
||||
puts "Executing rake task in #{prod_mode ? 'prod mode' : 'dry mode'}..."
|
||||
|
||||
show_post_install_hint = false
|
||||
::Package.api_packages({ version_name: version_name }).each do |package_data|
|
||||
next if package_name != 'all' && package_data['name'] != package_name
|
||||
next if package_up_to_date?(package_data)
|
||||
|
||||
show_post_install_hint |= install_package(package_data, prod_mode)
|
||||
end
|
||||
|
||||
return if !show_post_install_hint || !prod_mode
|
||||
|
||||
puts "Please run package migrations now via 'zammad:package:post_install'."
|
||||
end
|
||||
|
||||
def self.install_package(package_data, prod_mode)
|
||||
puts "Installing #{package_data['name']} #{package_data['version']}..."
|
||||
::Package.install(string: package_data.to_json) if prod_mode
|
||||
puts 'done.'
|
||||
prod_mode
|
||||
end
|
||||
|
||||
def self.package_up_to_date?(package_data)
|
||||
existing = ::Package.find_by(name: package_data['name'])
|
||||
existing.present? && Gem::Version.new(existing.version) >= Gem::Version.new(package_data['version'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/list.rb'
|
||||
require 'tasks/zammad/package/list'
|
||||
Tasks::Zammad::Package::List.register_rake_task
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class List < Tasks::Zammad::Command
|
||||
class List < Tasks::Zammad::PackageCommand
|
||||
|
||||
def self.description
|
||||
'List all installed Zammad addon packages'
|
||||
|
||||
4
lib/tasks/zammad/package/list_api.rake
Normal file
4
lib/tasks/zammad/package/list_api.rake
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/package/list_api'
|
||||
Tasks::Zammad::Package::ListApi.register_rake_task
|
||||
26
lib/tasks/zammad/package/list_api.rb
Normal file
26
lib/tasks/zammad/package/list_api.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class ListApi < Tasks::Zammad::PackageCommand
|
||||
def self.description
|
||||
'List packages remotely via API to a certain version.'
|
||||
end
|
||||
|
||||
def self.task_handler
|
||||
abort "Please set a token to access addons on support.zammad.com via:\n\nroot> zammad rails r \"Setting.set('packages_token', 'xxx')\"\n\n" if ::Package.api_token.blank?
|
||||
|
||||
puts 'Package'.ljust(50) + 'Version'.ljust(20) + 'Installed'.ljust(10) + 'Newest'.ljust(20)
|
||||
::Package.api_packages({ branch_name: 'master' }).each do |package_data|
|
||||
existing_data = ::Package.find_by(name: package_data['name'])
|
||||
|
||||
puts package_data['name'].ljust(50) + (existing_data ? existing_data['version'] : '').ljust(20) + (existing_data ? 'Yes' : 'No').ljust(10) + package_data['version'].ljust(20)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/migrate.rb'
|
||||
require 'tasks/zammad/package/migrate'
|
||||
Tasks::Zammad::Package::Migrate.register_rake_task
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
@@ -8,7 +8,7 @@ module Tasks
|
||||
# Package migrations must not be executed in the same process that also executed
|
||||
# Package.install or Package.link, as the codebase is in an inconsistent state.
|
||||
# This is enforced by Tasks:Zammad::Command which prevents command chaining.
|
||||
class Migrate < Tasks::Zammad::Command
|
||||
class Migrate < Tasks::Zammad::PackageCommand
|
||||
|
||||
def self.description
|
||||
'Execute all pending Zammad addon package migrations'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/post_install.rb'
|
||||
require 'tasks/zammad/package/post_install'
|
||||
Tasks::Zammad::Package::PostInstall.register_rake_task
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class PostInstall < Tasks::Zammad::Command
|
||||
class PostInstall < Tasks::Zammad::PackageCommand
|
||||
def self.description
|
||||
'Runs all steps to finalize package installation.'
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/precompile.rb'
|
||||
require 'tasks/zammad/package/precompile'
|
||||
Tasks::Zammad::Package::Precompile.register_rake_task
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
@@ -8,7 +8,7 @@ module Tasks
|
||||
# Package migrations must not be executed in the same process that also executed
|
||||
# Package.install or Package.link, as the codebase is in an inconsistent state.
|
||||
# This is enforced by Tasks:Zammad::Command which prevents command chaining.
|
||||
class Precompile < Tasks::Zammad::Command
|
||||
class Precompile < Tasks::Zammad::PackageCommand
|
||||
|
||||
def self.description
|
||||
'Execute all package related precompilations.'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/reinstall.rb'
|
||||
require 'tasks/zammad/package/reinstall'
|
||||
Tasks::Zammad::Package::Reinstall.register_rake_task
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class Reinstall < Tasks::Zammad::Command
|
||||
class Reinstall < Tasks::Zammad::PackageCommand
|
||||
|
||||
def self.usage
|
||||
"#{super} MyPackage"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/reinstall_all.rb'
|
||||
require 'tasks/zammad/package/reinstall_all'
|
||||
Tasks::Zammad::Package::ReinstallAll.register_rake_task
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class ReinstallAll < Tasks::Zammad::Command
|
||||
class ReinstallAll < Tasks::Zammad::PackageCommand
|
||||
|
||||
def self.description
|
||||
'Reinstall all installed Zammad addon packages'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/uninstall.rb'
|
||||
require 'tasks/zammad/package/uninstall'
|
||||
Tasks::Zammad::Package::Uninstall.register_rake_task
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class Uninstall < Tasks::Zammad::Command
|
||||
class Uninstall < Tasks::Zammad::PackageCommand
|
||||
|
||||
def self.usage
|
||||
"#{super} MyPackage"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/uninstall_all_files.rb'
|
||||
require 'tasks/zammad/package/uninstall_all_files'
|
||||
Tasks::Zammad::Package::UninstallAllFiles.register_rake_task
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class UninstallAllFiles < Tasks::Zammad::Command
|
||||
class UninstallAllFiles < Tasks::Zammad::PackageCommand
|
||||
def self.description
|
||||
'Uninstall all package files in the filesystem only without executing migrations'
|
||||
end
|
||||
|
||||
4
lib/tasks/zammad/package/update_api.rake
Normal file
4
lib/tasks/zammad/package/update_api.rake
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/package/update_api'
|
||||
Tasks::Zammad::Package::UpdateApi.register_rake_task
|
||||
53
lib/tasks/zammad/package/update_api.rb
Normal file
53
lib/tasks/zammad/package/update_api.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class UpdateApi < Tasks::Zammad::PackageCommand
|
||||
def self.usage
|
||||
"#{super} all 7.0.x dry\n#{super} all 7.0.x prod"
|
||||
end
|
||||
|
||||
def self.description
|
||||
'Update packages remotely via API to a certain version.'
|
||||
end
|
||||
|
||||
ARGUMENT_COUNT = 3
|
||||
|
||||
def self.task_handler
|
||||
package_name = ArgvHelper.argv[1]
|
||||
version_name = ArgvHelper.argv[2]
|
||||
prod_mode = ArgvHelper.argv[3] == 'prod'
|
||||
|
||||
validate_args!(version_name, ArgvHelper.argv[3])
|
||||
|
||||
puts "Executing rake task in #{prod_mode ? 'prod mode' : 'dry mode'}..."
|
||||
|
||||
show_post_install_hint = false
|
||||
::Package.api_packages({ version_name: version_name }).each do |package_data|
|
||||
next if package_name != 'all' && package_data['name'] != package_name
|
||||
|
||||
show_post_install_hint |= update_package(package_data, prod_mode)
|
||||
end
|
||||
|
||||
return if !show_post_install_hint || !prod_mode
|
||||
|
||||
puts "Please run package migrations now via 'zammad:package:post_install'."
|
||||
end
|
||||
|
||||
def self.update_package(package_data, prod_mode)
|
||||
existing = ::Package.find_by(name: package_data['name'])
|
||||
return false if existing.blank?
|
||||
return false if Gem::Version.new(existing.version) >= Gem::Version.new(package_data['version'])
|
||||
|
||||
puts "Updating #{package_data['name']} from version #{existing.version} to #{package_data['version']}..."
|
||||
::Package.install(string: package_data.to_json) if prod_mode
|
||||
puts 'done.'
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_dependency 'tasks/zammad/package/verify.rb'
|
||||
require 'tasks/zammad/package/verify'
|
||||
Tasks::Zammad::Package::Verify.register_rake_task
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
require 'tasks/zammad/package_command'
|
||||
|
||||
module Tasks
|
||||
module Zammad
|
||||
module Package
|
||||
class Verify < Tasks::Zammad::Command
|
||||
class Verify < Tasks::Zammad::PackageCommand
|
||||
|
||||
def self.description
|
||||
'Verfies all installed Zammad addon packages'
|
||||
'Verifies all installed Zammad addon packages'
|
||||
end
|
||||
|
||||
def self.task_handler
|
||||
|
||||
28
lib/tasks/zammad/package_command.rb
Normal file
28
lib/tasks/zammad/package_command.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'tasks/zammad/command'
|
||||
|
||||
class Tasks::Zammad::PackageCommand < Tasks::Zammad::Command
|
||||
def self.package_token
|
||||
ENV['PACKAGES_TOKEN'] || Setting.get('packages_token')
|
||||
end
|
||||
|
||||
def self.validate_args!(version_name, mode)
|
||||
abort "Error: Invalid parameter version name '#{version_name}'!" if version_name.blank? || version_name !~ %r{^(\d+)\.(\d+)\.x\.?$}
|
||||
abort "Error: Invalid parameter execution mode '#{mode}'!" if %w[dry prod].exclude?(mode)
|
||||
abort "Please set a token to access addons on support.zammad.com via:\n\nroot> zammad rails r \"Setting.set('packages_token', 'xxx')\"\n\n" if ::Package.api_token.blank?
|
||||
end
|
||||
|
||||
def self.request_zip_file(params)
|
||||
require 'zip'
|
||||
|
||||
response = UserAgent.get('https://support.zammad.com/api/v1/addon_releases/download/organization', params, { bearer_token: package_token })
|
||||
abort 'Error: Download of organization package files failed!' if !response.code.starts_with?('2')
|
||||
|
||||
zip_file = Tempfile.new
|
||||
zip_file.binmode
|
||||
zip_file.write(response.body)
|
||||
zip_file.rewind
|
||||
zip_file
|
||||
end
|
||||
end
|
||||
112
spec/lib/tasks/zammad/package/install_api_spec.rb
Normal file
112
spec/lib/tasks/zammad/package/install_api_spec.rb
Normal file
@@ -0,0 +1,112 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Tasks::Zammad::Package::InstallApi do
|
||||
describe '.description' do
|
||||
it 'returns the description' do
|
||||
expect(described_class.description).to eq('Install packages remotely via API to a certain version.')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.task_handler' do
|
||||
before do
|
||||
allow(ArgvHelper).to receive(:argv).and_return(argv)
|
||||
allow(Package).to receive_messages(api_token: token, api_packages: api_packages)
|
||||
end
|
||||
|
||||
let(:token) { 'secret' }
|
||||
let(:api_packages) { [] }
|
||||
|
||||
context 'with invalid version name' do
|
||||
let(:argv) { %w[zammad:package:install_api all invalid dry] }
|
||||
|
||||
it 'aborts with error' do
|
||||
expect { described_class.task_handler }
|
||||
.to raise_error(SystemExit)
|
||||
.and output("Error: Invalid parameter version name 'invalid'!\n").to_stderr
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid execution mode' do
|
||||
let(:argv) { %w[zammad:package:install_api all 7.0.x execute] }
|
||||
|
||||
it 'aborts with error' do
|
||||
expect { described_class.task_handler }
|
||||
.to raise_error(SystemExit)
|
||||
.and output("Error: Invalid parameter execution mode 'execute'!\n").to_stderr
|
||||
end
|
||||
end
|
||||
|
||||
context 'without token' do
|
||||
let(:argv) { %w[zammad:package:install_api all 7.0.x dry] }
|
||||
let(:token) { nil }
|
||||
|
||||
it 'aborts with hint' do
|
||||
expect { described_class.task_handler }
|
||||
.to raise_error(SystemExit)
|
||||
.and output(%r{Please set a token}).to_stderr
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid arguments in dry mode' do
|
||||
let(:argv) { %w[zammad:package:install_api all 7.0.x dry] }
|
||||
let(:api_packages) do
|
||||
[{ 'name' => 'TestPackage', 'version' => '7.0.1' }]
|
||||
end
|
||||
|
||||
it 'does not call Package.install', :aggregate_failures do
|
||||
allow(Package).to receive(:install)
|
||||
expect { described_class.task_handler }.to output(%r{dry mode}).to_stdout
|
||||
expect(Package).not_to have_received(:install)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid arguments in prod mode' do
|
||||
let(:argv) { %w[zammad:package:install_api all 7.0.x prod] }
|
||||
let(:api_packages) do
|
||||
[{ 'name' => 'TestPackage', 'version' => '7.0.1' }]
|
||||
end
|
||||
|
||||
before { allow(Package).to receive(:find_by).and_return(nil) }
|
||||
|
||||
it 'calls Package.install and outputs progress', :aggregate_failures do
|
||||
allow(Package).to receive(:install)
|
||||
expect { described_class.task_handler }
|
||||
.to output(%r{Installing TestPackage 7\.0\.1}).to_stdout
|
||||
expect(Package).to have_received(:install).with(string: api_packages.first.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when package is already up-to-date' do
|
||||
let(:argv) { %w[zammad:package:install_api all 7.0.x prod] }
|
||||
let(:api_packages) do
|
||||
[{ 'name' => 'TestPackage', 'version' => '7.0.1' }]
|
||||
end
|
||||
let(:existing) { instance_double(Package, version: '7.0.1') }
|
||||
|
||||
before { allow(Package).to receive(:find_by).and_return(existing) }
|
||||
|
||||
it 'skips the package' do
|
||||
allow(Package).to receive(:install)
|
||||
described_class.task_handler
|
||||
expect(Package).not_to have_received(:install)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by package name' do
|
||||
let(:argv) { %w[zammad:package:install_api OtherPackage 7.0.x prod] }
|
||||
let(:api_packages) do
|
||||
[{ 'name' => 'TestPackage', 'version' => '7.0.1' }]
|
||||
end
|
||||
|
||||
before { allow(Package).to receive(:find_by).and_return(nil) }
|
||||
|
||||
it 'skips packages that do not match' do
|
||||
allow(Package).to receive(:install)
|
||||
described_class.task_handler
|
||||
expect(Package).not_to have_received(:install)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
63
spec/lib/tasks/zammad/package/list_api_spec.rb
Normal file
63
spec/lib/tasks/zammad/package/list_api_spec.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Tasks::Zammad::Package::ListApi do
|
||||
describe '.description' do
|
||||
it 'returns the description' do
|
||||
expect(described_class.description).to eq('List packages remotely via API to a certain version.')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.task_handler' do
|
||||
before do
|
||||
allow(Package).to receive_messages(api_token: token, api_packages: api_packages)
|
||||
end
|
||||
|
||||
let(:token) { 'secret' }
|
||||
let(:api_packages) { [] }
|
||||
|
||||
context 'without token' do
|
||||
let(:token) { nil }
|
||||
|
||||
it 'aborts with hint' do
|
||||
expect { described_class.task_handler }
|
||||
.to raise_error(SystemExit)
|
||||
.and output(%r{Please set a token}).to_stderr
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no packages available' do
|
||||
it 'outputs only the header' do
|
||||
expect { described_class.task_handler }
|
||||
.to output(%r{Package.*Version.*Installed.*Newest}).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a package that is not installed' do
|
||||
let(:api_packages) { [{ 'name' => 'TestPackage', 'version' => '7.0.1' }] }
|
||||
|
||||
before { allow(Package).to receive(:find_by).and_return(nil) }
|
||||
|
||||
it 'shows the package as not installed' do
|
||||
expect { described_class.task_handler }
|
||||
.to output(%r{TestPackage.*No.*7.0.1}).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a package that is installed' do
|
||||
let(:api_packages) { [{ 'name' => 'TestPackage', 'version' => '7.0.2' }] }
|
||||
let(:existing) { instance_double(Package, version: '7.0.1') }
|
||||
|
||||
before do
|
||||
allow(Package).to receive(:find_by).and_return(existing)
|
||||
allow(existing).to receive(:[]).with('version').and_return('7.0.1')
|
||||
end
|
||||
|
||||
it 'shows the installed version and newest version' do
|
||||
expect { described_class.task_handler }
|
||||
.to output(%r{TestPackage.*7.0.1.*Yes.*7.0.2}).to_stdout
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
114
spec/lib/tasks/zammad/package/update_api_spec.rb
Normal file
114
spec/lib/tasks/zammad/package/update_api_spec.rb
Normal file
@@ -0,0 +1,114 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Tasks::Zammad::Package::UpdateApi do
|
||||
describe '.description' do
|
||||
it 'returns the description' do
|
||||
expect(described_class.description).to eq('Update packages remotely via API to a certain version.')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.task_handler' do
|
||||
before do
|
||||
allow(ArgvHelper).to receive(:argv).and_return(argv)
|
||||
allow(Package).to receive_messages(api_token: token, api_packages: api_packages)
|
||||
end
|
||||
|
||||
let(:token) { 'secret' }
|
||||
let(:api_packages) { [] }
|
||||
|
||||
context 'with invalid version name' do
|
||||
let(:argv) { %w[zammad:package:update_api all invalid dry] }
|
||||
|
||||
it 'aborts with error' do
|
||||
expect { described_class.task_handler }
|
||||
.to raise_error(SystemExit)
|
||||
.and output("Error: Invalid parameter version name 'invalid'!\n").to_stderr
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid execution mode' do
|
||||
let(:argv) { %w[zammad:package:update_api all 7.0.x execute] }
|
||||
|
||||
it 'aborts with error' do
|
||||
expect { described_class.task_handler }
|
||||
.to raise_error(SystemExit)
|
||||
.and output("Error: Invalid parameter execution mode 'execute'!\n").to_stderr
|
||||
end
|
||||
end
|
||||
|
||||
context 'without token' do
|
||||
let(:argv) { %w[zammad:package:update_api all 7.0.x dry] }
|
||||
let(:token) { nil }
|
||||
|
||||
it 'aborts with hint' do
|
||||
expect { described_class.task_handler }
|
||||
.to raise_error(SystemExit)
|
||||
.and output(%r{Please set a token}).to_stderr
|
||||
end
|
||||
end
|
||||
|
||||
context 'when package is not installed' do
|
||||
let(:argv) { %w[zammad:package:update_api all 7.0.x prod] }
|
||||
let(:api_packages) { [{ 'name' => 'TestPackage', 'version' => '7.0.2' }] }
|
||||
|
||||
before { allow(Package).to receive(:find_by).and_return(nil) }
|
||||
|
||||
it 'skips the package' do
|
||||
allow(Package).to receive(:install)
|
||||
described_class.task_handler
|
||||
expect(Package).not_to have_received(:install)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when installed version is already newest' do
|
||||
let(:argv) { %w[zammad:package:update_api all 7.0.x prod] }
|
||||
let(:api_packages) { [{ 'name' => 'TestPackage', 'version' => '7.0.2' }] }
|
||||
let(:existing) { instance_double(Package, version: '7.0.2') }
|
||||
|
||||
before { allow(Package).to receive(:find_by).and_return(existing) }
|
||||
|
||||
it 'skips the package' do
|
||||
allow(Package).to receive(:install)
|
||||
described_class.task_handler
|
||||
expect(Package).not_to have_received(:install)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a newer version is available in prod mode' do
|
||||
let(:argv) { %w[zammad:package:update_api all 7.0.x prod] }
|
||||
let(:api_packages) { [{ 'name' => 'TestPackage', 'version' => '7.0.2' }] }
|
||||
let(:existing) { instance_double(Package, version: '7.0.1') }
|
||||
|
||||
before do
|
||||
allow(Package).to receive(:find_by).and_return(existing)
|
||||
allow(existing).to receive(:[]).with('version').and_return('7.0.1')
|
||||
end
|
||||
|
||||
it 'calls Package.install and outputs progress', :aggregate_failures do
|
||||
allow(Package).to receive(:install)
|
||||
expect { described_class.task_handler }
|
||||
.to output(%r{Updating TestPackage from version 7\.0\.1 to 7\.0\.2}).to_stdout
|
||||
expect(Package).to have_received(:install).with(string: api_packages.first.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a newer version is available in dry mode' do
|
||||
let(:argv) { %w[zammad:package:update_api all 7.0.x dry] }
|
||||
let(:api_packages) { [{ 'name' => 'TestPackage', 'version' => '7.0.2' }] }
|
||||
let(:existing) { instance_double(Package, version: '7.0.1') }
|
||||
|
||||
before do
|
||||
allow(Package).to receive(:find_by).and_return(existing)
|
||||
allow(existing).to receive(:[]).with('version').and_return('7.0.1')
|
||||
end
|
||||
|
||||
it 'does not call Package.install', :aggregate_failures do
|
||||
allow(Package).to receive(:install)
|
||||
expect { described_class.task_handler }.to output(%r{dry mode}).to_stdout
|
||||
expect(Package).not_to have_received(:install)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user