zendesk_apps_support 4.29.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|