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.
- checksums.yaml +7 -0
- data/CONTRIBUTING.md +40 -0
- data/LICENSE.md +21 -0
- data/README.md +124 -0
- data/bin/shoperb +313 -0
- data/lib/shoperb_theme_editor/api/server.rb +10 -0
- data/lib/shoperb_theme_editor/api/views/callback.erb +53 -0
- data/lib/shoperb_theme_editor/api.rb +264 -0
- data/lib/shoperb_theme_editor/build/json.rb +167 -0
- data/lib/shoperb_theme_editor/build/liquid.rb +38 -0
- data/lib/shoperb_theme_editor/build/section.rb +51 -0
- data/lib/shoperb_theme_editor/build/settings.rb +224 -0
- data/lib/shoperb_theme_editor/build.rb +68 -0
- data/lib/shoperb_theme_editor/configuration.rb +98 -0
- data/lib/shoperb_theme_editor/error.rb +35 -0
- data/lib/shoperb_theme_editor/ext/array.rb +17 -0
- data/lib/shoperb_theme_editor/ext/nil_class.rb +7 -0
- data/lib/shoperb_theme_editor/ext/sequel.rb +82 -0
- data/lib/shoperb_theme_editor/ext.rb +2 -0
- data/lib/shoperb_theme_editor/init.rb +87 -0
- data/lib/shoperb_theme_editor/logger.rb +58 -0
- data/lib/shoperb_theme_editor/mounter/models/address.rb +65 -0
- data/lib/shoperb_theme_editor/mounter/models/attribute.rb +11 -0
- data/lib/shoperb_theme_editor/mounter/models/attribute_key.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/base.rb +196 -0
- data/lib/shoperb_theme_editor/mounter/models/blog_category.rb +47 -0
- data/lib/shoperb_theme_editor/mounter/models/blog_post.rb +45 -0
- data/lib/shoperb_theme_editor/mounter/models/brand.rb +11 -0
- data/lib/shoperb_theme_editor/mounter/models/cart.rb +35 -0
- data/lib/shoperb_theme_editor/mounter/models/cart_item.rb +71 -0
- data/lib/shoperb_theme_editor/mounter/models/category.rb +99 -0
- data/lib/shoperb_theme_editor/mounter/models/collection.rb +40 -0
- data/lib/shoperb_theme_editor/mounter/models/country.rb +18 -0
- data/lib/shoperb_theme_editor/mounter/models/currency.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/custom_field.rb +22 -0
- data/lib/shoperb_theme_editor/mounter/models/customer.rb +77 -0
- data/lib/shoperb_theme_editor/mounter/models/customer_customer_group.rb +12 -0
- data/lib/shoperb_theme_editor/mounter/models/customer_group.rb +13 -0
- data/lib/shoperb_theme_editor/mounter/models/customer_subscription.rb +40 -0
- data/lib/shoperb_theme_editor/mounter/models/customer_subscription_plan.rb +32 -0
- data/lib/shoperb_theme_editor/mounter/models/discount.rb +40 -0
- data/lib/shoperb_theme_editor/mounter/models/discount_variant.rb +15 -0
- data/lib/shoperb_theme_editor/mounter/models/image.rb +51 -0
- data/lib/shoperb_theme_editor/mounter/models/language.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/link.rb +61 -0
- data/lib/shoperb_theme_editor/mounter/models/media_file.rb +19 -0
- data/lib/shoperb_theme_editor/mounter/models/menu.rb +21 -0
- data/lib/shoperb_theme_editor/mounter/models/meta.rb +10 -0
- data/lib/shoperb_theme_editor/mounter/models/news_item.rb +11 -0
- data/lib/shoperb_theme_editor/mounter/models/order.rb +133 -0
- data/lib/shoperb_theme_editor/mounter/models/order_item.rb +137 -0
- data/lib/shoperb_theme_editor/mounter/models/order_item_attribute.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/order_return.rb +42 -0
- data/lib/shoperb_theme_editor/mounter/models/order_return_item.rb +29 -0
- data/lib/shoperb_theme_editor/mounter/models/order_return_item_entity.rb +25 -0
- data/lib/shoperb_theme_editor/mounter/models/order_return_parcel.rb +20 -0
- data/lib/shoperb_theme_editor/mounter/models/page.rb +26 -0
- data/lib/shoperb_theme_editor/mounter/models/payment_card.rb +21 -0
- data/lib/shoperb_theme_editor/mounter/models/payment_method.rb +67 -0
- data/lib/shoperb_theme_editor/mounter/models/payment_provider.rb +23 -0
- data/lib/shoperb_theme_editor/mounter/models/product.rb +144 -0
- data/lib/shoperb_theme_editor/mounter/models/product_attribute.rb +32 -0
- data/lib/shoperb_theme_editor/mounter/models/product_search.rb +53 -0
- data/lib/shoperb_theme_editor/mounter/models/product_type.rb +21 -0
- data/lib/shoperb_theme_editor/mounter/models/review.rb +38 -0
- data/lib/shoperb_theme_editor/mounter/models/search.rb +11 -0
- data/lib/shoperb_theme_editor/mounter/models/shipping_method.rb +39 -0
- data/lib/shoperb_theme_editor/mounter/models/shop.rb +58 -0
- data/lib/shoperb_theme_editor/mounter/models/state.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/theme.rb +89 -0
- data/lib/shoperb_theme_editor/mounter/models/variant.rb +96 -0
- data/lib/shoperb_theme_editor/mounter/models/variant_attribute.rb +46 -0
- data/lib/shoperb_theme_editor/mounter/models/vendor.rb +38 -0
- data/lib/shoperb_theme_editor/mounter/server/assets.rb +35 -0
- data/lib/shoperb_theme_editor/mounter/server/defaults.rb +44 -0
- data/lib/shoperb_theme_editor/mounter/server/exception_handler.rb +22 -0
- data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_footer.liquid +0 -0
- data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_header.liquid +0 -0
- data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_stylesheets.liquid +3 -0
- data/lib/shoperb_theme_editor/mounter/server/renderer.rb +166 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/cart.rb +127 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/dummy.rb +34 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/locale.rb +31 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/pages.rb +33 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/search.rb +18 -0
- data/lib/shoperb_theme_editor/mounter/server/routes.rb +366 -0
- data/lib/shoperb_theme_editor/mounter/server/routes_helper.rb +278 -0
- data/lib/shoperb_theme_editor/mounter/server.rb +66 -0
- data/lib/shoperb_theme_editor/mounter.rb +30 -0
- data/lib/shoperb_theme_editor/os.rb +13 -0
- data/lib/shoperb_theme_editor/package.rb +81 -0
- data/lib/shoperb_theme_editor/sync/images.rb +69 -0
- data/lib/shoperb_theme_editor/sync/pagination.rb +52 -0
- data/lib/shoperb_theme_editor/sync.rb +229 -0
- data/lib/shoperb_theme_editor/translations.rb +22 -0
- data/lib/shoperb_theme_editor/utils.rb +50 -0
- data/lib/shoperb_theme_editor.rb +159 -0
- data/shoperb_theme_editor.gemspec +60 -0
- 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,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,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
|