shoperb-theme-editor 0.8.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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTING.md +40 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +124 -0
  5. data/bin/shoperb +313 -0
  6. data/lib/shoperb_theme_editor/api/server.rb +10 -0
  7. data/lib/shoperb_theme_editor/api/views/callback.erb +53 -0
  8. data/lib/shoperb_theme_editor/api.rb +264 -0
  9. data/lib/shoperb_theme_editor/build/json.rb +167 -0
  10. data/lib/shoperb_theme_editor/build/liquid.rb +38 -0
  11. data/lib/shoperb_theme_editor/build/section.rb +51 -0
  12. data/lib/shoperb_theme_editor/build/settings.rb +224 -0
  13. data/lib/shoperb_theme_editor/build.rb +68 -0
  14. data/lib/shoperb_theme_editor/configuration.rb +98 -0
  15. data/lib/shoperb_theme_editor/error.rb +35 -0
  16. data/lib/shoperb_theme_editor/ext/array.rb +17 -0
  17. data/lib/shoperb_theme_editor/ext/nil_class.rb +7 -0
  18. data/lib/shoperb_theme_editor/ext/sequel.rb +82 -0
  19. data/lib/shoperb_theme_editor/ext.rb +2 -0
  20. data/lib/shoperb_theme_editor/init.rb +87 -0
  21. data/lib/shoperb_theme_editor/logger.rb +58 -0
  22. data/lib/shoperb_theme_editor/mounter/models/address.rb +65 -0
  23. data/lib/shoperb_theme_editor/mounter/models/attribute.rb +11 -0
  24. data/lib/shoperb_theme_editor/mounter/models/attribute_key.rb +17 -0
  25. data/lib/shoperb_theme_editor/mounter/models/base.rb +196 -0
  26. data/lib/shoperb_theme_editor/mounter/models/blog_category.rb +47 -0
  27. data/lib/shoperb_theme_editor/mounter/models/blog_post.rb +45 -0
  28. data/lib/shoperb_theme_editor/mounter/models/brand.rb +11 -0
  29. data/lib/shoperb_theme_editor/mounter/models/cart.rb +35 -0
  30. data/lib/shoperb_theme_editor/mounter/models/cart_item.rb +71 -0
  31. data/lib/shoperb_theme_editor/mounter/models/category.rb +99 -0
  32. data/lib/shoperb_theme_editor/mounter/models/collection.rb +40 -0
  33. data/lib/shoperb_theme_editor/mounter/models/country.rb +18 -0
  34. data/lib/shoperb_theme_editor/mounter/models/currency.rb +17 -0
  35. data/lib/shoperb_theme_editor/mounter/models/custom_field.rb +22 -0
  36. data/lib/shoperb_theme_editor/mounter/models/customer.rb +77 -0
  37. data/lib/shoperb_theme_editor/mounter/models/customer_customer_group.rb +12 -0
  38. data/lib/shoperb_theme_editor/mounter/models/customer_group.rb +13 -0
  39. data/lib/shoperb_theme_editor/mounter/models/customer_subscription.rb +40 -0
  40. data/lib/shoperb_theme_editor/mounter/models/customer_subscription_plan.rb +32 -0
  41. data/lib/shoperb_theme_editor/mounter/models/discount.rb +40 -0
  42. data/lib/shoperb_theme_editor/mounter/models/discount_variant.rb +15 -0
  43. data/lib/shoperb_theme_editor/mounter/models/image.rb +51 -0
  44. data/lib/shoperb_theme_editor/mounter/models/language.rb +17 -0
  45. data/lib/shoperb_theme_editor/mounter/models/link.rb +61 -0
  46. data/lib/shoperb_theme_editor/mounter/models/media_file.rb +19 -0
  47. data/lib/shoperb_theme_editor/mounter/models/menu.rb +21 -0
  48. data/lib/shoperb_theme_editor/mounter/models/meta.rb +10 -0
  49. data/lib/shoperb_theme_editor/mounter/models/news_item.rb +11 -0
  50. data/lib/shoperb_theme_editor/mounter/models/order.rb +133 -0
  51. data/lib/shoperb_theme_editor/mounter/models/order_item.rb +137 -0
  52. data/lib/shoperb_theme_editor/mounter/models/order_item_attribute.rb +17 -0
  53. data/lib/shoperb_theme_editor/mounter/models/order_return.rb +42 -0
  54. data/lib/shoperb_theme_editor/mounter/models/order_return_item.rb +29 -0
  55. data/lib/shoperb_theme_editor/mounter/models/order_return_item_entity.rb +25 -0
  56. data/lib/shoperb_theme_editor/mounter/models/order_return_parcel.rb +20 -0
  57. data/lib/shoperb_theme_editor/mounter/models/page.rb +26 -0
  58. data/lib/shoperb_theme_editor/mounter/models/payment_card.rb +21 -0
  59. data/lib/shoperb_theme_editor/mounter/models/payment_method.rb +67 -0
  60. data/lib/shoperb_theme_editor/mounter/models/payment_provider.rb +23 -0
  61. data/lib/shoperb_theme_editor/mounter/models/product.rb +144 -0
  62. data/lib/shoperb_theme_editor/mounter/models/product_attribute.rb +32 -0
  63. data/lib/shoperb_theme_editor/mounter/models/product_search.rb +53 -0
  64. data/lib/shoperb_theme_editor/mounter/models/product_type.rb +21 -0
  65. data/lib/shoperb_theme_editor/mounter/models/review.rb +38 -0
  66. data/lib/shoperb_theme_editor/mounter/models/search.rb +11 -0
  67. data/lib/shoperb_theme_editor/mounter/models/shipping_method.rb +39 -0
  68. data/lib/shoperb_theme_editor/mounter/models/shop.rb +58 -0
  69. data/lib/shoperb_theme_editor/mounter/models/state.rb +17 -0
  70. data/lib/shoperb_theme_editor/mounter/models/theme.rb +89 -0
  71. data/lib/shoperb_theme_editor/mounter/models/variant.rb +96 -0
  72. data/lib/shoperb_theme_editor/mounter/models/variant_attribute.rb +46 -0
  73. data/lib/shoperb_theme_editor/mounter/models/vendor.rb +38 -0
  74. data/lib/shoperb_theme_editor/mounter/server/assets.rb +35 -0
  75. data/lib/shoperb_theme_editor/mounter/server/defaults.rb +44 -0
  76. data/lib/shoperb_theme_editor/mounter/server/exception_handler.rb +22 -0
  77. data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_footer.liquid +0 -0
  78. data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_header.liquid +0 -0
  79. data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_stylesheets.liquid +3 -0
  80. data/lib/shoperb_theme_editor/mounter/server/renderer.rb +166 -0
  81. data/lib/shoperb_theme_editor/mounter/server/routes/cart.rb +127 -0
  82. data/lib/shoperb_theme_editor/mounter/server/routes/dummy.rb +34 -0
  83. data/lib/shoperb_theme_editor/mounter/server/routes/locale.rb +31 -0
  84. data/lib/shoperb_theme_editor/mounter/server/routes/pages.rb +33 -0
  85. data/lib/shoperb_theme_editor/mounter/server/routes/search.rb +18 -0
  86. data/lib/shoperb_theme_editor/mounter/server/routes.rb +366 -0
  87. data/lib/shoperb_theme_editor/mounter/server/routes_helper.rb +278 -0
  88. data/lib/shoperb_theme_editor/mounter/server.rb +66 -0
  89. data/lib/shoperb_theme_editor/mounter.rb +30 -0
  90. data/lib/shoperb_theme_editor/os.rb +13 -0
  91. data/lib/shoperb_theme_editor/package.rb +81 -0
  92. data/lib/shoperb_theme_editor/sync/images.rb +69 -0
  93. data/lib/shoperb_theme_editor/sync/pagination.rb +52 -0
  94. data/lib/shoperb_theme_editor/sync.rb +229 -0
  95. data/lib/shoperb_theme_editor/translations.rb +22 -0
  96. data/lib/shoperb_theme_editor/utils.rb +50 -0
  97. data/lib/shoperb_theme_editor.rb +159 -0
  98. data/shoperb_theme_editor.gemspec +60 -0
  99. metadata +510 -0
@@ -0,0 +1,224 @@
1
+ require 'tty-prompt'
2
+
3
+ module Shoperb
4
+ module Theme
5
+ module Editor
6
+ module Build
7
+ class Settings
8
+ def initialize(section_handle, json_manager)
9
+ @section_handle = section_handle
10
+ @json_manager = json_manager
11
+ @prompt = TTY::Prompt.new
12
+ end
13
+
14
+ def add_settings
15
+ loop do
16
+ if @json_manager.json_content["settings"].empty?
17
+
18
+ handle = @prompt.ask('Enter the config handle:', required: true)
19
+ type = @prompt.select('Select the config type:', %w[text richtext select number checkbox radio range color video collection product menu image blog_post category subcategory], per_page: 18)
20
+ additional_args = collect_additional_args(type)
21
+
22
+ @json_manager.add_config(handle, type, *additional_args)
23
+
24
+ File.open(File.join(Dir.pwd, "config/sections/#{@section_handle}.json"), 'w') do |file|
25
+ file.write(JSON.pretty_generate(@json_manager.json_content))
26
+ end
27
+
28
+ else
29
+ handle = @prompt.ask('Enter the config handle:', required: true) do |q|
30
+ q.validate { |input| @json_manager.json_content["settings"].none? { |s| s["handle"] == input } }
31
+ q.messages[:valid?] = 'Handle must be unique'
32
+ end
33
+ type = @prompt.select('Select the config type:', %w[text richtext select number checkbox radio range color video collection product menu image blog_post category subcategory], per_page: 18)
34
+ additional_args = collect_additional_args(type)
35
+
36
+ @json_manager.add_config(handle, type, *additional_args)
37
+
38
+ File.open(File.join(Dir.pwd, "config/sections/#{@section_handle}.json"), 'w') do |file|
39
+ file.write(JSON.pretty_generate(@json_manager.json_content))
40
+ end
41
+ end
42
+
43
+ break unless @prompt.yes?('Do you want to add another setting?')
44
+ end
45
+ end
46
+
47
+ def modify_setting(setting_handle)
48
+ settings = @json_manager.json_content["settings"]
49
+ setting = settings.find { |s| s["handle"] == setting_handle }
50
+
51
+ if setting
52
+ type = setting["type"]
53
+ handle = setting["handle"]
54
+
55
+ loop do
56
+ options = [
57
+ { name: "Handle: #{handle}", value: :handle },
58
+ { name: "Type: #{type}", value: :type },
59
+ { name: "Default: #{setting['default']}", value: :default },
60
+ { name: "", disabled: "" },
61
+ { name: "+ Save", value: :save },
62
+ { name: "- Remove", value: :remove }
63
+ ]
64
+ option = @prompt.select('Select an option to modify:', options, per_page: 18)
65
+
66
+ case option
67
+ when :handle
68
+ new_handle = @prompt.ask('Enter the new handle:', default: handle)
69
+ setting["handle"] = new_handle
70
+ when :type
71
+ new_type = @prompt.select('Select the new type:', %w[text richtext select number checkbox radio range color video collection product menu image blog_post category subcategory], per_page: 18, default: type)
72
+ setting["type"] = new_type
73
+ additional_args = collect_additional_args(new_type)
74
+ setting.merge!(build_config_item(new_handle, new_type, *additional_args))
75
+ when :default
76
+ new_default = @prompt.ask('Enter the new default value:', default: setting['default'])
77
+ setting["default"] = new_default
78
+ when :remove
79
+ settings.delete(setting)
80
+ break
81
+ when :save
82
+ break
83
+ end
84
+ end
85
+
86
+ File.open(File.join(Dir.pwd, "config/sections/#{@section_handle}.json"), 'w') do |file|
87
+ file.write(JSON.pretty_generate(@json_manager.json_content))
88
+ end
89
+
90
+ @prompt.say("Setting updated successfully.")
91
+ else
92
+ @prompt.say("Setting not found.")
93
+ end
94
+ end
95
+
96
+ def delete_settings
97
+ settings = @json_manager.json_content["settings"]
98
+
99
+ if settings.empty?
100
+ @prompt.say("No settings to delete.")
101
+ return
102
+ end
103
+
104
+ handle_to_delete = @prompt.select('Select the setting to delete:', settings.map { |s| "- #{s['handle']} (#{s['type']})" }, per_page: 18)
105
+
106
+ setting = settings.find { |s| s["handle"] == handle_to_delete }
107
+
108
+ if setting
109
+ settings.delete(setting)
110
+ File.open(File.join(Dir.pwd, "config/sections/#{@section_handle}.json"), 'w') do |file|
111
+ file.write(JSON.pretty_generate(@json_manager.json_content))
112
+ end
113
+ @prompt.say("Setting deleted successfully.")
114
+ else
115
+ @prompt.say("Setting not found.")
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def collect_additional_args(type)
122
+ case type
123
+ when 'text', 'richtext'
124
+ default = @prompt.ask('Enter the default value for text:', default: '')
125
+ [default]
126
+ when 'select', 'radio'
127
+ default = @prompt.ask('Enter the default value for select:', required: true)
128
+ options = []
129
+ loop do
130
+ value = @prompt.ask('Enter an option value (leave empty to finish):')
131
+ break if value.nil? || value.strip.empty?
132
+ option_handle = @prompt.ask('Enter the option handle:')
133
+ options << { "value" => value, "handle" => option_handle }
134
+ end
135
+ [default, options]
136
+ when 'range'
137
+ min = @prompt.ask('Enter the minimum value for range:', convert: :int, required: true)
138
+ max = @prompt.ask('Enter the maximum value for range:', convert: :int, required: true)
139
+ step = @prompt.ask('Enter the step value for range:', convert: :int, required: true)
140
+ [min, max, step]
141
+ when 'checkbox'
142
+ default = @prompt.yes?('Should the checkbox default to true?')
143
+ [default]
144
+ when 'subcategory'
145
+ sub_settings = []
146
+ loop do
147
+ sub_handle = @prompt.ask('Enter the sub-setting handle:', required: true)
148
+ sub_type = @prompt.select('Select the sub-setting type:', %w[text richtext select number checkbox radio range color video collection product menu image blog_post category])
149
+ sub_default = @prompt.ask('Enter the default value for sub-setting:', default: '')
150
+ sub_settings << build_config_item(sub_handle, sub_type, sub_default)
151
+ break unless @prompt.yes?('Do you want to add another sub-setting?')
152
+ end
153
+ [sub_settings]
154
+ else
155
+ default = @prompt.ask("Enter the default value for #{type}:", default: '')
156
+ [default]
157
+ end
158
+ end
159
+
160
+ def show_current_settings(settings)
161
+ if settings.empty?
162
+ puts "No current settings."
163
+ else
164
+ puts "Current settings:"
165
+ settings.each do |setting|
166
+ puts "Handle: #{setting['handle']}, Type: #{setting['type']}, Default: #{setting['default']}"
167
+ if setting['options']
168
+ puts "Options: #{setting['options'].map { |opt| "#{opt['value']}(#{opt['handle']})" }.join(', ')}"
169
+ end
170
+ if setting['min'] && setting['max'] && setting['step']
171
+ puts "Range: Min: #{setting['min']}, Max: #{setting['max']}, Step: #{setting['step']}"
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def build_config_item(handle, type, *args)
178
+ case type
179
+ when 'text', 'richtext'
180
+ {
181
+ "type" => type,
182
+ "handle" => handle,
183
+ "default" => args[0] || ""
184
+ }
185
+ when 'select', 'radio'
186
+ {
187
+ "type" => type,
188
+ "handle" => handle,
189
+ "default" => args[0] || "",
190
+ "options" => args[1] || []
191
+ }
192
+ when 'range'
193
+ {
194
+ "type" => "range",
195
+ "handle" => handle,
196
+ "min" => args[0],
197
+ "max" => args[1],
198
+ "step" => args[2]
199
+ }
200
+ when 'checkbox'
201
+ {
202
+ "type" => "checkbox",
203
+ "handle" => handle,
204
+ "default" => args[0]
205
+ }
206
+ when 'subcategory'
207
+ {
208
+ "type" => "subcategory",
209
+ "handle" => handle,
210
+ "settings" => args[0]
211
+ }
212
+ else
213
+ {
214
+ "type" => type,
215
+ "handle" => handle,
216
+ "default" => args[0] || ""
217
+ }
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'build/json'
2
+ require_relative 'build/liquid'
3
+ require_relative 'build/section'
4
+ require_relative 'build/settings'
5
+
6
+ module Shoperb
7
+ module Theme
8
+ module Editor
9
+ module_function
10
+
11
+ def create_section(section_handle, section_name)
12
+ section = Build::Section.new(section_handle, section_name)
13
+ section.create_or_update_json_file
14
+ section.create_or_update_liquid_file
15
+ end
16
+
17
+ def create_or_update_json_file(section_handle)
18
+ Build::Json.new(section_handle).create_or_update_json_file
19
+ end
20
+
21
+ def create_or_update_liquid_file(section_handle)
22
+ Build::Liquid.new(section_handle).create_or_update_liquid_file
23
+ end
24
+
25
+ def add_settings(section_handle)
26
+ json_manager = Build::Json.new(section_handle)
27
+ json_manager.create_or_update_json_file
28
+ Build::Settings.new(section_handle, json_manager).add_settings
29
+ end
30
+
31
+ def modify_setting(section_handle, setting_handle)
32
+ json_manager = Build::Json.new(section_handle)
33
+ json_manager.create_or_update_json_file
34
+ Build::Settings.new(section_handle, json_manager).modify_setting(setting_handle)
35
+ end
36
+
37
+ def delete_settings(section_handle)
38
+ json_manager = Build::Json.new(section_handle)
39
+ json_manager.create_or_update_json_file
40
+ Build::Settings.new(section_handle, json_manager).delete_settings
41
+ end
42
+
43
+ def remove_section(section_handle)
44
+ config_dir = File.join(Dir.pwd, "config/sections")
45
+ liquid_dir = File.join(Dir.pwd, "sections")
46
+
47
+ json_file_path = File.join(config_dir, "#{section_handle}.json")
48
+ liquid_file_path = File.join(liquid_dir, "#{section_handle}.liquid")
49
+
50
+ # Remove JSON config file
51
+ if File.exist?(json_file_path)
52
+ File.delete(json_file_path)
53
+ else
54
+ puts "JSON config file for section '#{section_handle}' not found."
55
+ end
56
+
57
+ # Remove Liquid template file
58
+ if File.exist?(liquid_file_path)
59
+ File.delete(liquid_file_path)
60
+ else
61
+ puts "Liquid template file for section '#{section_handle}' not found."
62
+ end
63
+
64
+ puts "Section '#{section_handle}' has been removed successfully."
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,98 @@
1
+ module Shoperb module Theme module Editor
2
+ class Configuration < HashWithIndifferentAccess
3
+
4
+ OPTIONS = {
5
+ "oauth-site" => "Your shoperb shop domain",
6
+ "oauth-redirect-uri" => "Url shoperb will redirect to after granting access",
7
+ "verbose" => "Enable verbose mode",
8
+ "port" => "Port you want your local shoperb theme instance to run at",
9
+ "server" => "Shoperb url & protocol for oauth to run against",
10
+ "preset" => "Theme preset to use",
11
+ }
12
+
13
+ QUESTION = {
14
+ "oauth-site" => "Insert Shoperb domain"
15
+ }.with_indifferent_access
16
+
17
+ HARDCODED = {
18
+ "oauth-client-id" => "jsikb3aoa42w1qkybugvj3t3l6tef2y",
19
+ "oauth-client-secret" => "np47hizd5b9v5749psdyklybt11ygr4",
20
+ "oauth-redirect-uri" => "http://localhost:4000/callback"
21
+ }.with_indifferent_access
22
+
23
+ DEFAULTS = {
24
+ "oauth-cache" => {}.with_indifferent_access,
25
+ "port" => "4000",
26
+ "verbose" => false,
27
+ "server" => {
28
+ "url" => "shoperb.app",
29
+ "protocol" => "https"
30
+ },
31
+ "preset" => nil
32
+ }.with_indifferent_access
33
+
34
+ ASKS = {
35
+ "oauth-password" => -> {
36
+ $stdin.noecho(&:gets).tap do
37
+ puts ""
38
+ end
39
+ }
40
+ }.with_indifferent_access
41
+
42
+ attr_accessor :file
43
+
44
+ def initialize options={}, *args
45
+ super()
46
+
47
+ self.file = Utils.base + ".shoperb"
48
+
49
+ begin
50
+ FileUtils.send(args.any? ? :mkdir : :mkdir_p, File.dirname(self.file))
51
+ rescue Errno::EEXIST
52
+ raise Error.new("Folder #{File.dirname(self.file)} already exists")
53
+ end
54
+
55
+ merge!(conf)
56
+ merge!(options)
57
+ merge!(HARDCODED)
58
+ end
59
+
60
+ def save
61
+ Sequel::Model.db&.disconnect
62
+ Logger.notify "Saving configuration to #{file.basename}" do
63
+ Utils.write_file(file) {
64
+ require "active_support/json/encoding"
65
+ JSON.pretty_generate(self.except(*HARDCODED.keys).as_json)
66
+ }
67
+ end
68
+ end
69
+
70
+ def [] name
71
+ has_key?(name) ? super(name) : (self[name] = ask(name))
72
+ end
73
+
74
+ def ask name
75
+ default = DEFAULTS[name].presence
76
+ if question = QUESTION[name]
77
+ Logger.info "#{question} #{"(Default is '#{default}') " if default}: "
78
+ # $stdin.gets avoids problem with ARGV & gets
79
+ ASKS.fetch(name, -> { $stdin.gets })[].strip.presence || default
80
+ end || default
81
+ end
82
+
83
+ def reset *names
84
+ names.each { |name| self[name] = DEFAULTS[name] }
85
+ end
86
+
87
+ def destroy
88
+ Logger.notify "Deleting configuration at #{file}" do
89
+ File.delete(file)
90
+ end if File.exist?(file)
91
+ end
92
+
93
+ def conf path=self.file
94
+ File.exist?(path) && (content = File.read(path).presence)? JSON.parse(content) || {} : {}
95
+ end
96
+
97
+ end
98
+ end end end
@@ -0,0 +1,35 @@
1
+ require "raven"
2
+
3
+ Raven.configure do |config|
4
+ config.dsn = 'https://71ae0199b499419da0ffe2a9695871ca:bc1386ebe0d4486aac1e1b41c3011f41@sentry.io/1273398'
5
+ config.excluded_exceptions += [
6
+ "Liquid::FileSystemError",
7
+ "Liquid::ArgumentError",
8
+ "Liquid::SyntaxError",
9
+ "Liquid::UndefinedFilter",
10
+ "Liquid::UndefinedDropMethod",
11
+ "Liquid::UndefinedVariable",
12
+ "Shoperb::Theme::Editor::Error"
13
+ ]
14
+ end
15
+
16
+ module Shoperb module Theme module Editor
17
+ class Error < Exception
18
+ def self.report exception
19
+ log(exception)
20
+ Raven.capture_exception(exception)
21
+ end
22
+
23
+ def self.report_rack exception, env
24
+ log(exception)
25
+ Raven::Rack.capture_exception(exception, env)
26
+ end
27
+
28
+ def self.log exception
29
+ display = "\r#{exception.class.name}"
30
+ display += " => #{exception.message}" if exception.message.presence
31
+ puts exception.backtrace
32
+ Logger.error "#{display}\n"
33
+ end
34
+ end
35
+ end end end
@@ -0,0 +1,17 @@
1
+ class Array
2
+ def exists?
3
+ size>0
4
+ end
5
+
6
+ def sorted
7
+ if first.respond_to?(:id)
8
+ self.sort_by(&:id)
9
+ else
10
+ self
11
+ end
12
+ end
13
+
14
+ def active
15
+ filter{|item| item.state == 'active'}
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ require 'bigdecimal/util'
2
+
3
+ class NilClass
4
+ def to_d
5
+ BigDecimal("0")
6
+ end
7
+ end
@@ -0,0 +1,82 @@
1
+ require 'sequel'
2
+ require 'sequel/adapters/sqlite.rb'
3
+
4
+ Sequel::Model.require_modification = false
5
+
6
+ class Sequel::Dataset
7
+ def preload *args, **args2
8
+ self
9
+ end
10
+
11
+ def includes *args, **args2
12
+ self
13
+ end
14
+
15
+ def reorder(dir)
16
+ order(dir)
17
+ end
18
+
19
+ def size(*args)
20
+ count(*args)
21
+ end
22
+
23
+ def count1(*args)
24
+ args = ["all"] if args == []
25
+ super(*args)
26
+ end
27
+
28
+ def exists?
29
+ exists
30
+ end
31
+
32
+ def sorted
33
+ self
34
+ end
35
+ end
36
+
37
+ class Sequel::SQLite::Database
38
+ def execute(sql, opts=OPTS, &block)
39
+ _execute(:select, sql, opts, &block)
40
+ rescue=>e
41
+ return if e.to_s.include?("no such table")
42
+ raise e
43
+ end
44
+ end
45
+ class Sequel::SQLite::Dataset
46
+ def to_liquid
47
+ to_a.map(&:to_liquid)
48
+ end
49
+ end
50
+
51
+ class Sequel::Model
52
+ def cache_key
53
+ if id.nil?
54
+ "#{model.table_name}/new"
55
+ else
56
+ if respond_to?(:updated_at)
57
+ "#{model.table_name}/#{id}-#{updated_at}"
58
+ else
59
+ "#{model.table_name}/#{id}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ module ShoperbLiquid
67
+ class CollectionDrop
68
+ def pagy(collection, vars = {})
69
+ vars[:count] = collection.count
70
+ vars[:limit] ||= pagy_get_limit(vars)
71
+ vars[:page] ||= pagy_get_page(vars)
72
+ pagy = Pagy.new(**vars)
73
+
74
+ [pagy, pagy_get_items(collection, pagy)]
75
+ end
76
+ end
77
+ class ArrayDrop
78
+ def to_ary
79
+ collection
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'ext/nil_class'
2
+ require_relative 'ext/array'
@@ -0,0 +1,87 @@
1
+ module Shoperb
2
+ module Theme
3
+ module Editor
4
+ class Init
5
+
6
+ Editor.autoload_all self, "init"
7
+
8
+ def self.available_templates
9
+ ["theme-blank"]
10
+ end
11
+
12
+ def initialize template, handle
13
+ template ||= "blank"
14
+ unless self.class.available_templates.include?(template)
15
+ raise Error.new("No such template, possible options are #{self.class.available_templates.map(&:inspect).to_sentence}")
16
+ end
17
+
18
+ url = URI.parse("https://github.com/shoperb/#{template}/archive/refs/heads/main.zip")
19
+ tmp_zip = nil
20
+ Logger.notify "Downloading #{template.inspect} template" do
21
+ tmp_zip = download_to_tempfile(url, template: template)
22
+ end
23
+
24
+ Logger.notify "Extracting template into theme folder" do
25
+ extract_zip_to_base(tmp_zip.path)
26
+ end
27
+ ensure
28
+ Utils.rm_tempfile(tmp_zip) if defined?(tmp_zip)
29
+ end
30
+
31
+ private
32
+
33
+ def download_to_tempfile(uri, limit = 5, template: "theme-blank")
34
+ require "net/http"
35
+ require "uri"
36
+ require "tempfile"
37
+
38
+ raise Error.new("Too many HTTP redirects while downloading template") if limit <= 0
39
+
40
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
41
+ request = Net::HTTP::Get.new(uri.request_uri)
42
+ request["User-Agent"] = "shoperb-theme-editor"
43
+ request["Accept"] = "application/zip, application/octet-stream"
44
+
45
+ response = http.request(request)
46
+ case response
47
+ when Net::HTTPRedirection
48
+ location = response["location"]
49
+ raise Error.new("Redirect without Location header") unless location
50
+ new_uri = URI.parse(location)
51
+ new_uri = URI.join("#{uri.scheme}://#{uri.host}", location) unless new_uri.host
52
+ return download_to_tempfile(new_uri, limit - 1)
53
+ when Net::HTTPSuccess
54
+ file = Tempfile.new([template.gsub(/[^a-zA-Z0-9\-_.]/, "-"), ".zip"])
55
+ if response.body.nil? || response.body.empty?
56
+ # Try reading body in chunks if not already loaded
57
+ response.read_body { |chunk| file.write(chunk) }
58
+ else
59
+ file.write(response.body)
60
+ end
61
+ file.flush
62
+ file.rewind
63
+ raise Error.new("Downloaded archive has zero size") if File.size(file.path) == 0
64
+ return file
65
+ else
66
+ raise Error.new("Failed to download template: #{response.code} #{response.message}")
67
+ end
68
+ end
69
+ end
70
+
71
+ def extract_zip_to_base(zip_path)
72
+ require "zip"
73
+ Zip::File.open(zip_path) do |zip|
74
+ zip.each do |entry|
75
+ next if entry.name_is_directory?
76
+ relative = entry.name.split('/', 2)[1] || entry.name
77
+ destination = (Utils.base + relative).to_s
78
+ FileUtils.mkdir_p(File.dirname(destination))
79
+ entry.extract(destination) { true }
80
+ end
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
1
+ require "colorize"
2
+ require "io/console"
3
+ module Shoperb module Theme module Editor
4
+ module Logger
5
+ extend self
6
+
7
+ LEVELS = {
8
+ "DEBUG" => { color: :green },
9
+ "INFO" => { mode: :bold },
10
+ "ERROR" => { color: :red }
11
+ }
12
+
13
+ mattr_accessor :logger
14
+ self.logger = ::Logger.new(STDOUT).tap do |log|
15
+ log.formatter = proc do |severity, datetime, progname, msg|
16
+ msg.colorize(LEVELS[severity])
17
+ end
18
+ end
19
+
20
+ LEVELS.each do |name, opts|
21
+ define_method name.downcase do |message, &block|
22
+ Array(message).each do |msg|
23
+ logger.send(name.downcase, msg, &block)
24
+ end
25
+ end
26
+ end
27
+
28
+ alias :success :debug
29
+
30
+ def notify msg
31
+ result = nil
32
+ self.info "#{fill(msg)}".rstrip
33
+ begin
34
+ self.info "\r"
35
+ result = yield
36
+ rescue Exception => e
37
+ self.error fill(msg, " [FAILED]")
38
+ else
39
+ self.success fill(msg, " [OK]")
40
+ ensure
41
+ self.info "\n"
42
+ end
43
+ raise e if e
44
+ result
45
+ end
46
+
47
+ def cols
48
+ @cols ||= begin
49
+ IO.console.winsize[1] - 1
50
+ end
51
+ end
52
+
53
+ def fill msg, ending=""
54
+ "#{msg.ljust(cols)[0..cols-ending.length]}#{ending}"
55
+ end
56
+
57
+ end
58
+ end end end