xat 1.32.0

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +176 -0
  3. data/README.md +38 -0
  4. data/app_template/README.md +14 -0
  5. data/app_template/app.css +0 -0
  6. data/app_template/app.js +12 -0
  7. data/app_template/assets/banner.png +0 -0
  8. data/app_template/assets/logo-promotion.png +0 -0
  9. data/app_template/assets/logo-small.png +0 -0
  10. data/app_template/assets/logo.png +0 -0
  11. data/app_template/manifest.json.tt +13 -0
  12. data/app_template/templates/layout.hdbs +10 -0
  13. data/app_template/translations/en.json +16 -0
  14. data/app_template_iframe/README.md +14 -0
  15. data/app_template_iframe/assets/banner.png +0 -0
  16. data/app_template_iframe/assets/iframe.html +13 -0
  17. data/app_template_iframe/assets/logo-promotion.png +0 -0
  18. data/app_template_iframe/assets/logo-small.png +0 -0
  19. data/app_template_iframe/assets/logo.png +0 -0
  20. data/app_template_iframe/manifest.json.tt +15 -0
  21. data/app_template_iframe/translations/en.json +16 -0
  22. data/bin/xat +5 -0
  23. data/features/clean.feature +9 -0
  24. data/features/fixtures/quote_character_translation.json +6 -0
  25. data/features/new.feature +117 -0
  26. data/features/package.feature +22 -0
  27. data/features/step_definitions/app_steps.rb +103 -0
  28. data/features/support/env.rb +5 -0
  29. data/features/validate.feature +15 -0
  30. data/lib/xat.rb +5 -0
  31. data/lib/zendesk_apps_tools/api_connection.rb +33 -0
  32. data/lib/zendesk_apps_tools/array_patch.rb +22 -0
  33. data/lib/zendesk_apps_tools/bump.rb +60 -0
  34. data/lib/zendesk_apps_tools/cache.rb +25 -0
  35. data/lib/zendesk_apps_tools/command.rb +183 -0
  36. data/lib/zendesk_apps_tools/command_helpers.rb +20 -0
  37. data/lib/zendesk_apps_tools/common.rb +40 -0
  38. data/lib/zendesk_apps_tools/deploy.rb +94 -0
  39. data/lib/zendesk_apps_tools/directory.rb +28 -0
  40. data/lib/zendesk_apps_tools/locale_identifier.rb +10 -0
  41. data/lib/zendesk_apps_tools/manifest_handler.rb +72 -0
  42. data/lib/zendesk_apps_tools/package_helper.rb +30 -0
  43. data/lib/zendesk_apps_tools/server.rb +65 -0
  44. data/lib/zendesk_apps_tools/settings.rb +82 -0
  45. data/lib/zendesk_apps_tools/translate.rb +168 -0
  46. data/templates/translation.erb.tt +13 -0
  47. metadata +238 -0
@@ -0,0 +1,20 @@
1
+ require 'zendesk_apps_tools/cache'
2
+ require 'zendesk_apps_tools/common'
3
+ require 'zendesk_apps_tools/api_connection'
4
+ require 'zendesk_apps_tools/deploy'
5
+ require 'zendesk_apps_tools/directory'
6
+ require 'zendesk_apps_tools/package_helper'
7
+ require 'zendesk_apps_tools/settings'
8
+ require 'zendesk_apps_tools/translate'
9
+ require 'zendesk_apps_tools/bump'
10
+
11
+ module ZendeskAppsTools
12
+ module CommandHelpers
13
+ include ZendeskAppsTools::Cache
14
+ include ZendeskAppsTools::Common
15
+ include ZendeskAppsTools::APIConnection
16
+ include ZendeskAppsTools::Deploy
17
+ include ZendeskAppsTools::Directory
18
+ include ZendeskAppsTools::PackageHelper
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ require 'faraday'
2
+
3
+ module ZendeskAppsTools
4
+ module Common
5
+ def api_request(url, request = Faraday.new)
6
+ request.get(url)
7
+ end
8
+
9
+ def say_error_and_exit(msg)
10
+ say msg, :red
11
+ exit 1
12
+ end
13
+
14
+ def get_value_from_stdin(prompt, opts = {})
15
+ options = {
16
+ valid_regex: opts[:allow_empty] ? /^.*$/ : /\S+/,
17
+ error_msg: 'Invalid, try again:',
18
+ allow_empty: false
19
+ }.merge(opts)
20
+
21
+ while input = ask(prompt)
22
+ return '' if input.empty? && options[:allow_empty]
23
+ if input =~ options[:valid_regex]
24
+ break
25
+ else
26
+ say(options[:error_msg], :red)
27
+ end
28
+ end
29
+
30
+ input
31
+ end
32
+
33
+ def get_password_from_stdin(prompt)
34
+ print "#{prompt} "
35
+ password = STDIN.noecho(&:gets).chomp
36
+ puts
37
+ password
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,94 @@
1
+ module ZendeskAppsTools
2
+ module Deploy
3
+ def deploy_app(connection_method, url, body)
4
+ body[:upload_id] = upload(options[:path]).to_s
5
+
6
+ connection = get_connection
7
+
8
+ response = connection.send(connection_method) do |req|
9
+ req.url url
10
+ req.headers[:content_type] = 'application/json'
11
+ req.body = JSON.generate body
12
+ end
13
+
14
+ check_status response
15
+
16
+ rescue Faraday::Error::ClientError, JSON::ParserError => e
17
+ say_error_and_exit e.message
18
+ end
19
+
20
+ def upload(path)
21
+ connection = get_connection :multipart
22
+ zipfile_path = options[:zipfile]
23
+
24
+ if zipfile_path
25
+ package_path = zipfile_path
26
+ else
27
+ package
28
+ package_path = Dir[File.join path, '/tmp/*.zip'].sort.last
29
+ end
30
+
31
+ payload = { uploaded_data: Faraday::UploadIO.new(package_path, 'application/zip') }
32
+
33
+ response = connection.post('/api/v2/apps/uploads.json', payload)
34
+ JSON.parse(response.body)['id']
35
+
36
+ rescue Faraday::Error::ClientError, JSON::ParserError => e
37
+ say_error_and_exit e.message
38
+ end
39
+
40
+ def find_app_id
41
+ say_status 'Update', 'app ID is missing, searching...'
42
+ name = get_value_from_stdin('Enter the name of the app:')
43
+
44
+ connection = get_connection
45
+
46
+ all_apps = connection.get('/api/v2/apps.json').body
47
+
48
+ app_id = JSON.parse(all_apps)['apps'].find { |app| app['name'] == name }['id']
49
+
50
+ save_cache 'app_id' => app_id
51
+ app_id
52
+ rescue Faraday::Error::ClientError => e
53
+ say_error_and_exit e.message
54
+ end
55
+
56
+ def check_status(response)
57
+ job = response.body
58
+ job_response = JSON.parse(job)
59
+ say_error_and_exit job_response['error'] if job_response['error']
60
+
61
+ job_id = job_response['job_id']
62
+ check_job job_id
63
+ end
64
+
65
+ def check_job(job_id)
66
+ connection = get_connection
67
+
68
+ loop do
69
+ response = connection.get("/api/v2/apps/job_statuses/#{job_id}")
70
+ info = JSON.parse(response.body)
71
+ status = info['status']
72
+ message = info['message']
73
+ app_id = info['app_id']
74
+
75
+ if %w(completed failed).include? status
76
+ case status
77
+ when 'completed'
78
+ save_cache 'app_id' => app_id
79
+ say_status @command, 'OK'
80
+ when 'failed'
81
+ say_status @command, message, :red
82
+ exit 1
83
+ end
84
+ break
85
+ end
86
+
87
+ say_status 'Status', status
88
+ sleep 3
89
+ end
90
+ rescue Faraday::Error::ClientError => e
91
+ say_error_and_exit e.message
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,28 @@
1
+ module ZendeskAppsTools
2
+ module Directory
3
+ def app_dir
4
+ @app_dir ||= Pathname.new(destination_root)
5
+ end
6
+
7
+ def tmp_dir
8
+ @tmp_dir ||= Pathname.new(File.join(app_dir, 'tmp')).tap do |dir|
9
+ FileUtils.mkdir_p(dir)
10
+ end
11
+ end
12
+
13
+ def prompt_new_app_dir
14
+ prompt = "Enter a directory name to save the new app (will create the dir if it does not exist, default to current dir):\n"
15
+ opts = { valid_regex: /^(\w|\/|\\)*$/, allow_empty: true }
16
+ while @app_dir = get_value_from_stdin(prompt, opts)
17
+ @app_dir = './' and break if @app_dir.empty?
18
+ if !File.exist?(@app_dir)
19
+ break
20
+ elsif !File.directory?(@app_dir)
21
+ puts 'Invalid dir, try again:'
22
+ else
23
+ break
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ module ZendeskAppsTools
2
+ class LocaleIdentifier
3
+ attr_reader :locale_id
4
+
5
+ # Convert :"en-US-x-12" to 'en-US'
6
+ def initialize(code)
7
+ @locale_id = code.start_with?('en-US') ? 'en-US' : code.sub(/-x-.*/, '').downcase
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,72 @@
1
+ require 'zendesk_apps_tools/array_patch'
2
+
3
+ module ZendeskAppsTools
4
+ module ManifestHandler
5
+ VERSION_PARTS = %i(major minor patch)
6
+
7
+ attr_reader :semver
8
+
9
+ VERSION_PARTS.each do |m|
10
+ define_method m do
11
+ load_manifest
12
+ read_version
13
+ normalize_version
14
+ super()
15
+ update_version
16
+ write_manifest
17
+ post_actions
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def manifest_json_path
24
+ 'manifest.json'
25
+ end
26
+
27
+ def load_manifest
28
+ manifest_json = File.read(manifest_json_path)
29
+ @manifest = JSON.load(manifest_json)
30
+ rescue => e
31
+ say(e.message, :red) and exit 1
32
+ end
33
+
34
+ def read_version
35
+ version = @manifest.fetch('version', '0.0.0')
36
+ sem_parts = sub_semver(version)
37
+ @semver = sem_parts.names.map(&:to_sym).zip(sem_parts.to_a.drop(1)).to_h
38
+ end
39
+
40
+ def normalize_version
41
+ VERSION_PARTS.each do |part|
42
+ semver[part] = (semver[part] || '0').to_i
43
+ end
44
+ end
45
+
46
+ def update_version
47
+ @manifest['version'] = version
48
+ end
49
+
50
+ def write_manifest
51
+ File.open(manifest_json_path, 'w') do |f|
52
+ f.write(JSON.pretty_generate(@manifest))
53
+ f.write("\n")
54
+ end
55
+ end
56
+
57
+ def sub_semver(v)
58
+ v.match(/\A(?<v>v)?(?<major>\d+)(?:\.(?<minor>\d+)(?:\.(?<patch>\d+))?)?\Z/)
59
+ end
60
+
61
+ def version(v: false)
62
+ [
63
+ v ? 'v' : semver[:v],
64
+ [
65
+ semver[:major],
66
+ semver[:minor],
67
+ semver[:patch]
68
+ ].compact.map(&:to_s).join('.')
69
+ ].compact.join
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,30 @@
1
+ module ZendeskAppsTools
2
+ require 'zendesk_apps_support'
3
+
4
+ module PackageHelper
5
+ include ZendeskAppsSupport
6
+
7
+ def app_package
8
+ @app_package ||= Package.new(app_dir.to_s)
9
+ end
10
+
11
+ def zip(archive_path)
12
+ Zip::ZipFile.open(archive_path, 'w') do |zipfile|
13
+ app_package.files.each do |file|
14
+ relative_path = file.relative_path
15
+ path = relative_path
16
+ say_status 'package', "adding #{path}"
17
+
18
+ # resolve symlink to source path
19
+ if File.symlink? file.absolute_path
20
+ path = File.expand_path(File.readlink(file.absolute_path), File.dirname(file.absolute_path))
21
+ end
22
+ if file.to_s == 'app.scss'
23
+ relative_path = relative_path.sub 'app.scss', 'app.css'
24
+ end
25
+ zipfile.add(relative_path, app_dir.join(path).to_s)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,65 @@
1
+ require 'sinatra/base'
2
+ require 'zendesk_apps_support/package'
3
+
4
+ module ZendeskAppsTools
5
+ class Server < Sinatra::Base
6
+ set :protection, :except => :frame_options
7
+ last_mtime = Time.new(0)
8
+ ZENDESK_DOMAINS_REGEX = /^http(?:s)?:\/\/[a-z0-9-]+\.(?:zendesk|zopim|zd-(?:dev|master|staging))\.com$/
9
+
10
+ get '/app.js' do
11
+ access_control_allow_origin
12
+ content_type 'text/javascript'
13
+
14
+ if File.exists? settings.config
15
+ curr_mtime = File.stat(settings.config).mtime
16
+ if curr_mtime > last_mtime
17
+ settings_helper = ZendeskAppsTools::Settings.new
18
+ settings.parameters = settings_helper.get_settings_from_file(settings.config, settings.manifest)
19
+ last_mtime = curr_mtime
20
+ end
21
+ end
22
+
23
+ package = ZendeskAppsSupport::Package.new(settings.root, false)
24
+ app_name = package.manifest_json['name'] || 'Local App'
25
+ installation = ZendeskAppsSupport::Installation.new(
26
+ id: settings.app_id,
27
+ app_id: settings.app_id,
28
+ app_name: app_name,
29
+ enabled: true,
30
+ requirements: package.requirements_json,
31
+ settings: settings.parameters.merge({title: app_name}),
32
+ updated_at: Time.now.iso8601,
33
+ created_at: Time.now.iso8601
34
+ )
35
+
36
+ app_js = package.compile_js(
37
+ app_id: settings.app_id,
38
+ app_name: package.manifest_json['name'] || 'Local App',
39
+ assets_dir: "http://localhost:#{settings.port}/",
40
+ locale: params['locale']
41
+ )
42
+
43
+ ZendeskAppsSupport::Installed.new([app_js], [installation]).compile_js
44
+ end
45
+
46
+ get "/:file" do |file|
47
+ access_control_allow_origin
48
+ send_file File.join(settings.root, 'assets', file)
49
+ end
50
+
51
+ # This is for any preflight request
52
+ # It reads 'Access-Control-Request-Headers' to set 'Access-Control-Allow-Headers'
53
+ # And also sets 'Access-Control-Allow-Origin' header
54
+ options "*" do
55
+ access_control_allow_origin
56
+ headers 'Access-Control-Allow-Headers' => request.env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] if request.env['HTTP_ORIGIN'] =~ ZENDESK_DOMAINS_REGEX
57
+ end
58
+
59
+ # This sets the 'Access-Control-Allow-Origin' header for requests coming from zendesk
60
+ def access_control_allow_origin
61
+ origin = request.env['HTTP_ORIGIN']
62
+ headers 'Access-Control-Allow-Origin' => origin if origin =~ ZENDESK_DOMAINS_REGEX
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,82 @@
1
+ require 'zendesk_apps_tools/common'
2
+ require 'yaml'
3
+
4
+ module ZendeskAppsTools
5
+ class Settings
6
+ def get_settings_from_user_input(user_input, parameters)
7
+ return {} if parameters.nil?
8
+
9
+ parameters.inject({}) do |settings, param|
10
+ if param['default']
11
+ input = user_input.get_value_from_stdin("Enter a value for parameter '#{param['name']}' or press 'Return' to use the default value '#{param['default']}':\n", allow_empty: true)
12
+ input = param['default'] if input.empty?
13
+ elsif param['required']
14
+ input = user_input.get_value_from_stdin("Enter a value for required parameter '#{param['name']}':\n")
15
+ else
16
+ input = user_input.get_value_from_stdin("Enter a value for optional parameter '#{param['name']}' or press 'Return' to skip:\n", allow_empty: true)
17
+ end
18
+
19
+ if param['type'] == 'checkbox'
20
+ input = convert_to_boolean_for_checkbox(input)
21
+ end
22
+
23
+ settings[param['name']] = input if input != ''
24
+ settings
25
+ end
26
+ end
27
+
28
+ def get_settings_from_file(filepath, parameters)
29
+ return {} if parameters.nil?
30
+ return nil unless File.exist? filepath
31
+
32
+ begin
33
+ settings_file = File.read(filepath)
34
+
35
+ if filepath =~ /\.json$/ || settings_file =~ /\A\s*{/
36
+ settings_data = JSON.load(settings_file)
37
+ else
38
+ settings_data = YAML.load(settings_file)
39
+ end
40
+
41
+ settings_data.each do |index, setting|
42
+ if setting.is_a?(Hash) || setting.is_a?(Array)
43
+ settings_data[index] = JSON.dump(setting)
44
+ end
45
+ end
46
+ rescue => err
47
+ puts "Failed to load #{filepath}"
48
+ puts err.message
49
+ return nil
50
+ end
51
+
52
+ parameters.inject({}) do |settings, param|
53
+ input = settings_data[param['name']]
54
+
55
+ if !input && param['default']
56
+ input = param['default']
57
+ end
58
+
59
+ if !input && param['required']
60
+ puts "'#{param['name']}' is required but not specified in the config file.\n"
61
+ return nil
62
+ end
63
+
64
+ if param['type'] == 'checkbox'
65
+ input = convert_to_boolean_for_checkbox(input)
66
+ end
67
+
68
+ settings[param['name']] = input if input != ''
69
+ settings
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def convert_to_boolean_for_checkbox(input)
76
+ unless [TrueClass, FalseClass].include?(input.class)
77
+ return (input =~ /^(true|t|yes|y|1)$/i) ? true : false
78
+ end
79
+ input
80
+ end
81
+ end
82
+ end