locomotivecms_wagon 2.0.0.pre.alpha.3 → 2.0.0.pre.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -6
  3. data/bin/wagon +1 -1
  4. data/generators/cloned/config/deploy.yml.tt +3 -2
  5. data/generators/content_type/app/content_types/%slug%.yml.tt +7 -0
  6. data/lib/locomotive/wagon/cli.rb +66 -73
  7. data/lib/locomotive/wagon/commands/clone_command.rb +35 -0
  8. data/lib/locomotive/wagon/commands/destroy_command.rb +29 -0
  9. data/lib/locomotive/wagon/commands/loggers/base_logger.rb +31 -0
  10. data/lib/locomotive/wagon/commands/loggers/pull_logger.rb +23 -0
  11. data/lib/locomotive/wagon/commands/loggers/push_logger.rb +23 -17
  12. data/lib/locomotive/wagon/commands/loggers/sync_logger.rb +23 -0
  13. data/lib/locomotive/wagon/commands/pull_command.rb +55 -0
  14. data/lib/locomotive/wagon/commands/pull_sub_commands/concerns/assets_concern.rb +75 -0
  15. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_base_command.rb +83 -0
  16. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_content_assets_command.rb +24 -0
  17. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_content_entries_command.rb +94 -0
  18. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_content_types_command.rb +42 -0
  19. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_pages_command.rb +54 -0
  20. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_site_command.rb +55 -0
  21. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_snippets_command.rb +29 -0
  22. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_theme_assets_command.rb +29 -0
  23. data/lib/locomotive/wagon/commands/pull_sub_commands/pull_translations_command.rb +24 -0
  24. data/lib/locomotive/wagon/commands/push_command.rb +1 -1
  25. data/lib/locomotive/wagon/commands/push_sub_commands/push_site_command.rb +28 -0
  26. data/lib/locomotive/wagon/commands/serve_command.rb +1 -1
  27. data/lib/locomotive/wagon/commands/sync_command.rb +57 -0
  28. data/lib/locomotive/wagon/commands/sync_sub_commands/concerns/base_concern.rb +41 -0
  29. data/lib/locomotive/wagon/commands/sync_sub_commands/sync_content_entries_command.rb +9 -0
  30. data/lib/locomotive/wagon/commands/sync_sub_commands/sync_pages_command.rb +41 -0
  31. data/lib/locomotive/wagon/commands/sync_sub_commands/sync_translations_command.rb +9 -0
  32. data/lib/locomotive/wagon/decorators/concerns/to_hash_concern.rb +1 -1
  33. data/lib/locomotive/wagon/decorators/content_type_decorator.rb +7 -3
  34. data/lib/locomotive/wagon/decorators/content_type_field_decorator.rb +6 -1
  35. data/lib/locomotive/wagon/decorators/site_decorator.rb +8 -0
  36. data/lib/locomotive/wagon/generators/site/base.rb +1 -1
  37. data/lib/locomotive/wagon/generators/site/cloned.rb +1 -1
  38. data/lib/locomotive/wagon/generators/site.rb +0 -2
  39. data/lib/locomotive/wagon/tools/styled_yaml.rb +122 -0
  40. data/lib/locomotive/wagon/version.rb +1 -1
  41. data/lib/locomotive/wagon.rb +22 -57
  42. data/locomotivecms_wagon.gemspec +2 -2
  43. data/spec/fixtures/cassettes/authenticate.yml +49 -122
  44. data/spec/fixtures/cassettes/push.yml +8960 -9147
  45. data/spec/fixtures/default/icon.png +0 -0
  46. data/spec/integration/commands/push_command_spec.rb +1 -1
  47. metadata +31 -13
  48. data/generators/blank/config.ru +0 -3
  49. data/generators/bootstrap3/config.ru +0 -3
  50. data/generators/cloned/config.ru +0 -3
  51. data/generators/foundation5/config.ru +0 -3
  52. data/generators/line_case/config.ru +0 -3
  53. data/lib/locomotive/wagon/tools/deployment_connection.rb +0 -120
  54. data/lib/locomotive/wagon/tools/hosting_api.rb +0 -117
@@ -0,0 +1,83 @@
1
+ require_relative './concerns/assets_concern'
2
+
3
+ module Locomotive::Wagon
4
+
5
+ class PullBaseCommand < Struct.new(:api_client, :current_site, :path)
6
+
7
+ include Locomotive::Wagon::AssetsConcern
8
+
9
+ def self.pull(api_client, current_site, path)
10
+ new(api_client, current_site, path).pull
11
+ end
12
+
13
+ def pull
14
+ instrument do
15
+ instrument :start
16
+ self._pull_with_timezone
17
+ instrument :done
18
+ end
19
+ end
20
+
21
+ def _pull_with_timezone
22
+ Time.use_zone(current_site.try(:timezone)) do
23
+ _pull
24
+ end
25
+ end
26
+
27
+ def instrument(action = nil, payload = {}, &block)
28
+ name = [instrument_base_name, [*action]].flatten.compact.join('.')
29
+ ActiveSupport::Notifications.instrument(name, { name: resource_name }.merge(payload), &block)
30
+ end
31
+
32
+ def dump(element, options = {})
33
+ if element.is_a?(Hash)
34
+ StyledYAML.dump(element.dup.tap do |attributes|
35
+ [*options[:inline]].each do |name|
36
+ attributes[name] = StyledYAML.inline(attributes[name])
37
+ end
38
+ end)
39
+ else
40
+ element.to_yaml
41
+ end.gsub(/\A---\n/, '')
42
+ end
43
+
44
+ def clean_attributes(attributes)
45
+ # remove nil or empty values
46
+ attributes.delete_if { |_, v| v.nil? || v == '' || (v.is_a?(Hash) && v.empty?) }
47
+ end
48
+
49
+ def write_to_file(filepath, content = nil, mode = 'w+', &block)
50
+ _filepath = File.join(path, filepath)
51
+
52
+ folder = File.dirname(_filepath)
53
+ FileUtils.mkdir_p(folder) unless File.exists?(folder)
54
+
55
+ File.open(_filepath, mode) do |file|
56
+ file.write(content ? content : yield)
57
+ end
58
+ end
59
+
60
+ def reset_file(filepath)
61
+ _filepath = File.join(path, filepath)
62
+ FileUtils.rm(_filepath) if File.exists?(_filepath)
63
+ end
64
+
65
+ def instrument_base_name
66
+ 'wagon.pull'
67
+ end
68
+
69
+ def resource_name
70
+ self.class.name[/::Pull(\w+)Command$/, 1].underscore
71
+ end
72
+
73
+ def default_locale
74
+ current_site.locales.first
75
+ end
76
+
77
+ def locales
78
+ current_site.locales
79
+ end
80
+
81
+ end
82
+
83
+ end
@@ -0,0 +1,24 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PullContentAssetsCommand < PullBaseCommand
4
+
5
+ def _pull
6
+ api_client.content_assets.all.each do |asset|
7
+ write_content_asset(asset)
8
+ end
9
+ end
10
+
11
+ def write_content_asset(asset)
12
+ binary = get_asset_binary(asset.url)
13
+ write_to_file(content_asset_filepath(asset), binary)
14
+ end
15
+
16
+ private
17
+
18
+ def content_asset_filepath(asset)
19
+ File.join('public', 'samples', 'all', asset.content_type_text, asset.filename)
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,94 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PullContentEntriesCommand < PullBaseCommand
4
+
5
+ def _pull
6
+ fetch_content_types do |content_type|
7
+ # delete the previous file
8
+ reset_file(content_entry_filepath(content_type))
9
+
10
+ fetch_content_entries(content_type) do |entries|
11
+ # entries is a list of max 10 elements (pagination)
12
+ write_content_entries(content_type, entries)
13
+ end
14
+ end
15
+ end
16
+
17
+ def write_content_entries(content_type, entries)
18
+ _entries = entries.map do |entry|
19
+ yaml_attributes(content_type, entry)
20
+ end
21
+
22
+ write_to_file(content_entry_filepath(content_type), dump(_entries), 'a')
23
+ end
24
+
25
+ private
26
+
27
+ def yaml_attributes(content_type, entry)
28
+ fields = %w(_slug) + content_type.fields.map { |f| f['name'] } + %w(seo_title meta_description meta_keywords)
29
+ localized_fields = (content_type.attributes['localized_names'] || []) + %w(_slug seo_title meta_description meta_keywords)
30
+ fields_with_urls = content_type.attributes['urls_names']
31
+
32
+ attributes = {}
33
+
34
+ fields.each do |name|
35
+ attributes[name] = if localized_fields.include?(name) && locales.size > 1 && !content_type.attributes['localized_names'].empty?
36
+ clean_attributes({}.tap do |translations|
37
+ locales.each { |l| translations[l] = value_of(content_type, entry, l, name) }
38
+ end)
39
+ else
40
+ value_of(content_type, entry, default_locale, name)
41
+ end
42
+ end
43
+
44
+ { entry[default_locale].attributes[content_type.label_field_name] => clean_attributes(attributes) }
45
+ end
46
+
47
+ def fetch_content_types(&block)
48
+ api_client.content_types.all.each do |content_type|
49
+ content_type.attributes['localized_names'] = content_type.fields.map { |f| f['localized'] ? f['name'] : nil }.compact
50
+ content_type.attributes['urls_names'] = content_type.fields.map { |f| %w(file text).include?(f['type']) ? f['name'] : nil }.compact
51
+ yield(content_type)
52
+ end
53
+ end
54
+
55
+ def fetch_content_entries(content_type, &block)
56
+ page = 1
57
+ while page do
58
+ entries, _next_page = {}, nil
59
+
60
+ locales.each do |locale|
61
+ next if locale != default_locale && content_type.localized_names.empty?
62
+
63
+ (_entries = api_client.content_entries(content_type).all(nil, { page: page }, locale)).each do |entry|
64
+ (entries[entry._id] ||= {})[locale] = entry
65
+ end
66
+
67
+ _next_page = _entries._next_page if _next_page.nil?
68
+ end
69
+
70
+ yield(entries.values)
71
+
72
+ page = _next_page
73
+ end
74
+ end
75
+
76
+ def value_of(content_type, entry, locale, name)
77
+ if value = entry[locale].attributes[name]
78
+ if content_type.attributes['urls_names'].try(:include?, name)
79
+ replace_asset_urls(value)
80
+ else
81
+ value
82
+ end
83
+ else
84
+ nil
85
+ end
86
+ end
87
+
88
+ def content_entry_filepath(content_type)
89
+ File.join('data', "#{content_type.slug}.yml")
90
+ end
91
+
92
+ end
93
+
94
+ end
@@ -0,0 +1,42 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PullContentTypesCommand < PullBaseCommand
4
+
5
+ def _pull
6
+ api_client.content_types.all.each do |content_type|
7
+ write_content_type(content_type)
8
+ end
9
+ end
10
+
11
+ def write_content_type(content_type)
12
+ yaml = dump(yaml_attributes(content_type), inline: %w(public_submission_account_emails))
13
+
14
+ write_to_file(content_type_filepath(content_type), yaml)
15
+ end
16
+
17
+ private
18
+
19
+ def yaml_attributes(content_type)
20
+ content_type.attributes.slice('name', 'slug', 'description', 'label_field_name', 'order_by', 'order_direction', 'group_by', 'public_submission_enabled', 'public_submission_account_emails', 'display_settings').tap do |attributes|
21
+ # fields
22
+ attributes['fields'] = content_type.fields.map { |f| field_yaml_attributes(f) }
23
+
24
+ clean_attributes(attributes)
25
+ end
26
+ end
27
+
28
+ def field_yaml_attributes(field)
29
+ attributes = field.slice('label', 'type', 'required', 'hint', 'localized', 'select_options', 'class_name', 'inverse_of', 'ui_enabled')
30
+
31
+ clean_attributes(attributes)
32
+
33
+ { field['name'] => attributes }
34
+ end
35
+
36
+ def content_type_filepath(content_type)
37
+ File.join('app', 'content_types', "#{content_type.slug}.yml")
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,54 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PullPagesCommand < PullBaseCommand
4
+
5
+ attr_reader :fullpaths
6
+
7
+ def _pull
8
+ @fullpaths = {}
9
+
10
+ locales.each do |locale|
11
+ api_client.pages.all(locale).each do |page|
12
+ fullpaths[page._id] = page.fullpath if locale == default_locale
13
+ write_page(page, locale)
14
+ end
15
+ end
16
+ end
17
+
18
+ def write_page(page, locale = nil)
19
+ write_to_file(page_filepath(page, locale)) do
20
+ <<-EOF
21
+ #{yaml_attributes(page, locale)}---
22
+ #{replace_asset_urls(page.template)}
23
+ EOF
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def yaml_attributes(page, locale)
30
+ attributes = page.attributes.slice('title', 'slug', 'handle', 'position', 'listed', 'published', 'redirect_url', 'is_layout', 'content_type', 'seo_title', 'meta_description', 'meta_keywords')
31
+
32
+ if locale != default_locale
33
+ attributes.delete_if { |k, _| %w(handle position listed published is_layout content_type).include?(k) }
34
+ end
35
+
36
+ # editable elements
37
+ attributes['editable_elements'] = page.editable_elements.inject({}) do |hash, el|
38
+ hash["#{el['block']}/#{el['slug']}"] = replace_asset_urls(el['content']) if el['content']
39
+ hash
40
+ end
41
+
42
+ clean_attributes(attributes)
43
+
44
+ attributes.to_yaml
45
+ end
46
+
47
+ def page_filepath(page, locale)
48
+ fullpath = locale == default_locale ? page.fullpath : "#{fullpaths[page._id]}.#{locale}"
49
+ File.join('app', 'views', 'pages', fullpath + '.liquid')
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,55 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PullSiteCommand < PullBaseCommand
4
+
5
+ def _pull
6
+ attributes = current_site.attributes.slice('name', 'locales', 'domains', 'timezone', 'seo_title', 'meta_keywords', 'meta_description', 'picture_thumbnail_url')
7
+
8
+ locales.each_with_index do |locale, index|
9
+ if index == 0
10
+ transform_in_default_locale(attributes, locale)
11
+ else
12
+ add_other_locale(attributes, locale)
13
+ end
14
+ end if locales.size > 1
15
+
16
+ write_icon(attributes.delete('picture_thumbnail_url'))
17
+
18
+ write_to_file(File.join('config', 'site.yml')) do
19
+ dump(attributes, inline: %w(locales domains))
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def write_icon(url)
26
+ return if url.blank?
27
+
28
+ unless url =~ /\Ahttp:\/\//
29
+ base = api_client.uri.dup.tap { |u| u.path = '' }.to_s
30
+ url = URI.join(base, url).to_s
31
+ end
32
+
33
+ File.open(File.join(path, 'icon.png'), 'wb') do |file|
34
+ file.write Faraday.get(url).body
35
+ end
36
+ end
37
+
38
+ def localized_attributes(&block)
39
+ %w(seo_title meta_keywords meta_description).each do |name|
40
+ yield(name)
41
+ end
42
+ end
43
+
44
+ def transform_in_default_locale(attributes, locale)
45
+ localized_attributes { |k| attributes[k] = { locale => attributes[k] } }
46
+ end
47
+
48
+ def add_other_locale(attributes, locale)
49
+ _site = api_client.current_site.get(locale)
50
+ localized_attributes { |k| attributes[k][locale] = _site.attributes[k] }
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,29 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PullSnippetsCommand < PullBaseCommand
4
+
5
+ def _pull
6
+ locales.each do |locale|
7
+ api_client.snippets.all(locale).each do |snippet|
8
+ write_snippet(snippet, locale)
9
+ end
10
+ end
11
+ end
12
+
13
+ def write_snippet(snippet, locale = nil)
14
+ if (template = snippet.template).present?
15
+ _template = replace_asset_urls(template)
16
+ write_to_file(snippet_filepath(snippet, locale), _template)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def snippet_filepath(snippet, locale)
23
+ filename = locale == default_locale ? snippet.slug : "#{snippet.slug}.#{locale}"
24
+ File.join('app', 'views', 'snippets', filename + '.liquid')
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,29 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PullThemeAssetsCommand < PullBaseCommand
4
+
5
+ def _pull
6
+ api_client.theme_assets.all.each do |asset|
7
+ write_theme_asset(asset)
8
+ end
9
+ end
10
+
11
+ def write_theme_asset(asset)
12
+ binary = get_asset_binary(asset.url)
13
+
14
+ if %w(javascript stylesheet).include?(asset.content_type)
15
+ binary = replace_asset_urls(binary)
16
+ end
17
+
18
+ write_to_file(theme_asset_filepath(asset), binary)
19
+ end
20
+
21
+ private
22
+
23
+ def theme_asset_filepath(asset)
24
+ File.join('public', asset.folder, asset.local_path)
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,24 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PullTranslationsCommand < PullBaseCommand
4
+
5
+ def _pull
6
+ translations = api_client.translations.all.inject({}) do |hash, translation|
7
+ hash[translation.key] = translation.values
8
+ hash
9
+ end
10
+
11
+ unless translations.empty?
12
+ write_to_file(translations_filepath, dump(translations))
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def translations_filepath
19
+ File.join('config', 'translations.yml')
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -13,7 +13,7 @@ module Locomotive::Wagon
13
13
 
14
14
  class PushCommand < Struct.new(:env, :path, :options, :shell)
15
15
 
16
- RESOURCES = %w(content_types content_entries pages snippets theme_assets translations).freeze
16
+ RESOURCES = %w(site content_types content_entries pages snippets theme_assets translations).freeze
17
17
 
18
18
  RESOURCES_WITH_CONTENT = %w(content_entries translations).freeze
19
19
 
@@ -0,0 +1,28 @@
1
+ module Locomotive::Wagon
2
+
3
+ class PushSiteCommand < PushBaseCommand
4
+
5
+ def entities
6
+ [repositories.site.first]
7
+ end
8
+
9
+ def decorate(entity)
10
+ IconSiteDecorator.new(entity)
11
+ end
12
+
13
+ def persist(decorated_entity)
14
+ _attributes = decorated_entity.to_hash
15
+ if !_attributes.empty? && api_client.current_site.get.attributes[:picture_url].nil?
16
+ api_client.current_site.update(_attributes)
17
+ else
18
+ raise SkipPersistingException.new
19
+ end
20
+ end
21
+
22
+ def label_for(decorated_entity)
23
+ decorated_entity.name
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -56,7 +56,7 @@ module Locomotive::Wagon
56
56
 
57
57
  Locomotive::Steam.configure do |config|
58
58
  config.mode = :test
59
- config.adapter = { name: :filesystem, path: path }
59
+ config.adapter = { name: :filesystem, path: File.expand_path(path) }
60
60
  config.asset_path = File.expand_path(File.join(path, 'public'))
61
61
 
62
62
  if port = options[:live_reload_port]
@@ -0,0 +1,57 @@
1
+ require 'locomotive/common'
2
+
3
+ require_relative '../tools/styled_yaml'
4
+
5
+ require_relative 'loggers/sync_logger'
6
+
7
+ require_relative_all 'concerns'
8
+ require_relative 'sync_sub_commands/concerns/base_concern'
9
+
10
+ require_relative 'pull_sub_commands/pull_base_command'
11
+ require_relative_all 'pull_sub_commands'
12
+ require_relative_all 'sync_sub_commands'
13
+
14
+ module Locomotive::Wagon
15
+
16
+ class SyncCommand < Struct.new(:env, :path, :options)
17
+
18
+ RESOURCES = %w(pages content_entries translations).freeze
19
+
20
+ include ApiConcern
21
+ include DeployFileConcern
22
+ include InstrumentationConcern
23
+
24
+ def self.sync(env, path, options)
25
+ self.new(env, path, options).sync
26
+ end
27
+
28
+ def sync
29
+ SyncLogger.new if options[:verbose]
30
+
31
+ api_client = api_site_client(connection_information)
32
+ site = api_client.current_site.get
33
+
34
+ each_resource do |klass|
35
+ klass.sync(api_client, site, path)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def each_resource
42
+ RESOURCES.each do |name|
43
+ next if !options[:resources].blank? && !options[:resources].include?(name)
44
+
45
+ klass = "Locomotive::Wagon::Sync#{name.camelcase}Command".constantize
46
+
47
+ yield klass
48
+ end
49
+ end
50
+
51
+ def connection_information
52
+ read_deploy_settings(self.env, self.path)
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,41 @@
1
+ require 'active_support/concern'
2
+
3
+ module Locomotive::Wagon
4
+
5
+ module BaseConcern
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+
11
+ alias :_sync :_pull
12
+
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def sync(api_client, current_site, path)
18
+ new(api_client, current_site, path).sync
19
+ end
20
+
21
+ end
22
+
23
+ def sync
24
+ instrument do
25
+ instrument :start
26
+ self._sync
27
+ instrument :done
28
+ end
29
+ end
30
+
31
+ def instrument_base_name
32
+ 'wagon.sync'
33
+ end
34
+
35
+ def resource_name
36
+ self.class.name[/::Sync(\w+)Command$/, 1].underscore
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,9 @@
1
+ module Locomotive::Wagon
2
+
3
+ class SyncContentEntriesCommand < PullContentEntriesCommand
4
+
5
+ include Locomotive::Wagon::BaseConcern
6
+
7
+ end
8
+
9
+ end
@@ -0,0 +1,41 @@
1
+ module Locomotive::Wagon
2
+
3
+ class SyncPagesCommand < PullPagesCommand
4
+
5
+ include Locomotive::Wagon::BaseConcern
6
+
7
+ def write_page(page, locale = nil)
8
+ if attributes = editable_elements_attributes(page, locale)
9
+ new_content = replace_editable_elements(page_filepath(page, locale), dump(attributes))
10
+
11
+ write_to_file(page_filepath(page, locale), new_content)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def editable_elements_attributes(page, locale)
18
+ list = page.editable_elements.inject({}) do |hash, el|
19
+ hash["#{el['block']}/#{el['slug']}"] = replace_asset_urls(el['content']) if el['content']
20
+ hash
21
+ end
22
+
23
+ list.empty? ? nil : { 'editable_elements' => list }
24
+ end
25
+
26
+ def replace_editable_elements(filepath, replacement)
27
+ content = File.read(File.join(path, filepath))
28
+ existing = content =~ /\A.*?editable_elements:.*?\n---/m
29
+
30
+ content.gsub /\A---(.*?)\n---/m do |s|
31
+ if existing
32
+ s.gsub(/editable_elements:\n(.*?)\n(\S)/m) { |_s| "#{replacement}#{$2}" }
33
+ else
34
+ s.gsub(/---\Z/) { |_s| "#{replacement}\n---" }
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,9 @@
1
+ module Locomotive::Wagon
2
+
3
+ class SyncTranslationsCommand < PullTranslationsCommand
4
+
5
+ include Locomotive::Wagon::BaseConcern
6
+
7
+ end
8
+
9
+ end
@@ -17,7 +17,7 @@ module Locomotive::Wagon
17
17
  end
18
18
 
19
19
  def prepare_value_for_hash(value)
20
- if value.is_a?(Array) && value.first.respond_to?(:__attributes__)
20
+ if value.is_a?(Array) && value.any? { |v| v.respond_to?(:__attributes__) }
21
21
  value.map(&:to_hash)
22
22
  elsif value.is_a?(Array) && value.empty?
23
23
  nil # reset the array