xat 1.32.0

Sign up to get free protection for your applications and to get access to all the features.
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