disco_app 0.8.5 → 0.8.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/disco_app/components/filterable_shop_list.js.jsx +65 -0
- data/app/assets/javascripts/disco_app/components/shop_filter_tab.js.jsx +34 -0
- data/app/assets/javascripts/disco_app/components/shop_filter_tabs.js.jsx +21 -0
- data/app/assets/javascripts/disco_app/components/shop_list.js.jsx +140 -0
- data/app/assets/javascripts/disco_app/components/shop_row.js.jsx +27 -0
- data/app/controllers/disco_app/admin/app_settings_controller.rb +3 -0
- data/app/controllers/disco_app/admin/application_controller.rb +3 -0
- data/app/controllers/disco_app/admin/concerns/app_settings_controller.rb +24 -0
- data/app/controllers/disco_app/admin/concerns/authenticated_controller.rb +20 -0
- data/app/controllers/disco_app/admin/concerns/shops_controller.rb +7 -0
- data/app/controllers/disco_app/admin/resources/shops_controller.rb +3 -0
- data/app/controllers/disco_app/admin/shops_controller.rb +3 -0
- data/app/controllers/disco_app/charges_controller.rb +1 -1
- data/app/controllers/disco_app/concerns/app_proxy_controller.rb +40 -0
- data/app/controllers/disco_app/concerns/authenticated_controller.rb +42 -0
- data/app/controllers/disco_app/concerns/carrier_request_controller.rb +21 -0
- data/app/controllers/disco_app/install_controller.rb +1 -1
- data/app/models/disco_app/app_settings.rb +3 -0
- data/app/models/disco_app/concerns/app_settings.rb +7 -0
- data/app/models/disco_app/concerns/shop.rb +5 -0
- data/app/models/disco_app/concerns/{synchronises_with_shopify.rb → synchronises.rb} +16 -3
- data/app/resources/disco_app/admin/resources/concerns/shop_resource.rb +46 -0
- data/app/resources/disco_app/admin/resources/shop_resource.rb +4 -0
- data/app/views/disco_app/admin/app_settings/edit.html.erb +5 -0
- data/app/views/disco_app/admin/shops/index.html.erb +12 -0
- data/app/views/layouts/admin/_navbar.html.erb +24 -0
- data/app/views/layouts/admin.html.erb +27 -0
- data/config/routes.rb +15 -0
- data/db/migrate/20150525000000_create_shops_if_not_existent.rb +1 -1
- data/db/migrate/20160112233706_create_disco_app_sessions.rb +2 -2
- data/db/migrate/20160223111044_create_disco_app_settings.rb +8 -0
- data/lib/disco_app/engine.rb +1 -0
- data/lib/disco_app/version.rb +1 -1
- data/lib/generators/disco_app/adminify/adminify_generator.rb +35 -0
- data/lib/generators/disco_app/disco_app_generator.rb +1 -0
- data/lib/generators/disco_app/templates/controllers/home_controller.rb +2 -2
- data/test/controllers/disco_app/admin/shops_controller_test.rb +54 -0
- data/test/dummy/app/controllers/disco_app/admin/shops_controller.rb +8 -0
- data/test/dummy/app/controllers/home_controller.rb +2 -2
- data/test/dummy/app/controllers/proxy_controller.rb +1 -1
- data/test/dummy/app/jobs/products_create_job.rb +7 -0
- data/test/dummy/app/jobs/products_delete_job.rb +7 -0
- data/test/dummy/app/jobs/products_update_job.rb +7 -0
- data/test/dummy/app/models/product.rb +6 -0
- data/test/dummy/config/database.codeship.yml +23 -0
- data/test/dummy/config/database.yml +8 -13
- data/test/dummy/db/migrate/20160307182229_create_products.rb +11 -0
- data/test/dummy/db/schema.rb +27 -10
- data/test/fixtures/products.yml +4 -0
- data/test/fixtures/webhooks/product_created.json +167 -0
- data/test/fixtures/webhooks/product_deleted.json +3 -0
- data/test/fixtures/webhooks/product_updated.json +167 -0
- data/test/integration/synchronises_test.rb +55 -0
- metadata +69 -9
- data/app/controllers/disco_app/app_proxy_controller.rb +0 -42
- data/app/controllers/disco_app/authenticated_controller.rb +0 -44
- data/app/controllers/disco_app/carrier_request_controller.rb +0 -23
- data/test/integration/navigation_test.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56c1ba4929abd6bf9f7539edd256f66061ed90ddd974f583b229c5803cc71fdc
|
4
|
+
data.tar.gz: 75f3dcee0e4087e98c2ec9933211cdc5d5ce586a5d674f6e1507e7134267f3bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae82c3a87f6b50d7c6cf878bf2eb34b047fd2d180e2609e4b15226a85dd742a973c0454363f8d4eb64ba4ea65af23b6adce122f1e4160290c9cab524a38fe45f
|
7
|
+
data.tar.gz: 6d184f92b1aed8bc41c8733b0d67869b7e5f70c82be4462657efb731c49c138bbce16c3f83322f82acfc0723f3d0d58d30c3ee5309ed6f69df19c01515bb9e28
|
@@ -0,0 +1,65 @@
|
|
1
|
+
var FilterableShopList = React.createClass({
|
2
|
+
|
3
|
+
getDefaultProps: function() {
|
4
|
+
return {
|
5
|
+
filterTabs: [
|
6
|
+
{ label: 'All Shops', filter: {} },
|
7
|
+
{ label: 'Never Installed', filter: { 'filter[status]': 'never_installed' } },
|
8
|
+
{ label: 'Awaiting Install', filter: { 'filter[status]': 'awaiting_install' } },
|
9
|
+
{ label: 'Installing', filter: { 'filter[status]': 'installing' } },
|
10
|
+
{ label: 'Installed', filter: { 'filter[status]': 'installed' } },
|
11
|
+
{ label: 'Awaiting Uninstall', filter: { 'filter[status]': 'awaiting_uninstall' } },
|
12
|
+
{ label: 'Uninstalling', filter: { 'filter[status]': 'uninstalling' } },
|
13
|
+
{ label: 'Uninstalled', filter: { 'filter[status]': 'uninstalled' } }
|
14
|
+
],
|
15
|
+
availableFilters: {
|
16
|
+
'filter[status]': {
|
17
|
+
getLabel: function(value) {
|
18
|
+
return 'Status is ' + value;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
},
|
24
|
+
|
25
|
+
getInitialState: function() {
|
26
|
+
return {
|
27
|
+
filter: {}
|
28
|
+
};
|
29
|
+
},
|
30
|
+
|
31
|
+
onFilterReplace: function(filter) {
|
32
|
+
this.setState({
|
33
|
+
filter: filter
|
34
|
+
});
|
35
|
+
},
|
36
|
+
|
37
|
+
onFilterSet: function(name, value) {
|
38
|
+
this.onFiltersSet([{
|
39
|
+
name: name,
|
40
|
+
value: value
|
41
|
+
}]);
|
42
|
+
},
|
43
|
+
|
44
|
+
onFiltersSet: function(filters) {
|
45
|
+
var nextFilter = $.extend({}, this.state.filter);
|
46
|
+
filters.forEach(function(filter) {
|
47
|
+
if(!filter.value) {
|
48
|
+
delete nextFilter[filter.name];
|
49
|
+
} else {
|
50
|
+
nextFilter[filter.name] = filter.value;
|
51
|
+
}
|
52
|
+
});
|
53
|
+
this.onFilterReplace(nextFilter);
|
54
|
+
},
|
55
|
+
|
56
|
+
render: function() {
|
57
|
+
return (
|
58
|
+
<div className="next-card">
|
59
|
+
<ShopFilterTabs filterTabs={this.props.filterTabs} filter={this.state.filter} onFilterReplace={this.onFilterReplace} />
|
60
|
+
<ShopList shopsUrl={this.props.shopsUrl} editShopUrl={this.props.editShopUrl} filter={this.state.filter} />
|
61
|
+
</div>
|
62
|
+
);
|
63
|
+
}
|
64
|
+
|
65
|
+
});
|
@@ -0,0 +1,34 @@
|
|
1
|
+
var ShopFilterTab = React.createClass({
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Handle a click event on the individual tab link.
|
5
|
+
* @param e
|
6
|
+
*/
|
7
|
+
handleClick: function(e) {
|
8
|
+
e.preventDefault();
|
9
|
+
|
10
|
+
// Don't do anything if this is already the currently selected tab.
|
11
|
+
if(this.isActive()) return;
|
12
|
+
|
13
|
+
this.props.onFilterTabSelected(this.props.filterTab);
|
14
|
+
},
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Return true if this is the currently active tab, determined by whether the filter values for this tab match the
|
18
|
+
* currently active filter.
|
19
|
+
* @returns {boolean}
|
20
|
+
*/
|
21
|
+
isActive: function() {
|
22
|
+
return (JSON.stringify(this.props.filterTab.filter) == JSON.stringify(this.props.filter));
|
23
|
+
},
|
24
|
+
|
25
|
+
render: function() {
|
26
|
+
var tabClassName = this.isActive() ? 'next-tab next-tab--is-active' : 'next-tab';
|
27
|
+
return (
|
28
|
+
<li role="presentation">
|
29
|
+
<a href="#" className={tabClassName} onClick={this.handleClick}>{this.props.filterTab.label}</a>
|
30
|
+
</li>
|
31
|
+
)
|
32
|
+
}
|
33
|
+
|
34
|
+
});
|
@@ -0,0 +1,21 @@
|
|
1
|
+
var ShopFilterTabs = React.createClass({
|
2
|
+
|
3
|
+
onFilterTabSelected: function(filterTab) {
|
4
|
+
this.props.onFilterReplace(filterTab.filter);
|
5
|
+
},
|
6
|
+
|
7
|
+
render: function() {
|
8
|
+
var filterTabNodes = this.props.filterTabs.map(function(filterTab) {
|
9
|
+
return (
|
10
|
+
<ShopFilterTab key={filterTab.label} filter={this.props.filter} filterTab={filterTab} onFilterTabSelected={this.onFilterTabSelected} />
|
11
|
+
)
|
12
|
+
}, this);
|
13
|
+
|
14
|
+
return (
|
15
|
+
<ul className="next-tab__list">
|
16
|
+
{filterTabNodes}
|
17
|
+
</ul>
|
18
|
+
)
|
19
|
+
}
|
20
|
+
|
21
|
+
});
|
@@ -0,0 +1,140 @@
|
|
1
|
+
var ShopList = React.createClass({
|
2
|
+
|
3
|
+
getDefaultProps: function() {
|
4
|
+
return {
|
5
|
+
pageSize: 50
|
6
|
+
}
|
7
|
+
},
|
8
|
+
|
9
|
+
getInitialState: function() {
|
10
|
+
return {
|
11
|
+
shops: [],
|
12
|
+
page: 1
|
13
|
+
}
|
14
|
+
},
|
15
|
+
|
16
|
+
componentDidMount: function() {
|
17
|
+
this.getShops();
|
18
|
+
$(document).on('shops.changed', this.onShopsChanged);
|
19
|
+
},
|
20
|
+
|
21
|
+
componentWillUnmount: function() {
|
22
|
+
$(document).off('shops.changed', this.onShopsChanged);
|
23
|
+
},
|
24
|
+
|
25
|
+
componentDidUpdate: function (prevProps, prevState) {
|
26
|
+
if(prevProps.filter != this.props.filter) {
|
27
|
+
this.getShops();
|
28
|
+
}
|
29
|
+
if(prevState.page != this.state.page) {
|
30
|
+
this.getShops();
|
31
|
+
}
|
32
|
+
},
|
33
|
+
|
34
|
+
onPageChanged: function(increment) {
|
35
|
+
this.setState({page: this.state.page + increment});
|
36
|
+
},
|
37
|
+
|
38
|
+
onShopsChanged: function() {
|
39
|
+
this.setState({page: 1});
|
40
|
+
this.getShops();
|
41
|
+
},
|
42
|
+
|
43
|
+
getShops: function() {
|
44
|
+
//ShopifyApp.Bar.loadingOn();
|
45
|
+
|
46
|
+
// Set up request data with filter and pagination parameters.
|
47
|
+
var data = $.extend({
|
48
|
+
'page[size]': this.props.pageSize,
|
49
|
+
'page[number]': this.state.page
|
50
|
+
}, this.props.filter);
|
51
|
+
|
52
|
+
// Make the request to fetch the order list.
|
53
|
+
$.getJSON(this.props.shopsUrl, data, function(result) {
|
54
|
+
if(this.isMounted()) {
|
55
|
+
this.setState({
|
56
|
+
shops: result.data
|
57
|
+
});
|
58
|
+
}
|
59
|
+
}.bind(this))
|
60
|
+
.always(function() {
|
61
|
+
//ShopifyApp.Bar.loadingOff();
|
62
|
+
});
|
63
|
+
},
|
64
|
+
|
65
|
+
render: function() {
|
66
|
+
var shopRows = this.state.shops.map(function(shop) {
|
67
|
+
return (
|
68
|
+
<ShopRow shop={shop} editShopUrl={this.props.editShopUrl} key={shop.id} />
|
69
|
+
)
|
70
|
+
}.bind(this));
|
71
|
+
|
72
|
+
return (
|
73
|
+
<div>
|
74
|
+
<div className="next-card__section">
|
75
|
+
<table className="table-hover expanded">
|
76
|
+
<thead>
|
77
|
+
<tr>
|
78
|
+
<th>ID</th>
|
79
|
+
<th>Shopify Domain</th>
|
80
|
+
<th>Status</th>
|
81
|
+
<th>Email</th>
|
82
|
+
<th>Country Name</th>
|
83
|
+
<th>Currency</th>
|
84
|
+
<th>Domain</th>
|
85
|
+
<th>Plan</th>
|
86
|
+
<th>Created</th>
|
87
|
+
<th>Installed Duration</th>
|
88
|
+
</tr>
|
89
|
+
</thead>
|
90
|
+
<tbody>
|
91
|
+
{shopRows}
|
92
|
+
</tbody>
|
93
|
+
</table>
|
94
|
+
</div>
|
95
|
+
<div className="next-card__section">
|
96
|
+
<div className="next-grid next-grid--no-padding next-grid--center-aligned">
|
97
|
+
<div className="next-grid__cell next-grid__cell--no-flex">
|
98
|
+
<ShopList.Paginator page={this.state.page} pageSize={this.props.pageSize} items={this.state.shops} onPageChanged={this.onPageChanged} />
|
99
|
+
</div>
|
100
|
+
</div>
|
101
|
+
</div>
|
102
|
+
</div>
|
103
|
+
)
|
104
|
+
}
|
105
|
+
|
106
|
+
});
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Paginator.
|
110
|
+
*/
|
111
|
+
ShopList.Paginator = React.createClass({
|
112
|
+
|
113
|
+
handlePreviousClick: function(e) {
|
114
|
+
this.props.onPageChanged(-1);
|
115
|
+
},
|
116
|
+
|
117
|
+
handleNextClick: function(e) {
|
118
|
+
this.props.onPageChanged(1);
|
119
|
+
},
|
120
|
+
|
121
|
+
render: function() {
|
122
|
+
return (
|
123
|
+
<div className="btn-group" role="group">
|
124
|
+
<ShopList.PaginatorButton label="‹" isDisabled={this.props.page < 2} onClick={this.handlePreviousClick} />
|
125
|
+
<ShopList.PaginatorButton label="›" isDisabled={this.props.items.length < this.props.pageSize} onClick={this.handleNextClick} />
|
126
|
+
</div>
|
127
|
+
)
|
128
|
+
}
|
129
|
+
|
130
|
+
});
|
131
|
+
|
132
|
+
/**
|
133
|
+
* Individual button within a paginator.
|
134
|
+
*/
|
135
|
+
ShopList.PaginatorButton = (props) => {
|
136
|
+
var className = 'btn btn-default' + (props.isDisabled ? ' disabled' : '');
|
137
|
+
return (
|
138
|
+
<button className={className} disabled={props.isDisabled} onClick={props.onClick}>{props.label}</button>
|
139
|
+
)
|
140
|
+
};
|
@@ -0,0 +1,27 @@
|
|
1
|
+
var ShopRow = (props) => {
|
2
|
+
|
3
|
+
var shop = props.shop,
|
4
|
+
editShopUrl = props.editShopUrl.replace(':id', shop.id),
|
5
|
+
shopifyDomain = shop.attributes['shopify_domain'],
|
6
|
+
countryName = shop.attributes['country_name'],
|
7
|
+
currency = shop.attributes['currency'],
|
8
|
+
domainName = shop.attributes['domain'],
|
9
|
+
planName = shop.attributes['plan_display_name'],
|
10
|
+
createdDate = shop.attributes['created_at'],
|
11
|
+
installedDuration = shop.attributes['installed_duration']
|
12
|
+
|
13
|
+
return (
|
14
|
+
<tr>
|
15
|
+
<td><a href={editShopUrl}>#{shop.id}</a></td>
|
16
|
+
<td>{shopifyDomain}</td>
|
17
|
+
<td>{shop.attributes.status}</td>
|
18
|
+
<td>{shop.attributes.email}</td>
|
19
|
+
<td>{countryName}</td>
|
20
|
+
<td>{shop.attributes.currency}</td>
|
21
|
+
<td>{shop.attributes.domain}</td>
|
22
|
+
<td>{planName}</td>
|
23
|
+
<td>{createdDate}</td>
|
24
|
+
<td>{installedDuration}</td>
|
25
|
+
</tr>
|
26
|
+
)
|
27
|
+
};
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module DiscoApp::Admin::Concerns::AppSettingsController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def edit
|
5
|
+
@app_settings = DiscoApp::AppSettings.instance
|
6
|
+
end
|
7
|
+
|
8
|
+
def update
|
9
|
+
@app_settings = DiscoApp::AppSettings.instance
|
10
|
+
if @app_settings.update_attributes(app_settings_params)
|
11
|
+
flash[:success] = 'Settings updated.'
|
12
|
+
redirect_to edit_admin_app_settings_path
|
13
|
+
else
|
14
|
+
render 'edit'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def app_settings_params
|
21
|
+
params.require(:app_settings)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module DiscoApp::Admin::Concerns::AuthenticatedController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
|
6
|
+
protect_from_forgery with: :exception
|
7
|
+
before_action :authenticate_administrator
|
8
|
+
layout 'admin'
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def authenticate_administrator
|
15
|
+
authenticate_or_request_with_http_basic do |username, password|
|
16
|
+
(not username.blank?) && (not password.blank?) && username == ENV['ADMIN_APP_USERNAME'] && password == ENV['ADMIN_APP_PASSWORD']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module DiscoApp::Concerns::AppProxyController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :verify_proxy_signature
|
6
|
+
before_action :shopify_shop
|
7
|
+
after_action :add_liquid_header
|
8
|
+
|
9
|
+
rescue_from ActiveRecord::RecordNotFound do |exception|
|
10
|
+
render_error 404
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def verify_proxy_signature
|
17
|
+
unless proxy_signature_is_valid?
|
18
|
+
head :unauthorized
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def proxy_signature_is_valid?
|
23
|
+
return true if DiscoApp.configuration.skip_proxy_verification?
|
24
|
+
DiscoApp::ProxyService.proxy_signature_is_valid?(request.query_string, ShopifyApp.configuration.secret)
|
25
|
+
end
|
26
|
+
|
27
|
+
def shopify_shop
|
28
|
+
@shop = DiscoApp::Shop.find_by_shopify_domain!(params[:shop])
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_liquid_header
|
32
|
+
response.headers['Content-Type'] = 'application/liquid'
|
33
|
+
end
|
34
|
+
|
35
|
+
def render_error(status)
|
36
|
+
add_liquid_header
|
37
|
+
render "disco_app/proxy_errors/#{status}", status: status
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module DiscoApp::Concerns::AuthenticatedController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :login_again_if_different_shop
|
6
|
+
before_action :shopify_shop
|
7
|
+
before_action :verify_status
|
8
|
+
around_filter :shopify_session
|
9
|
+
layout 'embedded_app'
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def shopify_shop
|
15
|
+
if shop_session
|
16
|
+
@shop = DiscoApp::Shop.find_by!(shopify_domain: @shop_session.url)
|
17
|
+
else
|
18
|
+
redirect_to_login
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def verify_status
|
23
|
+
if not (@shop.charge_active? or @shop.charge_waived?)
|
24
|
+
redirect_if_not_current_path(disco_app.new_charge_path)
|
25
|
+
elsif @shop.charge_accepted?
|
26
|
+
redirect_if_not_current_path(disco_app.activate_charge_path)
|
27
|
+
elsif @shop.never_installed? or @shop.uninstalled?
|
28
|
+
redirect_if_not_current_path(disco_app.install_path)
|
29
|
+
elsif @shop.awaiting_install? or @shop.installing?
|
30
|
+
redirect_if_not_current_path(disco_app.installing_path)
|
31
|
+
elsif @shop.awaiting_uninstall? or @shop.uninstalling?
|
32
|
+
redirect_if_not_current_path(disco_app.uninstalling_path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def redirect_if_not_current_path(target)
|
37
|
+
if request.path != target
|
38
|
+
redirect_to target
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module DiscoApp::Concerns::CarrierRequestController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :verify_carrier_request
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def verify_carrier_request
|
11
|
+
unless carrier_request_signature_is_valid?
|
12
|
+
head :unauthorized
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def carrier_request_signature_is_valid?
|
17
|
+
return true if DiscoApp.configuration.skip_carrier_request_verification?
|
18
|
+
DiscoApp::CarrierRequestService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'])
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module DiscoApp
|
2
2
|
class InstallController < ApplicationController
|
3
|
-
include DiscoApp::AuthenticatedController
|
3
|
+
include DiscoApp::Concerns::AuthenticatedController
|
4
4
|
|
5
5
|
# Start the installation process for the current shop, then redirect to the installing screen.
|
6
6
|
def install
|
@@ -3,6 +3,7 @@ module DiscoApp::Concerns::Shop
|
|
3
3
|
|
4
4
|
included do
|
5
5
|
include ShopifyApp::Shop
|
6
|
+
include ActionView::Helpers::DateHelper
|
6
7
|
|
7
8
|
# Define relationships to plans and subscriptions.
|
8
9
|
has_many :subscriptions
|
@@ -65,6 +66,10 @@ module DiscoApp::Concerns::Shop
|
|
65
66
|
"https://#{shopify_domain}/admin"
|
66
67
|
end
|
67
68
|
|
69
|
+
def installed_duration
|
70
|
+
distance_of_time_in_words_to_now(created_at.time)
|
71
|
+
end
|
72
|
+
|
68
73
|
end
|
69
74
|
|
70
75
|
end
|
@@ -1,11 +1,17 @@
|
|
1
|
-
module DiscoApp::Concerns::
|
1
|
+
module DiscoApp::Concerns::Synchronises
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
|
4
4
|
class_methods do
|
5
5
|
|
6
|
-
def
|
6
|
+
def should_synchronise?(shop, data)
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def synchronise(shop, data)
|
7
11
|
data = data.with_indifferent_access
|
8
12
|
|
13
|
+
return unless should_synchronise?(shop, data)
|
14
|
+
|
9
15
|
instance = self.find_or_create_by!(id: data[:id]) do |instance|
|
10
16
|
instance.shop = shop
|
11
17
|
instance.data = data
|
@@ -16,8 +22,15 @@ module DiscoApp::Concerns::SynchronisesWithShopify
|
|
16
22
|
instance
|
17
23
|
end
|
18
24
|
|
19
|
-
def
|
25
|
+
def should_synchronise_deletion?(shop, data)
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def synchronise_deletion(shop, data)
|
20
30
|
data = data.with_indifferent_access
|
31
|
+
|
32
|
+
return unless should_synchronise_deletion?(shop, data)
|
33
|
+
|
21
34
|
self.destroy_all(shop: shop, id: data[:id])
|
22
35
|
end
|
23
36
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'jsonapi/resource'
|
2
|
+
|
3
|
+
module DiscoApp::Admin::Resources::Concerns::ShopResource
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
|
8
|
+
attributes :shopify_domain, :status, :email, :country_name
|
9
|
+
attributes :currency, :domain, :plan_display_name, :created_at
|
10
|
+
attributes :installed_duration
|
11
|
+
|
12
|
+
model_name 'DiscoApp::Shop'
|
13
|
+
|
14
|
+
filters :status
|
15
|
+
|
16
|
+
# Adjust the base records method to ensure only models for the authenticated domain are retrieved.
|
17
|
+
def self.records(options = {})
|
18
|
+
records = DiscoApp::Shop.order(created_at: :desc)
|
19
|
+
records
|
20
|
+
end
|
21
|
+
|
22
|
+
# Apply filters.
|
23
|
+
def self.apply_filter(records, filter, value, options)
|
24
|
+
return records if value.blank?
|
25
|
+
|
26
|
+
# Perform appropriate filtering.
|
27
|
+
case filter
|
28
|
+
when :status
|
29
|
+
return records.where(status: value.map { |v| DiscoApp::Shop.statuses[v.to_sym] } )
|
30
|
+
else
|
31
|
+
return super(records, filter, value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Don't allow the update of any fields via the API.
|
36
|
+
def self.updatable_fields(context)
|
37
|
+
[]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Don't allow the creation of any fields via the API.
|
41
|
+
def self.creatable_fields(context)
|
42
|
+
[]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<% provide(:title, 'Shops') %>
|
2
|
+
|
3
|
+
<div class="next-grid">
|
4
|
+
<div class="next-grid__cell">
|
5
|
+
|
6
|
+
<%= react_component('FilterableShopList', {
|
7
|
+
shopsUrl: admin_resources_shops_path(format: :json),
|
8
|
+
editShopUrl: edit_admin_shop_path(':id')
|
9
|
+
}) %>
|
10
|
+
|
11
|
+
</div>
|
12
|
+
</div>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<nav class="navbar navbar-default navbar-static-top">
|
2
|
+
<div class="container-fluid">
|
3
|
+
|
4
|
+
<!-- Brand & Toggle -->
|
5
|
+
<div class="navbar-header">
|
6
|
+
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
|
7
|
+
<span class="sr-only">Toggle navigation</span>
|
8
|
+
<span class="icon-bar"></span>
|
9
|
+
<span class="icon-bar"></span>
|
10
|
+
<span class="icon-bar"></span>
|
11
|
+
</button>
|
12
|
+
<a class="navbar-brand" href="<%= admin_shops_path %>">Credible</a>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<!-- Collapsing Navigation Links -->
|
16
|
+
<div class="collapse navbar-collapse">
|
17
|
+
<ul class="nav navbar-nav">
|
18
|
+
<%= active_link_to 'Shops', admin_shops_path, wrap_tag: :li %>
|
19
|
+
<%= active_link_to 'Settings', edit_admin_app_settings_path, wrap_tag: :li %>
|
20
|
+
</ul>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
</div>
|
24
|
+
</nav>
|