xat 1.32.0
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 +38 -0
- data/app_template/README.md +14 -0
- data/app_template/app.css +0 -0
- data/app_template/app.js +12 -0
- data/app_template/assets/banner.png +0 -0
- data/app_template/assets/logo-promotion.png +0 -0
- data/app_template/assets/logo-small.png +0 -0
- data/app_template/assets/logo.png +0 -0
- data/app_template/manifest.json.tt +13 -0
- data/app_template/templates/layout.hdbs +10 -0
- data/app_template/translations/en.json +16 -0
- data/app_template_iframe/README.md +14 -0
- data/app_template_iframe/assets/banner.png +0 -0
- data/app_template_iframe/assets/iframe.html +13 -0
- data/app_template_iframe/assets/logo-promotion.png +0 -0
- data/app_template_iframe/assets/logo-small.png +0 -0
- data/app_template_iframe/assets/logo.png +0 -0
- data/app_template_iframe/manifest.json.tt +15 -0
- data/app_template_iframe/translations/en.json +16 -0
- data/bin/xat +5 -0
- data/features/clean.feature +9 -0
- data/features/fixtures/quote_character_translation.json +6 -0
- data/features/new.feature +117 -0
- data/features/package.feature +22 -0
- data/features/step_definitions/app_steps.rb +103 -0
- data/features/support/env.rb +5 -0
- data/features/validate.feature +15 -0
- data/lib/xat.rb +5 -0
- data/lib/zendesk_apps_tools/api_connection.rb +33 -0
- data/lib/zendesk_apps_tools/array_patch.rb +22 -0
- data/lib/zendesk_apps_tools/bump.rb +60 -0
- data/lib/zendesk_apps_tools/cache.rb +25 -0
- data/lib/zendesk_apps_tools/command.rb +183 -0
- data/lib/zendesk_apps_tools/command_helpers.rb +20 -0
- data/lib/zendesk_apps_tools/common.rb +40 -0
- data/lib/zendesk_apps_tools/deploy.rb +94 -0
- data/lib/zendesk_apps_tools/directory.rb +28 -0
- data/lib/zendesk_apps_tools/locale_identifier.rb +10 -0
- data/lib/zendesk_apps_tools/manifest_handler.rb +72 -0
- data/lib/zendesk_apps_tools/package_helper.rb +30 -0
- data/lib/zendesk_apps_tools/server.rb +65 -0
- data/lib/zendesk_apps_tools/settings.rb +82 -0
- data/lib/zendesk_apps_tools/translate.rb +168 -0
- data/templates/translation.erb.tt +13 -0
- metadata +238 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
Feature: package a zendesk app into a zip file
|
2
|
+
|
3
|
+
Background: create a new zendesk app
|
4
|
+
Given an app is created in directory "tmp/aruba"
|
5
|
+
|
6
|
+
Scenario: package a zendesk app by running 'xat package' command
|
7
|
+
When I run the command "xat package --path tmp/aruba" to package the app
|
8
|
+
And the command output should contain "adding app.js"
|
9
|
+
And the command output should contain "adding assets/logo-small.png"
|
10
|
+
And the command output should contain "adding assets/logo.png"
|
11
|
+
And the command output should contain "adding manifest.json"
|
12
|
+
And the command output should contain "adding templates/layout.hdbs"
|
13
|
+
And the command output should contain "adding translations/en.json"
|
14
|
+
And the command output should contain "created"
|
15
|
+
And the zip file should exist in directory "tmp/aruba/tmp"
|
16
|
+
|
17
|
+
|
18
|
+
Scenario: package a zendesk app by running 'xat package' command
|
19
|
+
When I create a symlink from "./templates/translation.erb.tt" to "tmp/aruba/assets/translation.erb.tt"
|
20
|
+
Then "tmp/aruba/assets/translation.erb.tt" should be a symlink
|
21
|
+
When I run the command "xat package --path tmp/aruba" to package the app
|
22
|
+
Then the zip file in "tmp/aruba/tmp" should not contain any symlinks
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'zip/zip'
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
When /^I move to the app directory$/ do
|
6
|
+
@previous_dir = Dir.pwd
|
7
|
+
Dir.chdir(@app_dir)
|
8
|
+
end
|
9
|
+
|
10
|
+
When /^I reset the working directory$/ do
|
11
|
+
Dir.chdir(@previous_dir)
|
12
|
+
end
|
13
|
+
|
14
|
+
Given /^an app directory "(.*?)" exists$/ do |app_dir|
|
15
|
+
@app_dir = app_dir
|
16
|
+
FileUtils.rm_rf(@app_dir)
|
17
|
+
FileUtils.mkdir_p(@app_dir)
|
18
|
+
end
|
19
|
+
|
20
|
+
Given /^an app is created in directory "(.*?)"$/ do |app_dir|
|
21
|
+
steps %(
|
22
|
+
Given an app directory "#{app_dir}" exists
|
23
|
+
And I run "xat new" command with the following details:
|
24
|
+
| author name | John Citizen |
|
25
|
+
| author email | john@example.com |
|
26
|
+
| author url | http://myapp.com |
|
27
|
+
| app name | John Test App |
|
28
|
+
| app dir | #{app_dir} |
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
When /^I run "(.*?)" command with the following details:$/ do |cmd, table|
|
33
|
+
IO.popen(cmd, 'w+') do |pipe|
|
34
|
+
# [ ['parameter name', 'value'] ]
|
35
|
+
table.raw.each do |row|
|
36
|
+
pipe.puts row.last
|
37
|
+
end
|
38
|
+
pipe.close_write
|
39
|
+
@output = pipe.readlines
|
40
|
+
@output.each { |line| puts line }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
When /^I create a symlink from "(.*?)" to "(.*?)"$/ do |src, dest|
|
45
|
+
@link_destname = File.basename(dest)
|
46
|
+
# create a symlink
|
47
|
+
FileUtils.ln_s(src, dest)
|
48
|
+
end
|
49
|
+
|
50
|
+
When /^I run the command "(.*?)" to (validate|package|clean) the app$/ do |cmd, _action|
|
51
|
+
IO.popen(cmd, 'w+') do |pipe|
|
52
|
+
pipe.puts "\n"
|
53
|
+
pipe.close_write
|
54
|
+
@output = pipe.readlines
|
55
|
+
@output.each { |line| puts line }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Then /^the app file "(.*?)" is created with:$/ do |file, content|
|
60
|
+
File.read(file).chomp.gsub(' ', '').should == content.gsub(' ', '')
|
61
|
+
end
|
62
|
+
|
63
|
+
Then /^the app file "(.*?)" is created$/ do |filename|
|
64
|
+
File.exist?(filename).should be_truthy
|
65
|
+
end
|
66
|
+
|
67
|
+
Then /^the fixture "(.*?)" is used for "(.*?)"$/ do |fixture, app_file|
|
68
|
+
fixture_file = File.join('features', 'fixtures', fixture)
|
69
|
+
app_file_path = File.join(@app_dir, app_file)
|
70
|
+
|
71
|
+
FileUtils.cp(fixture_file, app_file_path)
|
72
|
+
end
|
73
|
+
|
74
|
+
Then /^the zip file should exist in directory "(.*?)"$/ do |path|
|
75
|
+
Dir[path + '/app-*.zip'].size.should == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
Given /^I remove file "(.*?)"$/ do |file|
|
79
|
+
File.delete(file)
|
80
|
+
end
|
81
|
+
|
82
|
+
Then /^the zip file in "(.*?)" folder should not exist$/ do |path|
|
83
|
+
expect(Dir[path + '/app-*.zip'].size).to eq 0
|
84
|
+
end
|
85
|
+
|
86
|
+
Then /^it should pass the validation$/ do
|
87
|
+
@output.last.should =~ /OK/
|
88
|
+
$CHILD_STATUS.should == 0
|
89
|
+
end
|
90
|
+
|
91
|
+
Then /^the command output should contain "(.*?)"$/ do |output|
|
92
|
+
@output.join.should =~ /#{output}/
|
93
|
+
end
|
94
|
+
|
95
|
+
Then /^"(.*?)" should be a symlink$/ do |path|
|
96
|
+
File.symlink?(path).should be_truthy
|
97
|
+
end
|
98
|
+
|
99
|
+
Then /^the zip file in "(.*?)" should not contain any symlinks$/ do |path|
|
100
|
+
Zip::ZipFile.foreach Dir[path + '/app-*.zip'][0] do |p|
|
101
|
+
p.symlink?.should be_falsy
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Feature: validate a zendesk app
|
2
|
+
|
3
|
+
Validate a zendesk app by running 'xat package' command
|
4
|
+
|
5
|
+
Background: create a new zendesk app
|
6
|
+
Given an app is created in directory "tmp/aruba"
|
7
|
+
|
8
|
+
Scenario: valid app
|
9
|
+
When I run the command "xat validate --path tmp/aruba" to validate the app
|
10
|
+
Then it should pass the validation
|
11
|
+
|
12
|
+
Scenario: invalid app (missing manifest.json
|
13
|
+
Given I remove file "tmp/aruba/manifest.json"
|
14
|
+
When I run the command "xat validate --path tmp/aruba" to validate the app
|
15
|
+
Then the command output should contain "Could not find manifest.json"
|
data/lib/xat.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module ZendeskAppsTools
|
2
|
+
module APIConnection
|
3
|
+
FULL_URL = /https?:\/\//
|
4
|
+
URL_TEMPLATE = 'https://%s.zendesk.com/'
|
5
|
+
|
6
|
+
def prepare_api_auth
|
7
|
+
@subdomain ||= fetch_cache('subdomain') || get_value_from_stdin('Enter your Zendesk subdomain or full Zendesk URL:')
|
8
|
+
@username ||= fetch_cache('username') || get_value_from_stdin('Enter your username:')
|
9
|
+
@password ||= fetch_cache('password') || get_password_from_stdin('Enter your password:')
|
10
|
+
|
11
|
+
save_cache 'subdomain' => @subdomain, 'username' => @username
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_connection(encoding = :url_encoded)
|
15
|
+
prepare_api_auth
|
16
|
+
Faraday.new full_url do |f|
|
17
|
+
f.request encoding
|
18
|
+
f.adapter :net_http
|
19
|
+
f.basic_auth @username, @password
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def full_url
|
26
|
+
if FULL_URL =~ @subdomain
|
27
|
+
@subdomain
|
28
|
+
else
|
29
|
+
URL_TEMPLATE % @subdomain
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Array
|
2
|
+
unless instance_methods.include? :to_h
|
3
|
+
def to_h
|
4
|
+
if elem_index = index { |elem| !elem.is_a?(Array) }
|
5
|
+
raise TypeError.new("wrong element type #{self[elem_index].class} at #{elem_index} (expected array)")
|
6
|
+
end
|
7
|
+
|
8
|
+
each_with_index.inject({}) do |hash, elem|
|
9
|
+
pair, index = elem
|
10
|
+
|
11
|
+
if pair.size != 2
|
12
|
+
raise ArgumentError.new("wrong array length at #{index} (expected 2, was #{pair.size})")
|
13
|
+
end
|
14
|
+
|
15
|
+
hash.tap do |h|
|
16
|
+
key, val = pair
|
17
|
+
h[key] = val
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'zendesk_apps_tools/manifest_handler'
|
5
|
+
|
6
|
+
module ZendeskAppsTools
|
7
|
+
class Bump < Thor
|
8
|
+
include Thor::Actions
|
9
|
+
prepend ManifestHandler
|
10
|
+
|
11
|
+
SHARED_OPTIONS = {
|
12
|
+
['commit', '-c'] => false,
|
13
|
+
['message', '-m'] => nil,
|
14
|
+
['tag', '-t'] => false
|
15
|
+
}
|
16
|
+
|
17
|
+
desc 'major', 'Bump major version'
|
18
|
+
method_options SHARED_OPTIONS
|
19
|
+
def major
|
20
|
+
semver[:major] += 1
|
21
|
+
semver[:minor] = 0
|
22
|
+
semver[:patch] = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'minor', 'Bump minor version'
|
26
|
+
method_options SHARED_OPTIONS
|
27
|
+
def minor
|
28
|
+
semver[:minor] += 1
|
29
|
+
semver[:patch] = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'patch', 'Bump patch version'
|
33
|
+
method_options SHARED_OPTIONS
|
34
|
+
def patch
|
35
|
+
semver[:patch] += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
default_task :patch
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def post_actions
|
43
|
+
return tag if options[:tag]
|
44
|
+
commit if options[:commit]
|
45
|
+
end
|
46
|
+
|
47
|
+
def commit
|
48
|
+
`git commit -am #{commit_message}`
|
49
|
+
end
|
50
|
+
|
51
|
+
def commit_message
|
52
|
+
options[:message] || version(v: true)
|
53
|
+
end
|
54
|
+
|
55
|
+
def tag
|
56
|
+
commit
|
57
|
+
`git tag #{version(v: true)}`
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ZendeskAppsTools
|
2
|
+
module Cache
|
3
|
+
CACHE_FILE_NAME = '.xat'
|
4
|
+
|
5
|
+
def save_cache(hash)
|
6
|
+
return if options[:zipfile]
|
7
|
+
|
8
|
+
@cache = File.exist?(cache_path) ? JSON.parse(File.read(@cache_path)).update(hash) : hash
|
9
|
+
File.open(@cache_path, 'w') { |f| f.write JSON.pretty_generate(@cache) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def fetch_cache(key)
|
13
|
+
@cache ||= File.exist?(cache_path) ? JSON.parse(File.read(@cache_path)) : {}
|
14
|
+
@cache[key] if @cache
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear_cache
|
18
|
+
File.delete cache_path if options[:clean] && File.exist?(cache_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache_path
|
22
|
+
@cache_path ||= File.join options[:path], CACHE_FILE_NAME
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'zip/zip'
|
3
|
+
require 'pathname'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
require 'faraday'
|
7
|
+
require 'io/console'
|
8
|
+
require 'zendesk_apps_support'
|
9
|
+
|
10
|
+
require 'zendesk_apps_tools/command_helpers'
|
11
|
+
|
12
|
+
module ZendeskAppsTools
|
13
|
+
class Command < Thor
|
14
|
+
include Thor::Actions
|
15
|
+
include ZendeskAppsSupport
|
16
|
+
include ZendeskAppsTools::CommandHelpers
|
17
|
+
|
18
|
+
SHARED_OPTIONS = {
|
19
|
+
['path', '-p'] => './',
|
20
|
+
clean: false
|
21
|
+
}
|
22
|
+
|
23
|
+
source_root File.expand_path(File.join(File.dirname(__FILE__), '../..'))
|
24
|
+
|
25
|
+
desc 'translate SUBCOMMAND', 'Manage translation files', hide: true
|
26
|
+
subcommand 'translate', Translate
|
27
|
+
|
28
|
+
desc 'bump SUBCOMMAND', 'Bump version for app', hide: true
|
29
|
+
subcommand 'bump', Bump
|
30
|
+
|
31
|
+
desc 'new', 'Generate a new app'
|
32
|
+
method_option :'iframe-only', type: :boolean,
|
33
|
+
default: false,
|
34
|
+
desc: 'Create an iFrame Only app template',
|
35
|
+
aliases: ['-i', '--v2']
|
36
|
+
def new
|
37
|
+
enter = ->(variable) { "Enter this app author's #{variable}:\n" }
|
38
|
+
invalid = ->(variable) { "Invalid #{variable}, try again:" }
|
39
|
+
@author_name = get_value_from_stdin(enter.call('name'),
|
40
|
+
error_msg: invalid.call('name'))
|
41
|
+
@author_email = get_value_from_stdin(enter.call('email'),
|
42
|
+
valid_regex: /^.+@.+\..+$/,
|
43
|
+
error_msg: invalid.call('email'))
|
44
|
+
@author_url = get_value_from_stdin(enter.call('url'),
|
45
|
+
valid_regex: %r{^https?://.+$},
|
46
|
+
error_msg: invalid.call('url'),
|
47
|
+
allow_empty: true)
|
48
|
+
@app_name = get_value_from_stdin("Enter a name for this new app:\n",
|
49
|
+
error_msg: invalid.call('app name'))
|
50
|
+
|
51
|
+
@iframe_location = if options[:'iframe-only']
|
52
|
+
iframe_uri_text = 'Enter your iFrame URI or leave it blank to use'\
|
53
|
+
" a default local template page:\n"
|
54
|
+
value = get_value_from_stdin(iframe_uri_text, allow_empty: true)
|
55
|
+
value == '' ? 'assets/iframe.html' : value
|
56
|
+
else
|
57
|
+
'_legacy'
|
58
|
+
end
|
59
|
+
|
60
|
+
prompt_new_app_dir
|
61
|
+
|
62
|
+
skeleton = options[:'iframe-only'] ? 'app_template_iframe' : 'app_template'
|
63
|
+
is_custom_iframe = options[:'iframe-only'] && @iframe_location != 'assets/iframe.html'
|
64
|
+
directory_options = is_custom_iframe ? { exclude_pattern: /iframe.html/ } : {}
|
65
|
+
directory(skeleton, @app_dir, directory_options)
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'validate', 'Validate your app'
|
69
|
+
method_options SHARED_OPTIONS
|
70
|
+
def validate
|
71
|
+
setup_path(options[:path])
|
72
|
+
errors = app_package.validate(marketplace: false)
|
73
|
+
valid = errors.none?
|
74
|
+
|
75
|
+
if valid
|
76
|
+
app_package.warnings.each { |w| say w.to_s, :yellow }
|
77
|
+
say_status 'validate', 'OK'
|
78
|
+
else
|
79
|
+
errors.each do |e|
|
80
|
+
say_status 'validate', e.to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
@destination_stack.pop if options[:path]
|
85
|
+
exit 1 unless valid
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
desc 'package', 'Package your app'
|
90
|
+
method_options SHARED_OPTIONS
|
91
|
+
def package
|
92
|
+
return false unless invoke(:validate, [])
|
93
|
+
|
94
|
+
setup_path(options[:path])
|
95
|
+
archive_path = File.join(tmp_dir, "app-#{Time.now.strftime('%Y%m%d%H%M%S')}.zip")
|
96
|
+
|
97
|
+
archive_rel_path = relative_to_original_destination_root(archive_path)
|
98
|
+
|
99
|
+
zip archive_path
|
100
|
+
|
101
|
+
say_status 'package', "created at #{archive_rel_path}"
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
desc 'clean', 'Remove app packages in temp folder'
|
106
|
+
method_option :path, default: './', required: false, aliases: '-p'
|
107
|
+
def clean
|
108
|
+
setup_path(options[:path])
|
109
|
+
|
110
|
+
return unless File.exist?(Pathname.new(File.join(app_dir, 'tmp')).to_s)
|
111
|
+
|
112
|
+
FileUtils.rm(Dir["#{tmp_dir}/app-*.zip"])
|
113
|
+
end
|
114
|
+
|
115
|
+
DEFAULT_SERVER_PATH = './'
|
116
|
+
DEFAULT_CONFIG_PATH = './settings.yml'
|
117
|
+
DEFAULT_SERVER_PORT = 4567
|
118
|
+
DEFAULT_APP_ID = 0
|
119
|
+
|
120
|
+
desc 'server', 'Run a http server to serve the local app'
|
121
|
+
method_option :path, default: DEFAULT_SERVER_PATH, required: false, aliases: '-p'
|
122
|
+
method_option :config, default: DEFAULT_CONFIG_PATH, required: false, aliases: '-c'
|
123
|
+
method_option :port, default: DEFAULT_SERVER_PORT, required: false
|
124
|
+
method_option :app_id, default: DEFAULT_APP_ID, required: false
|
125
|
+
def server
|
126
|
+
setup_path(options[:path])
|
127
|
+
manifest = app_package.manifest_json
|
128
|
+
|
129
|
+
settings_helper = ZendeskAppsTools::Settings.new
|
130
|
+
|
131
|
+
settings = settings_helper.get_settings_from_file options[:config], manifest['parameters']
|
132
|
+
|
133
|
+
unless settings
|
134
|
+
settings = settings_helper.get_settings_from_user_input self, manifest['parameters']
|
135
|
+
end
|
136
|
+
|
137
|
+
require 'zendesk_apps_tools/server'
|
138
|
+
ZendeskAppsTools::Server.tap do |server|
|
139
|
+
server.set :port, options[:port]
|
140
|
+
server.set :root, options[:path]
|
141
|
+
server.set :parameters, settings
|
142
|
+
server.set :manifest, manifest['parameters']
|
143
|
+
server.set :config, options[:config]
|
144
|
+
server.set :app_id, options[:app_id]
|
145
|
+
server.run!
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
desc 'create', 'Create app on your account'
|
150
|
+
method_options SHARED_OPTIONS
|
151
|
+
method_option :zipfile, default: nil, required: false, type: :string
|
152
|
+
def create
|
153
|
+
clear_cache
|
154
|
+
@command = 'Create'
|
155
|
+
|
156
|
+
unless options[:zipfile]
|
157
|
+
app_name = JSON.parse(File.read(File.join options[:path], 'manifest.json'))['name']
|
158
|
+
end
|
159
|
+
app_name ||= get_value_from_stdin('Enter app name:')
|
160
|
+
deploy_app(:post, '/api/v2/apps.json', name: app_name)
|
161
|
+
end
|
162
|
+
|
163
|
+
desc 'update', 'Update app on the server'
|
164
|
+
method_options SHARED_OPTIONS
|
165
|
+
method_option :zipfile, default: nil, required: false, type: :string
|
166
|
+
def update
|
167
|
+
clear_cache
|
168
|
+
@command = 'Update'
|
169
|
+
|
170
|
+
app_id = fetch_cache('app_id') || find_app_id
|
171
|
+
unless /\d+/ =~ app_id.to_s
|
172
|
+
say_error_and_exit "App id not found\nPlease try running command with --clean or check your internet connection"
|
173
|
+
end
|
174
|
+
deploy_app(:put, "/api/v2/apps/#{app_id}.json", {})
|
175
|
+
end
|
176
|
+
|
177
|
+
protected
|
178
|
+
|
179
|
+
def setup_path(path)
|
180
|
+
@destination_stack << relative_to_original_destination_root(path) unless @destination_stack.last == path
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|