zendesk_apps_support 4.29.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +176 -0
- data/README.md +39 -0
- data/config/locales/en.yml +163 -0
- data/config/locales/translations/zendesk_apps_support.yml +405 -0
- data/lib/zendesk_apps_support.rb +42 -0
- data/lib/zendesk_apps_support/app_file.rb +44 -0
- data/lib/zendesk_apps_support/app_requirement.rb +11 -0
- data/lib/zendesk_apps_support/app_version.rb +78 -0
- data/lib/zendesk_apps_support/assets/default_app_logo.svg +10 -0
- data/lib/zendesk_apps_support/assets/default_styles.scss +3 -0
- data/lib/zendesk_apps_support/assets/default_template.html.erb +10 -0
- data/lib/zendesk_apps_support/assets/installed.js.erb +21 -0
- data/lib/zendesk_apps_support/assets/src.js.erb +37 -0
- data/lib/zendesk_apps_support/build_translation.rb +51 -0
- data/lib/zendesk_apps_support/engine.rb +11 -0
- data/lib/zendesk_apps_support/finders.rb +36 -0
- data/lib/zendesk_apps_support/i18n.rb +31 -0
- data/lib/zendesk_apps_support/installation.rb +22 -0
- data/lib/zendesk_apps_support/installed.rb +27 -0
- data/lib/zendesk_apps_support/location.rb +71 -0
- data/lib/zendesk_apps_support/manifest.rb +197 -0
- data/lib/zendesk_apps_support/manifest/location_options.rb +35 -0
- data/lib/zendesk_apps_support/manifest/no_override_hash.rb +50 -0
- data/lib/zendesk_apps_support/manifest/parameter.rb +23 -0
- data/lib/zendesk_apps_support/package.rb +402 -0
- data/lib/zendesk_apps_support/product.rb +31 -0
- data/lib/zendesk_apps_support/sass_functions.rb +43 -0
- data/lib/zendesk_apps_support/stylesheet_compiler.rb +38 -0
- data/lib/zendesk_apps_support/validations/banner.rb +35 -0
- data/lib/zendesk_apps_support/validations/manifest.rb +412 -0
- data/lib/zendesk_apps_support/validations/marketplace.rb +31 -0
- data/lib/zendesk_apps_support/validations/mime.rb +41 -0
- data/lib/zendesk_apps_support/validations/requests.rb +47 -0
- data/lib/zendesk_apps_support/validations/requirements.rb +154 -0
- data/lib/zendesk_apps_support/validations/secrets.rb +77 -0
- data/lib/zendesk_apps_support/validations/secure_settings.rb +37 -0
- data/lib/zendesk_apps_support/validations/source.rb +25 -0
- data/lib/zendesk_apps_support/validations/stylesheets.rb +28 -0
- data/lib/zendesk_apps_support/validations/svg.rb +81 -0
- data/lib/zendesk_apps_support/validations/templates.rb +20 -0
- data/lib/zendesk_apps_support/validations/translations.rb +160 -0
- data/lib/zendesk_apps_support/validations/validation_error.rb +77 -0
- metadata +327 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ZendeskAppsSupport
|
4
|
+
class Location
|
5
|
+
extend ZendeskAppsSupport::Finders
|
6
|
+
attr_reader :id, :name, :orderable, :collapsible, :visible, :product_code, :v2_only
|
7
|
+
|
8
|
+
def self.unique_ids
|
9
|
+
@ids ||= Set.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(attrs)
|
13
|
+
@id = attrs.fetch(:id)
|
14
|
+
raise 'Duplicate id' if Location.unique_ids.include? @id
|
15
|
+
Location.unique_ids.add @id
|
16
|
+
@name = attrs.fetch(:name)
|
17
|
+
@orderable = attrs.fetch(:orderable, false)
|
18
|
+
@collapsible = attrs.fetch(:collapsible, false)
|
19
|
+
@visible = attrs.fetch(:visible, false)
|
20
|
+
@product_code = attrs.fetch(:product_code)
|
21
|
+
@v2_only = attrs.fetch(:v2_only, product != Product::SUPPORT)
|
22
|
+
end
|
23
|
+
|
24
|
+
def product
|
25
|
+
Product.find_by(code: product_code)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.all
|
29
|
+
LOCATIONS_AVAILABLE
|
30
|
+
end
|
31
|
+
|
32
|
+
# the ids below match the enum values on the database, do not change them!
|
33
|
+
LOCATIONS_AVAILABLE = [
|
34
|
+
Location.new(id: 1, orderable: true, name: 'top_bar',
|
35
|
+
product_code: Product::SUPPORT.code, visible: true),
|
36
|
+
Location.new(id: 2, orderable: true, name: 'nav_bar',
|
37
|
+
product_code: Product::SUPPORT.code, visible: true),
|
38
|
+
Location.new(id: 3, orderable: true, collapsible: true, name: 'ticket_sidebar',
|
39
|
+
product_code: Product::SUPPORT.code, visible: true),
|
40
|
+
Location.new(id: 4, orderable: true, collapsible: true, name: 'new_ticket_sidebar',
|
41
|
+
product_code: Product::SUPPORT.code, visible: true),
|
42
|
+
Location.new(id: 5, orderable: true, collapsible: true, name: 'user_sidebar',
|
43
|
+
product_code: Product::SUPPORT.code, visible: true),
|
44
|
+
Location.new(id: 6, orderable: true, collapsible: true, name: 'organization_sidebar',
|
45
|
+
product_code: Product::SUPPORT.code, visible: true),
|
46
|
+
Location.new(id: 7, name: 'background', product_code: Product::SUPPORT.code),
|
47
|
+
Location.new(id: 8, orderable: true, collapsible: true, name: 'chat_sidebar',
|
48
|
+
product_code: Product::CHAT.code, visible: true),
|
49
|
+
Location.new(id: 9, name: 'modal', product_code: Product::SUPPORT.code, v2_only: true),
|
50
|
+
Location.new(id: 10, name: 'ticket_editor', product_code: Product::SUPPORT.code, v2_only: true, visible: true),
|
51
|
+
Location.new(id: 11, name: 'nav_bar', product_code: Product::STANDALONE_CHAT.code, v2_only: false, visible: true),
|
52
|
+
Location.new(id: 12, name: 'system_top_bar', product_code: Product::SUPPORT.code),
|
53
|
+
Location.new(id: 13, name: 'system_top_bar',
|
54
|
+
product_code: Product::STANDALONE_CHAT.code, v2_only: false),
|
55
|
+
Location.new(id: 14, name: 'background',
|
56
|
+
product_code: Product::CHAT.code),
|
57
|
+
Location.new(id: 15, name: 'deal_card', product_code: Product::SELL.code, collapsible: true, visible: true),
|
58
|
+
Location.new(id: 16, name: 'person_card', product_code: Product::SELL.code, collapsible: true, visible: true),
|
59
|
+
Location.new(id: 17, name: 'company_card', product_code: Product::SELL.code, collapsible: true, visible: true),
|
60
|
+
Location.new(id: 18, name: 'lead_card', product_code: Product::SELL.code, collapsible: true, visible: true),
|
61
|
+
Location.new(id: 19, name: 'background', product_code: Product::SELL.code),
|
62
|
+
Location.new(id: 20, name: 'modal', product_code: Product::SELL.code),
|
63
|
+
Location.new(id: 21, name: 'dashboard', product_code: Product::SELL.code, visible: true),
|
64
|
+
Location.new(id: 22, name: 'note_editor', product_code: Product::SELL.code, visible: true),
|
65
|
+
Location.new(id: 23, name: 'call_log_editor', product_code: Product::SELL.code, visible: true),
|
66
|
+
Location.new(id: 24, name: 'email_editor', product_code: Product::SELL.code, visible: true),
|
67
|
+
Location.new(id: 25, name: 'top_bar', product_code: Product::SELL.code, visible: true),
|
68
|
+
Location.new(id: 26, name: 'visit_editor', product_code: Product::SELL.code, visible: true)
|
69
|
+
].freeze
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ZendeskAppsSupport
|
4
|
+
class Manifest
|
5
|
+
LEGACY_URI_STUB = '_legacy'
|
6
|
+
|
7
|
+
RUBY_TO_JSON = {
|
8
|
+
requirements_only: 'requirementsOnly',
|
9
|
+
marketing_only: 'marketingOnly',
|
10
|
+
version: 'version',
|
11
|
+
author: 'author',
|
12
|
+
name: 'name', # currently only used in ZAT
|
13
|
+
experiments: 'experiments',
|
14
|
+
framework_version: 'frameworkVersion',
|
15
|
+
single_install: 'singleInstall',
|
16
|
+
signed_urls: 'signedUrls',
|
17
|
+
no_template: 'noTemplate',
|
18
|
+
default_locale: 'defaultLocale',
|
19
|
+
original_locations: 'location',
|
20
|
+
private: 'private',
|
21
|
+
oauth: 'oauth',
|
22
|
+
original_parameters: 'parameters',
|
23
|
+
domain_whitelist: 'domainWhitelist',
|
24
|
+
remote_installation_url: 'remoteInstallationURL',
|
25
|
+
terms_conditions_url: 'termsConditionsURL',
|
26
|
+
google_analytics_code: 'gaID'
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
attr_reader(*RUBY_TO_JSON.keys)
|
30
|
+
|
31
|
+
alias_method :requirements_only?, :requirements_only
|
32
|
+
alias_method :marketing_only?, :marketing_only
|
33
|
+
alias_method :signed_urls?, :signed_urls
|
34
|
+
alias_method :single_install?, :single_install
|
35
|
+
alias_method :private?, :private
|
36
|
+
|
37
|
+
def no_template?
|
38
|
+
if no_template.is_a?(Array)
|
39
|
+
false
|
40
|
+
else
|
41
|
+
no_template
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def no_template_locations
|
46
|
+
no_template || []
|
47
|
+
end
|
48
|
+
|
49
|
+
def products
|
50
|
+
@products ||=
|
51
|
+
if requirements_only?
|
52
|
+
[ Product::SUPPORT ]
|
53
|
+
elsif marketing_only?
|
54
|
+
products_ignore_locations || [ Product::SUPPORT ]
|
55
|
+
else
|
56
|
+
products_from_locations
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def products_ignore_locations
|
61
|
+
locations.keys.map do |product_name|
|
62
|
+
Product.find_by(name: product_name)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def location_options
|
67
|
+
@location_options ||= locations.flat_map do |product_key, locations|
|
68
|
+
product = Product.find_by(name: product_key)
|
69
|
+
locations.map do |location_key, location_options|
|
70
|
+
location = product && Location.find_by(product_code: product.code, name: location_key)
|
71
|
+
options_with_defaults = {
|
72
|
+
'signed' => signed_urls?
|
73
|
+
}.merge(location_options)
|
74
|
+
Manifest::LocationOptions.new(location, options_with_defaults)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def app_class_properties
|
80
|
+
{
|
81
|
+
experiments: experiments,
|
82
|
+
location: locations,
|
83
|
+
noTemplate: no_template_locations,
|
84
|
+
singleInstall: single_install?,
|
85
|
+
signedUrls: signed_urls?
|
86
|
+
}.reject { |_k, v| v.nil? }
|
87
|
+
end
|
88
|
+
|
89
|
+
def unknown_locations(host)
|
90
|
+
product = Product.find_by(name: host)
|
91
|
+
|
92
|
+
if locations.key?(host)
|
93
|
+
product_locations = Location.where(product_code: product.code)
|
94
|
+
locations[host].keys.uniq - product_locations.map(&:name)
|
95
|
+
else
|
96
|
+
[]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def unknown_hosts
|
101
|
+
@unknown_hosts ||=
|
102
|
+
@used_hosts - Product::PRODUCTS_AVAILABLE.flat_map { |p| [p.name, p.legacy_name] }
|
103
|
+
end
|
104
|
+
|
105
|
+
def iframe_only?
|
106
|
+
framework_version && Gem::Version.new(framework_version) >= Gem::Version.new('2')
|
107
|
+
end
|
108
|
+
|
109
|
+
def parameters
|
110
|
+
@parameters ||= begin
|
111
|
+
parameter_array = @original_parameters.is_a?(Array) ? @original_parameters : []
|
112
|
+
parameter_array.map do |parameter_hash|
|
113
|
+
Parameter.new(parameter_hash)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def enabled_experiments
|
119
|
+
experiments.select { |_k, v| v }.keys
|
120
|
+
end
|
121
|
+
|
122
|
+
def initialize(manifest_text)
|
123
|
+
m = parse_json(manifest_text)
|
124
|
+
RUBY_TO_JSON.each do |ruby, json|
|
125
|
+
instance_variable_set(:"@#{ruby}", m[json])
|
126
|
+
end
|
127
|
+
@requirements_only ||= false
|
128
|
+
@marketing_only ||= false
|
129
|
+
@single_install ||= false
|
130
|
+
@private = m.fetch('private', true)
|
131
|
+
@signed_urls ||= false
|
132
|
+
@no_template ||= false
|
133
|
+
@experiments ||= {}
|
134
|
+
set_locations_and_hosts
|
135
|
+
end
|
136
|
+
|
137
|
+
LEGACY_LOCATION_OBJECT = { 'url' => LEGACY_URI_STUB }.freeze
|
138
|
+
private_constant :LEGACY_LOCATION_OBJECT
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
attr_reader :locations
|
143
|
+
|
144
|
+
def products_from_locations
|
145
|
+
location_options.map { |lo| lo.location && lo.location.product_code }
|
146
|
+
.compact
|
147
|
+
.uniq
|
148
|
+
.map { |code| Product.find_by(code: code) }
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_locations_and_hosts
|
152
|
+
@locations =
|
153
|
+
case original_locations
|
154
|
+
when Hash
|
155
|
+
@used_hosts = original_locations.keys
|
156
|
+
replace_legacy_locations original_locations
|
157
|
+
when Array
|
158
|
+
@used_hosts = ['support']
|
159
|
+
new_locations = NoOverrideHash[original_locations.map { |location| [ location, LEGACY_LOCATION_OBJECT ] }]
|
160
|
+
{ 'support' => new_locations }
|
161
|
+
when String
|
162
|
+
@used_hosts = ['support']
|
163
|
+
{ 'support' => { original_locations => LEGACY_LOCATION_OBJECT } }
|
164
|
+
# TODO: error out for numbers and Booleans
|
165
|
+
else # NilClass
|
166
|
+
@used_hosts = ['support']
|
167
|
+
{ 'support' => {} }
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def replace_legacy_locations(original_locations)
|
172
|
+
NoOverrideHash.new.tap do |new_locations_obj|
|
173
|
+
Product::PRODUCTS_AVAILABLE.each do |product|
|
174
|
+
product_key = product.name.to_s
|
175
|
+
legacy_key = product.legacy_name.to_s
|
176
|
+
value_for_product = original_locations.fetch(product_key, original_locations[legacy_key])
|
177
|
+
value_for_product && new_locations_obj[product_key] = replace_string_uris(value_for_product)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def replace_string_uris(product_locations)
|
183
|
+
product_locations.each_with_object({}) do |(k, v), new_locations|
|
184
|
+
new_locations[k] = if v.is_a? Hash
|
185
|
+
v
|
186
|
+
else
|
187
|
+
{ 'url' => v }
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def parse_json(manifest_text)
|
193
|
+
parser_opts = { object_class: Manifest::NoOverrideHash }
|
194
|
+
JSON.parse(manifest_text, parser_opts)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ZendeskAppsSupport
|
4
|
+
class Manifest
|
5
|
+
class LocationOptions
|
6
|
+
RUBY_TO_JSON = {
|
7
|
+
legacy: 'legacy',
|
8
|
+
auto_load: 'autoLoad',
|
9
|
+
auto_hide: 'autoHide',
|
10
|
+
signed: 'signed',
|
11
|
+
url: 'url'
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
attr_reader :location
|
15
|
+
attr_reader(*RUBY_TO_JSON.keys)
|
16
|
+
|
17
|
+
alias_method :signed?, :signed
|
18
|
+
alias_method :legacy?, :legacy
|
19
|
+
alias_method :auto_load?, :auto_load
|
20
|
+
alias_method :auto_hide?, :auto_hide
|
21
|
+
|
22
|
+
def initialize(location, options)
|
23
|
+
@location = location
|
24
|
+
|
25
|
+
RUBY_TO_JSON.each do |ruby, json|
|
26
|
+
instance_variable_set(:"@#{ruby}", options[json])
|
27
|
+
end
|
28
|
+
@legacy ||= @url == ZendeskAppsSupport::Manifest::LEGACY_URI_STUB
|
29
|
+
@auto_load = options.fetch('autoLoad', true)
|
30
|
+
@auto_hide ||= false
|
31
|
+
@signed ||= false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ZendeskAppsSupport
|
4
|
+
class Manifest
|
5
|
+
class OverrideError < StandardError
|
6
|
+
attr_reader :key, :original, :attempted
|
7
|
+
attr_accessor :message
|
8
|
+
def initialize(key, original, attempted)
|
9
|
+
@key = key
|
10
|
+
@original = original
|
11
|
+
@attempted = attempted
|
12
|
+
end
|
13
|
+
|
14
|
+
def message
|
15
|
+
@message ||= begin
|
16
|
+
translated_error_key = 'txt.apps.admin.error.app_build.duplicate_reference'
|
17
|
+
translated_error = ZendeskAppsSupport::I18n.t(translated_error_key, key: key)
|
18
|
+
|
19
|
+
# if the error contains the word `_legacy` in the second sentence, let's
|
20
|
+
# only use the first one.
|
21
|
+
if [original, attempted].any? { |val| val =~ /_legacy/ }
|
22
|
+
return translated_error
|
23
|
+
end
|
24
|
+
translated_detail_key = 'txt.apps.admin.error.app_build.duplicate_reference_values'
|
25
|
+
translated_detail = ZendeskAppsSupport::I18n.t(translated_detail_key,
|
26
|
+
original: original,
|
27
|
+
attempted: attempted)
|
28
|
+
"#{translated_error} #{translated_detail}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class NoOverrideHash < Hash
|
34
|
+
class << self
|
35
|
+
def [](array)
|
36
|
+
new.tap do |hash|
|
37
|
+
array.each do |key, value|
|
38
|
+
hash[key] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def []=(key, value)
|
45
|
+
raise OverrideError.new(key, self[key], value) if key? key
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ZendeskAppsSupport
|
4
|
+
class Manifest
|
5
|
+
class Parameter
|
6
|
+
TYPES = %w[text password checkbox url number multiline hidden oauth].freeze
|
7
|
+
ATTRIBUTES = %i[name type required secure default].freeze
|
8
|
+
attr_reader(*ATTRIBUTES)
|
9
|
+
def default?
|
10
|
+
@has_default
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(p)
|
14
|
+
@name = p['name']
|
15
|
+
@type = p['type'] || 'text'
|
16
|
+
@required = p['required'] || false
|
17
|
+
@secure = p['secure'] || false
|
18
|
+
@has_default = p.key? 'default'
|
19
|
+
@default = p['default'] if @has_default
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,402 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'erubis'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module ZendeskAppsSupport
|
8
|
+
class Package
|
9
|
+
extend Gem::Deprecate
|
10
|
+
include ZendeskAppsSupport::BuildTranslation
|
11
|
+
|
12
|
+
MANIFEST_FILENAME = 'manifest.json'
|
13
|
+
REQUIREMENTS_FILENAME = 'requirements.json'
|
14
|
+
|
15
|
+
DEFAULT_LAYOUT = Erubis::Eruby.new(File.read(File.expand_path('../assets/default_template.html.erb', __FILE__)))
|
16
|
+
DEFAULT_SCSS = File.read(File.expand_path('../assets/default_styles.scss', __FILE__))
|
17
|
+
SRC_TEMPLATE = Erubis::Eruby.new(File.read(File.expand_path('../assets/src.js.erb', __FILE__)))
|
18
|
+
|
19
|
+
LOCATIONS_WITH_ICONS_PER_PRODUCT = {
|
20
|
+
Product::SUPPORT => %w[top_bar nav_bar system_top_bar ticket_editor].freeze,
|
21
|
+
Product::SELL => %w[top_bar].freeze
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
attr_reader :lib_root, :root, :warnings
|
25
|
+
|
26
|
+
def initialize(dir, is_cached = true)
|
27
|
+
@root = Pathname.new(File.expand_path(dir))
|
28
|
+
@lib_root = Pathname.new(File.join(root, 'lib'))
|
29
|
+
|
30
|
+
@is_cached = is_cached # disabled by ZAT for development
|
31
|
+
@warnings = []
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate(marketplace: true, skip_marketplace_translations: false)
|
35
|
+
errors = []
|
36
|
+
errors << Validations::Manifest.call(self)
|
37
|
+
|
38
|
+
if has_valid_manifest?(errors)
|
39
|
+
errors << Validations::Marketplace.call(self) if marketplace
|
40
|
+
errors << Validations::Source.call(self)
|
41
|
+
errors << Validations::Translations.call(self, skip_marketplace_translations: skip_marketplace_translations)
|
42
|
+
errors << Validations::Requirements.call(self)
|
43
|
+
|
44
|
+
# only adds warnings
|
45
|
+
Validations::SecureSettings.call(self)
|
46
|
+
Validations::Requests.call(self)
|
47
|
+
|
48
|
+
unless manifest.requirements_only? || manifest.marketing_only? || manifest.iframe_only?
|
49
|
+
errors << Validations::Templates.call(self)
|
50
|
+
errors << Validations::Stylesheets.call(self)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
errors << Validations::Banner.call(self) if has_banner?
|
55
|
+
errors << Validations::Svg.call(self) if has_svgs?
|
56
|
+
errors << Validations::Mime.call(self)
|
57
|
+
|
58
|
+
# only adds warnings
|
59
|
+
Validations::Secrets.call(self)
|
60
|
+
|
61
|
+
errors.flatten.compact
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate!(marketplace: true, skip_marketplace_translations: false)
|
65
|
+
errors = validate(marketplace: marketplace, skip_marketplace_translations: skip_marketplace_translations)
|
66
|
+
raise errors.first if errors.any?
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def assets
|
71
|
+
@assets ||= Dir.chdir(root) do
|
72
|
+
Dir['assets/**/*'].select { |f| File.file?(f) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def path_to(file)
|
77
|
+
File.join(root, file)
|
78
|
+
end
|
79
|
+
|
80
|
+
def requirements_path
|
81
|
+
path_to(REQUIREMENTS_FILENAME)
|
82
|
+
end
|
83
|
+
|
84
|
+
def locales
|
85
|
+
translations.keys
|
86
|
+
end
|
87
|
+
|
88
|
+
def files
|
89
|
+
files = []
|
90
|
+
Dir[root.join('**/**')].each do |f|
|
91
|
+
next unless File.file?(f)
|
92
|
+
relative_file_name = f.sub(%r{#{root}/?}, '')
|
93
|
+
next if relative_file_name =~ %r{^tmp/}
|
94
|
+
files << AppFile.new(self, relative_file_name)
|
95
|
+
end
|
96
|
+
files
|
97
|
+
end
|
98
|
+
|
99
|
+
def text_files
|
100
|
+
@text_files ||= files.select { |f| f =~ %r{.*(html?|xml|js|json?)$} }
|
101
|
+
end
|
102
|
+
|
103
|
+
def js_files
|
104
|
+
@js_files ||= files.select { |f| f =~ %r{^.*\.jsx?$} }
|
105
|
+
end
|
106
|
+
|
107
|
+
def html_files
|
108
|
+
@html_files ||= files.select { |f| f =~ %r{.*\.html?$} }
|
109
|
+
end
|
110
|
+
|
111
|
+
def lib_files
|
112
|
+
@lib_files ||= js_files.select { |f| f =~ %r{^lib/} }
|
113
|
+
end
|
114
|
+
|
115
|
+
def svg_files
|
116
|
+
@svg_files ||= files.select { |f| f =~ %r{^assets/.*\.svg$} }
|
117
|
+
end
|
118
|
+
|
119
|
+
def template_files
|
120
|
+
files.select { |f| f =~ %r{^templates/.*\.hdbs$} }
|
121
|
+
end
|
122
|
+
|
123
|
+
def translation_files
|
124
|
+
files.select { |f| f =~ %r{^translations/} }
|
125
|
+
end
|
126
|
+
|
127
|
+
# this is not really compile_js, it compiles the whole app including scss for v1 apps
|
128
|
+
def compile(options)
|
129
|
+
begin
|
130
|
+
app_id = options.fetch(:app_id)
|
131
|
+
asset_url_prefix = options.fetch(:assets_dir)
|
132
|
+
name = options.fetch(:app_name)
|
133
|
+
rescue KeyError => e
|
134
|
+
raise ArgumentError, e.message
|
135
|
+
end
|
136
|
+
|
137
|
+
locale = options.fetch(:locale, 'en')
|
138
|
+
|
139
|
+
source = manifest.iframe_only? ? nil : app_js
|
140
|
+
app_class_name = "app-#{app_id}"
|
141
|
+
# if no_template is an array, we still need the templates
|
142
|
+
templates = manifest.no_template == true ? {} : compiled_templates(app_id, asset_url_prefix)
|
143
|
+
|
144
|
+
SRC_TEMPLATE.result(
|
145
|
+
name: name,
|
146
|
+
version: manifest.version,
|
147
|
+
source: source,
|
148
|
+
app_class_properties: manifest.app_class_properties,
|
149
|
+
asset_url_prefix: asset_url_prefix,
|
150
|
+
logo_asset_hash: generate_logo_hash(manifest.products),
|
151
|
+
location_icons: location_icons,
|
152
|
+
app_class_name: app_class_name,
|
153
|
+
author: manifest.author,
|
154
|
+
translations: manifest.iframe_only? ? nil : runtime_translations(translations_for(locale)),
|
155
|
+
framework_version: manifest.framework_version,
|
156
|
+
templates: templates,
|
157
|
+
modules: commonjs_modules,
|
158
|
+
iframe_only: manifest.iframe_only?
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
alias compile_js compile
|
163
|
+
deprecate :compile_js, :compile, 2017, 1
|
164
|
+
|
165
|
+
def manifest_json
|
166
|
+
@manifest_json ||= read_json(MANIFEST_FILENAME)
|
167
|
+
end
|
168
|
+
deprecate :manifest_json, :manifest, 2016, 9
|
169
|
+
|
170
|
+
def manifest
|
171
|
+
@manifest ||= Manifest.new(read_file(MANIFEST_FILENAME))
|
172
|
+
end
|
173
|
+
|
174
|
+
def requirements_json
|
175
|
+
return nil unless has_requirements?
|
176
|
+
@requirements ||= read_json(REQUIREMENTS_FILENAME, object_class: Manifest::NoOverrideHash)
|
177
|
+
end
|
178
|
+
|
179
|
+
def is_no_template
|
180
|
+
manifest.no_template?
|
181
|
+
end
|
182
|
+
deprecate :is_no_template, 'manifest.no_template?', 2016, 9
|
183
|
+
|
184
|
+
def no_template_locations
|
185
|
+
manifest.no_template_locations
|
186
|
+
end
|
187
|
+
deprecate :no_template_locations, 'manifest.no_template_locations', 2016, 9
|
188
|
+
|
189
|
+
def compiled_templates(app_id, asset_url_prefix)
|
190
|
+
compiler = ZendeskAppsSupport::StylesheetCompiler.new(DEFAULT_SCSS + app_css, app_id, asset_url_prefix)
|
191
|
+
compiled_css = compiler.compile(sassc: manifest.enabled_experiments.include?('newCssCompiler'))
|
192
|
+
|
193
|
+
layout = templates['layout'] || DEFAULT_LAYOUT.result
|
194
|
+
|
195
|
+
templates.tap do |templates|
|
196
|
+
templates['layout'] = "<style>\n#{compiled_css}</style>\n#{layout}"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def translations_for(locale)
|
201
|
+
trans = translations
|
202
|
+
return trans[locale] if trans[locale]
|
203
|
+
trans[manifest.default_locale]
|
204
|
+
end
|
205
|
+
|
206
|
+
def has_file?(path)
|
207
|
+
File.file?(path_to(path))
|
208
|
+
end
|
209
|
+
|
210
|
+
def has_svgs?
|
211
|
+
svg_files.any?
|
212
|
+
end
|
213
|
+
|
214
|
+
def has_requirements?
|
215
|
+
has_file?(REQUIREMENTS_FILENAME)
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.has_custom_object_requirements?(requirements_hash)
|
219
|
+
return false if requirements_hash.nil?
|
220
|
+
|
221
|
+
custom_object_requirements = requirements_hash.fetch(AppRequirement::CUSTOM_OBJECTS_KEY, {})
|
222
|
+
types = custom_object_requirements.fetch(AppRequirement::CUSTOM_OBJECTS_TYPE_KEY, [])
|
223
|
+
relationships = custom_object_requirements.fetch(AppRequirement::CUSTOM_OBJECTS_RELATIONSHIP_TYPE_KEY, [])
|
224
|
+
|
225
|
+
(types | relationships).any?
|
226
|
+
end
|
227
|
+
|
228
|
+
def app_css
|
229
|
+
return File.read(path_to('app.scss')) if has_file?('app.scss')
|
230
|
+
return File.read(path_to('app.css')) if has_file?('app.css')
|
231
|
+
''
|
232
|
+
end
|
233
|
+
|
234
|
+
def app_js
|
235
|
+
if @is_cached
|
236
|
+
@app_js ||= read_file('app.js')
|
237
|
+
else
|
238
|
+
read_file('app.js')
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def iframe_only?
|
243
|
+
manifest.iframe_only?
|
244
|
+
end
|
245
|
+
deprecate :iframe_only?, 'manifest.iframe_only?', 2016, 9
|
246
|
+
|
247
|
+
def templates
|
248
|
+
templates_dir = path_to('templates')
|
249
|
+
Dir["#{templates_dir}/*.hdbs"].each_with_object({}) do |file, memo|
|
250
|
+
str = File.read(file)
|
251
|
+
str.chomp!
|
252
|
+
memo[File.basename(file, File.extname(file))] = str
|
253
|
+
memo
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def translations
|
258
|
+
return @translations if @is_cached && @translations
|
259
|
+
|
260
|
+
@translations = begin
|
261
|
+
translation_dir = path_to('translations')
|
262
|
+
return {} unless File.directory?(translation_dir)
|
263
|
+
|
264
|
+
locale_path = "#{translation_dir}/#{manifest.default_locale}.json"
|
265
|
+
default_translations = process_translations(locale_path, default_locale: true)
|
266
|
+
|
267
|
+
Dir["#{translation_dir}/*.json"].each_with_object({}) do |path, memo|
|
268
|
+
locale = File.basename(path, File.extname(path))
|
269
|
+
|
270
|
+
locale_translations = if locale == manifest.default_locale
|
271
|
+
default_translations
|
272
|
+
else
|
273
|
+
deep_merge_hash(default_translations, process_translations(path))
|
274
|
+
end
|
275
|
+
|
276
|
+
memo[locale] = locale_translations
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def location_icons
|
282
|
+
Hash.new { |h, k| h[k] = {} }.tap do |location_icons|
|
283
|
+
manifest.location_options.each do |location_options|
|
284
|
+
# no location information in the manifest
|
285
|
+
next unless location_options.location
|
286
|
+
|
287
|
+
product = location_options.location.product
|
288
|
+
location_name = location_options.location.name
|
289
|
+
# the location on the product does not support icons
|
290
|
+
next unless LOCATIONS_WITH_ICONS_PER_PRODUCT.fetch(product, []).include?(location_name)
|
291
|
+
|
292
|
+
host = location_options.location.product.name
|
293
|
+
product_directory = manifest.products.count > 1 ? "#{host}/" : ''
|
294
|
+
location_icons[host][location_name] = build_location_icons_hash(location_name, product_directory)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
private
|
300
|
+
|
301
|
+
def generate_logo_hash(products)
|
302
|
+
{}.tap do |logo_hash|
|
303
|
+
products.each do |product|
|
304
|
+
product_directory = products.count > 1 ? "#{product.name.downcase}/" : ''
|
305
|
+
logo_hash[product.name.downcase] = "#{product_directory}logo-small.png"
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def has_valid_manifest?(errors)
|
311
|
+
has_manifest? && errors.flatten.empty?
|
312
|
+
end
|
313
|
+
|
314
|
+
def runtime_translations(translations)
|
315
|
+
result = translations.dup
|
316
|
+
result.delete('name')
|
317
|
+
result.delete('short_description')
|
318
|
+
result.delete('long_description')
|
319
|
+
result.delete('installation_instructions')
|
320
|
+
result
|
321
|
+
end
|
322
|
+
|
323
|
+
def process_translations(locale_path, default_locale: false)
|
324
|
+
translations = File.exist?(locale_path) ? JSON.parse(File.read(locale_path)) : {}
|
325
|
+
translations['app'].delete('name') if !default_locale && translations.key?('app')
|
326
|
+
translations['app'].delete('package') if translations.key?('app')
|
327
|
+
remove_zendesk_keys(translations)
|
328
|
+
end
|
329
|
+
|
330
|
+
def has_lib_js?
|
331
|
+
lib_files.any?
|
332
|
+
end
|
333
|
+
|
334
|
+
def has_manifest?
|
335
|
+
has_file?(MANIFEST_FILENAME)
|
336
|
+
end
|
337
|
+
|
338
|
+
def has_banner?
|
339
|
+
has_file?('assets/banner.png')
|
340
|
+
end
|
341
|
+
|
342
|
+
def build_location_icons_hash(location, product_directory)
|
343
|
+
inactive_png = "icon_#{location}_inactive.png"
|
344
|
+
if has_file?("assets/#{product_directory}icon_#{location}.svg")
|
345
|
+
build_svg_icon_hash(location, product_directory)
|
346
|
+
elsif has_file?("assets/#{product_directory}#{inactive_png}")
|
347
|
+
build_png_icons_hash(location, product_directory)
|
348
|
+
else
|
349
|
+
{}
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def build_svg_icon_hash(location, product_directory)
|
354
|
+
cache_busting_param = "?#{Time.now.to_i}" unless @is_cached
|
355
|
+
{ 'svg' => "#{product_directory}icon_#{location}.svg#{cache_busting_param}" }
|
356
|
+
end
|
357
|
+
|
358
|
+
def build_png_icons_hash(location, product_directory)
|
359
|
+
inactive_png = "#{product_directory}icon_#{location}_inactive.png"
|
360
|
+
{
|
361
|
+
'inactive' => inactive_png
|
362
|
+
}.tap do |icon_state_hash|
|
363
|
+
%w[active hover].each do |state|
|
364
|
+
specific_png = "#{product_directory}icon_#{location}_#{state}.png"
|
365
|
+
selected_png = has_file?("assets/#{specific_png}") ? specific_png : inactive_png
|
366
|
+
icon_state_hash[state] = selected_png
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
def commonjs_modules
|
372
|
+
return {} unless has_lib_js?
|
373
|
+
|
374
|
+
lib_files.each_with_object({}) do |file, modules|
|
375
|
+
name = file.relative_path.gsub(/^lib\//, '')
|
376
|
+
content = file.read
|
377
|
+
modules[name] = content
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def deep_merge_hash(h, another_h)
|
382
|
+
result_h = h.dup
|
383
|
+
another_h.each do |key, value|
|
384
|
+
result_h[key] = if h.key?(key) && h[key].is_a?(Hash) && value.is_a?(Hash)
|
385
|
+
deep_merge_hash(h[key], value)
|
386
|
+
else
|
387
|
+
value
|
388
|
+
end
|
389
|
+
end
|
390
|
+
result_h
|
391
|
+
end
|
392
|
+
|
393
|
+
def read_file(path)
|
394
|
+
File.read(path_to(path))
|
395
|
+
end
|
396
|
+
|
397
|
+
def read_json(path, parser_opts = {})
|
398
|
+
file = read_file(path)
|
399
|
+
JSON.parse(read_file(path), parser_opts) unless file.nil?
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|