express_admin 1.7.8 → 1.7.9
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/ace/mode-css.js +829 -0
- data/app/assets/stylesheets/express_admin/screen.sass +1 -0
- data/app/assets/stylesheets/express_admin/shared/_buttons.sass +2 -0
- data/app/assets/stylesheets/express_admin/shared/_forms.sass +1 -1
- data/app/components/express_admin/file_upload.rb +131 -0
- data/app/components/express_admin/media_form.rb +27 -0
- data/app/components/express_admin/oauth_sign_in_links.rb +28 -0
- data/app/components/express_admin/smart_table.rb +51 -5
- data/app/helpers/express_admin/admin_helper.rb +0 -4
- data/app/views/devise/sessions/new.html.et +2 -5
- data/lib/express_admin/engine.rb +14 -14
- data/lib/express_admin/standard_actions.rb +13 -1
- data/lib/express_admin/version.rb +1 -1
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/test/components/smart_table_test.rb +37 -1
- metadata +7 -2
@@ -0,0 +1,131 @@
|
|
1
|
+
module ExpressAdmin
|
2
|
+
module Components
|
3
|
+
class FileUpload < ExpressTemplates::Components::Forms::FormComponent
|
4
|
+
|
5
|
+
has_option :action, 'The form action containing the resource path or url.'
|
6
|
+
has_option :max_file_size, 'The maximum file size a user can upload', type: :int
|
7
|
+
|
8
|
+
contains -> {
|
9
|
+
div(class: 'dropzone-previews') {
|
10
|
+
div(class: 'dz-default dz-message') {
|
11
|
+
span(class: 'dz-message-instruction') {'Drop files here or click to upload.'}
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
div(class: 'dz-preview dz-file-preview', id: 'preview-template'){
|
16
|
+
div(class: 'dz-error-message hide') {
|
17
|
+
span('data-dz-errormessage' => 'true')
|
18
|
+
}
|
19
|
+
div(class: 'dz-image'){
|
20
|
+
img('data-dz-thumbnail' => true)
|
21
|
+
}
|
22
|
+
div(class: 'dz-close hide') {
|
23
|
+
icon_link('close-round', 'data-dz-remove' => true)
|
24
|
+
}
|
25
|
+
div(class: 'dz-details hide'){
|
26
|
+
div(class: 'dz-filename'){
|
27
|
+
span('data-dz-name' => true)
|
28
|
+
}
|
29
|
+
div(class: 'dz-size', 'data-dz-size' => true)
|
30
|
+
}
|
31
|
+
div(class: 'dz-progress') {
|
32
|
+
span(class: 'dz-upload', 'data-dz-uploadprogress' => true)
|
33
|
+
}
|
34
|
+
div(class: 'dz-success-mark hide') {
|
35
|
+
icon('checkmark-circled')
|
36
|
+
}
|
37
|
+
div(class: 'dz-error-mark hide') {
|
38
|
+
icon('close-circled')
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
content_for(:page_javascript) {
|
43
|
+
script {
|
44
|
+
%Q(
|
45
|
+
$(function() {
|
46
|
+
previewNode = $('#preview-template').get(0).outerHTML;
|
47
|
+
$('#preview-template').remove();
|
48
|
+
var submitButton;
|
49
|
+
Dropzone.options.#{config[:id].to_s.camelize(:lower)} = {
|
50
|
+
url: window.location.origin + '#{config[:action]}',
|
51
|
+
paramName: '#{config[:id]}[file]',
|
52
|
+
autoProcessQueue: false,
|
53
|
+
uploadMultiple: false,
|
54
|
+
maxFiles: 1,
|
55
|
+
maxFilesize: #{max_file_size},
|
56
|
+
previewTemplate: previewNode,
|
57
|
+
previewsContainer: '.dropzone-previews',
|
58
|
+
clickable: '.dropzone-previews',
|
59
|
+
init: function() {
|
60
|
+
var myDropzone = this;
|
61
|
+
submitButton = this.element.querySelector('input[type=submit]');
|
62
|
+
|
63
|
+
$(submitButton).on('click', function(e) {
|
64
|
+
e.preventDefault();
|
65
|
+
myDropzone.processQueue();
|
66
|
+
});
|
67
|
+
|
68
|
+
this.on('addedfile', function(file) {
|
69
|
+
if (this.files[1]) {
|
70
|
+
this.removeFile(this.files[0]);
|
71
|
+
}
|
72
|
+
$('.dz-message.dz-default').addClass('hide');
|
73
|
+
});
|
74
|
+
|
75
|
+
this.on('removedfile', function() {
|
76
|
+
$('.dz-message.dz-default').removeClass('hide');
|
77
|
+
$(submitButton).removeClass('disabled');
|
78
|
+
});
|
79
|
+
|
80
|
+
this.on('processing', function() {
|
81
|
+
$('.dz-image').zIndex('-999');
|
82
|
+
$('.dz-image > img').addClass('blur');
|
83
|
+
$(submitButton).addClass('disabled');
|
84
|
+
});
|
85
|
+
|
86
|
+
this.on('maxfilesexceeded', function(file) {
|
87
|
+
this.removeAllFiles();
|
88
|
+
this.addFile(file);
|
89
|
+
});
|
90
|
+
|
91
|
+
this.on('success', function(file) {
|
92
|
+
$('.dz-progress').css('opacity', 0);
|
93
|
+
$('.dz-success-mark').removeClass('hide');
|
94
|
+
setTimeout( function() {
|
95
|
+
$('.dz-image').zIndex('999');
|
96
|
+
$('.dz-image > img').removeClass('blur');
|
97
|
+
location.reload();
|
98
|
+
}, 2000);
|
99
|
+
});
|
100
|
+
|
101
|
+
this.on('complete', function(file) {
|
102
|
+
if (file.accepted === false) {
|
103
|
+
$('.dz-progress').css('opacity', 0);
|
104
|
+
setTimeout(function() { $('.dz-error-mark').removeClass('hide') }, 500);
|
105
|
+
|
106
|
+
setTimeout(function() {
|
107
|
+
$('.dz-image').zIndex('999');
|
108
|
+
$('.dz-image > img').removeClass('blur');
|
109
|
+
$('.dz-error-message').removeClass('hide');
|
110
|
+
$(submitButton).addClass('disabled');
|
111
|
+
}, 3000);
|
112
|
+
}
|
113
|
+
});
|
114
|
+
|
115
|
+
this.on('drop', function() {
|
116
|
+
$('.dz-message.dz-default').addClass('hide');
|
117
|
+
});
|
118
|
+
},
|
119
|
+
}
|
120
|
+
});
|
121
|
+
).html_safe
|
122
|
+
}
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
def max_file_size
|
127
|
+
config[:max_file_size]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ExpressAdmin
|
2
|
+
module Components
|
3
|
+
class MediaForm < ExpressTemplates::Components::Configurable
|
4
|
+
|
5
|
+
has_option :max_file_size, 'The maximum file size a user can upload', type: :int, default: 3
|
6
|
+
|
7
|
+
contains -> {
|
8
|
+
express_form(config[:id], enctype: 'multipart/form-data', class: 'dropzone'){
|
9
|
+
text :title
|
10
|
+
text :description
|
11
|
+
label_tag('tags', "Tags")
|
12
|
+
select_tag("media_item[tags]", helpers.options_for_select(ExpressSite::Tag.all.map(&:name), media_item.tags.map(&:name)), class: 'select2', multiple: true)
|
13
|
+
file_upload config[:id], action: form_action, max_file_size: max_file_size
|
14
|
+
submit class: 'button', value: 'Upload'
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
def form_action
|
19
|
+
config[:action] || (resource.try(:persisted?) ? resource_path(resource) : collection_path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def max_file_size
|
23
|
+
config[:max_file_size]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ExpressAdmin
|
2
|
+
class OauthSignInLinks < ExpressTemplates::Components::Configurable
|
3
|
+
|
4
|
+
has_argument :providers, "OAuth providers to generate links for", as: :providers, type: :array
|
5
|
+
|
6
|
+
contains -> {
|
7
|
+
span(class: 'text-gray') { "Login with:" }
|
8
|
+
providers.each do |provider|
|
9
|
+
sign_in_link(provider)
|
10
|
+
end
|
11
|
+
}
|
12
|
+
|
13
|
+
def sign_in_link(provider)
|
14
|
+
link_to "#{provider_name(provider)}", user_omniauth_authorize_path(provider), class: 'hollow button button-login'
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def provider_name(provider)
|
20
|
+
provider.to_s.humanize
|
21
|
+
end
|
22
|
+
|
23
|
+
def providers
|
24
|
+
config[:providers]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -8,6 +8,11 @@ module ExpressAdmin
|
|
8
8
|
|
9
9
|
MAX_COLS_TO_SHOW_IDX = 5
|
10
10
|
MAX_ROWS_TO_SHOW_IDX = 10
|
11
|
+
COLUMN_REGEX_LIST = {
|
12
|
+
link: /(\w+)_link$/,
|
13
|
+
checkmark: /(\w+)_checkmark/,
|
14
|
+
in_words: /(\w+)_in_words/
|
15
|
+
}
|
11
16
|
|
12
17
|
attr :columns
|
13
18
|
|
@@ -33,6 +38,9 @@ module ExpressAdmin
|
|
33
38
|
end
|
34
39
|
options = options + resource_class.instance_methods.grep(/_count$/).map(&:to_s)
|
35
40
|
}
|
41
|
+
has_option :sortable, 'Specify the target column. Provide column sort with caret display to signify column is sortable',
|
42
|
+
type: :array,
|
43
|
+
default: nil
|
36
44
|
has_option :rows, 'Specify the number of rows to show', type: :integer
|
37
45
|
|
38
46
|
has_option :pagination, 'Add pagination to the bottom of the table', type: :string, default: 'bottom'
|
@@ -50,7 +58,7 @@ module ExpressAdmin
|
|
50
58
|
tr {
|
51
59
|
display_columns.each do |column|
|
52
60
|
th(class: column.name) {
|
53
|
-
column.title
|
61
|
+
config[:sortable].present? ? sortable(column) : column.title
|
54
62
|
}
|
55
63
|
end
|
56
64
|
actions_header if should_show_actions?
|
@@ -82,6 +90,31 @@ module ExpressAdmin
|
|
82
90
|
_initialize_columns
|
83
91
|
}
|
84
92
|
|
93
|
+
def sortable(column)
|
94
|
+
table_column = column.table_column
|
95
|
+
if config[:sortable].include?(column.title) && table_column.present?
|
96
|
+
a(href: sort_link(column)) {
|
97
|
+
text_node column.title
|
98
|
+
if helpers.params[:asc].eql?(table_column)
|
99
|
+
i(class: 'icon ion-ios-arrow-up')
|
100
|
+
else
|
101
|
+
i(class: 'icon ion-ios-arrow-down')
|
102
|
+
end
|
103
|
+
}
|
104
|
+
else
|
105
|
+
column.title
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def sort_link(column)
|
110
|
+
uri = URI.parse(config[:id].to_s)
|
111
|
+
table_column = column.table_column
|
112
|
+
param_asc = helpers.params[:asc]
|
113
|
+
sort_direction = param_asc.eql?(table_column) ? :desc : :asc
|
114
|
+
uri.query = { sort_direction => table_column }.to_param
|
115
|
+
uri.to_s
|
116
|
+
end
|
117
|
+
|
85
118
|
def pagination
|
86
119
|
paginate collection, :route_set => route_set
|
87
120
|
end
|
@@ -154,12 +187,12 @@ module ExpressAdmin
|
|
154
187
|
rescue
|
155
188
|
'Error: '+$!.to_s
|
156
189
|
end
|
157
|
-
elsif attrib = accessor.to_s.match(
|
190
|
+
elsif attrib = accessor.to_s.match(COLUMN_REGEX_LIST[:link]).try(:[], 1)
|
158
191
|
# TODO: only works with non-namespaced routes
|
159
192
|
helpers.link_to item.send(attrib), resource_path(item)
|
160
|
-
elsif attrib = accessor.to_s.match(
|
193
|
+
elsif attrib = accessor.to_s.match(COLUMN_REGEX_LIST[:checkmark]).try(:[], 1)
|
161
194
|
"<i class='ion-checkmark-round'></i>".html_safe if item.send(attrib)
|
162
|
-
elsif attrib = accessor.to_s.match(
|
195
|
+
elsif attrib = accessor.to_s.match(COLUMN_REGEX_LIST[:in_words]).try(:[], 1)
|
163
196
|
if item.send(attrib)
|
164
197
|
if item.send(attrib) < DateTime.now
|
165
198
|
"#{helpers.time_ago_in_words(item.send(attrib))} ago"
|
@@ -193,11 +226,24 @@ module ExpressAdmin
|
|
193
226
|
end
|
194
227
|
|
195
228
|
class Column
|
196
|
-
attr :name, :title, :accessor
|
229
|
+
attr :name, :title, :accessor, :table_column
|
197
230
|
def initialize(accessor, title = nil)
|
198
231
|
@name = accessor.kind_of?(Symbol) ? accessor.to_s.underscore : title.titleize.gsub(/\s+/,'').underscore
|
199
232
|
@accessor = accessor
|
200
233
|
@title = title || @name.titleize
|
234
|
+
@table_column = table_column
|
235
|
+
end
|
236
|
+
|
237
|
+
def table_column
|
238
|
+
if accessor.kind_of?(Symbol)
|
239
|
+
COLUMN_REGEX_LIST.map do |key, value|
|
240
|
+
accessor_match(COLUMN_REGEX_LIST[key])
|
241
|
+
end.compact.first
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def accessor_match(regex)
|
246
|
+
accessor.to_s.match(regex).try(:[], 1)
|
201
247
|
end
|
202
248
|
end
|
203
249
|
|
@@ -1,9 +1,5 @@
|
|
1
1
|
module ExpressAdmin
|
2
2
|
module AdminHelper
|
3
|
-
|
4
|
-
# TODO move this out
|
5
|
-
#include ExpressMedia::Admin::MediaHelper if Kernel.const_defined?("ExpressMedia::Engine")
|
6
|
-
|
7
3
|
def title_partial
|
8
4
|
(ExpressAdmin::Engine.config.title_partial rescue nil) || 'shared/express_admin/title'
|
9
5
|
end
|
@@ -51,13 +51,10 @@ div(class: 'grid-container') {
|
|
51
51
|
}
|
52
52
|
|
53
53
|
hr
|
54
|
-
|
55
|
-
a(href: "/users/auth/appexpress") { "Sign in with appexpress" }
|
56
|
-
br
|
57
|
-
}
|
54
|
+
oauth_sign_in_links(Devise.omniauth_providers)
|
58
55
|
}
|
59
56
|
}
|
60
57
|
}
|
61
58
|
}
|
62
59
|
}
|
63
|
-
}
|
60
|
+
}
|
data/lib/express_admin/engine.rb
CHANGED
@@ -26,19 +26,19 @@ components.each {|component| require component }
|
|
26
26
|
module ExpressAdmin
|
27
27
|
class Engine < ::Rails::Engine
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
29
|
+
initializer :assets do |config|
|
30
|
+
engine_assets_path = File.join(File.dirname(__FILE__), '..', '..', 'app', 'assets')
|
31
|
+
all_assets = Dir.glob File.join(engine_assets_path, 'stylesheets', '**', '*.css*')
|
32
|
+
all_assets += Dir.glob File.join(engine_assets_path, 'javascripts', '**', '*.js*')
|
33
|
+
all_assets.each {|path| path.gsub!("#{engine_assets_path}/stylesheets/", '')}
|
34
|
+
all_assets.each {|path| path.gsub!("#{engine_assets_path}/javascripts/", '')}
|
35
|
+
all_assets.each {|path| path.gsub!("#{engine_assets_path}/fonts/", '')}
|
36
|
+
all_assets.each {|path| path.gsub!(/.(scss|coffee)$/, '')}
|
37
|
+
Rails.application.config.assets.paths << Rails.root.join('app', 'assets', 'fonts')
|
38
|
+
Rails.application.config.assets.precompile << /\.(?:svg|eot|woff|ttf|png|jpg|jpeg|gif)$/
|
39
|
+
Rails.application.config.assets.precompile << /\.(?:mp4|webm|mp3)$/
|
40
|
+
Rails.application.config.assets.precompile += all_assets
|
41
|
+
end
|
42
42
|
|
43
43
|
def all_rails_engines
|
44
44
|
Rails.application.eager_load!
|
@@ -59,4 +59,4 @@ module ExpressAdmin
|
|
59
59
|
class Railtie < ::Rails::Railtie
|
60
60
|
config.app_generators.template_engine :et
|
61
61
|
end
|
62
|
-
end
|
62
|
+
end
|
@@ -238,13 +238,25 @@ module ExpressAdmin
|
|
238
238
|
resource_class
|
239
239
|
end
|
240
240
|
scope = search?(scope)
|
241
|
-
|
241
|
+
scope = instance_variable(scope)
|
242
|
+
scope = params[:asc] || params[:desc] ? sort_table(scope) : scope
|
243
|
+
self.instance_variable_set(collection_ivar, scope)
|
242
244
|
end
|
243
245
|
|
244
246
|
def instance_variable(scope)
|
245
247
|
scope.kind_of?(Array) ? scope : scope.all
|
246
248
|
end
|
247
249
|
|
250
|
+
def sort_table(scope)
|
251
|
+
if params.keys.include?('asc')
|
252
|
+
scope.sort{ |a, b| a.send(params[:asc]) <=> b.send(params[:asc])}
|
253
|
+
elsif params.keys.include?('desc')
|
254
|
+
scope.sort{ |a, b| b.send(params[:desc]) <=> a.send(params[:desc])}
|
255
|
+
else
|
256
|
+
scope
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
248
260
|
def search?(scope)
|
249
261
|
params[:search_string].present? ? search(scope) : scope
|
250
262
|
end
|
data/test/dummy/db/test.sqlite3
CHANGED
Binary file
|
@@ -100,6 +100,42 @@ module Components
|
|
100
100
|
assert_match /class="created_at.*less than a minute ago/, fragment
|
101
101
|
end
|
102
102
|
|
103
|
-
|
103
|
+
test 'assign column to be sortable which generates a header link' do
|
104
|
+
fragment = arbre(widget: Widget.first, widgets: Widget.all){
|
105
|
+
smart_table(:widgets,
|
106
|
+
sortable: ['Created'],
|
107
|
+
columns: {
|
108
|
+
'Created' => :created_at_in_words
|
109
|
+
})
|
110
|
+
}
|
111
|
+
|
112
|
+
assert_match "href=\"widgets?asc=created_at\"", fragment
|
113
|
+
end
|
114
|
+
|
115
|
+
test 'assign unknown column to sortable which should not generate a header link' do
|
116
|
+
fragment = arbre(widget: Widget.first, widgets: Widget.all){
|
117
|
+
smart_table(:widgets,
|
118
|
+
sortable: ['Ghost'],
|
119
|
+
columns: {
|
120
|
+
'Created' => :created_at_in_words
|
121
|
+
})
|
122
|
+
}
|
104
123
|
|
124
|
+
assert_no_match "href=\"widgets?asc=ghost\"", fragment
|
125
|
+
end
|
126
|
+
|
127
|
+
test 'assign column with proc value to sortable which should not generate header link' do
|
128
|
+
fragment = arbre(widget: Widget.first, widgets: Widget.all){
|
129
|
+
smart_table(:widgets,
|
130
|
+
sortable: ['This column in will not be a link'],
|
131
|
+
columns: {
|
132
|
+
'Created' => :created_at_in_words,
|
133
|
+
"This column will not be a link" => -> (widget) { widget.column2.upcase },
|
134
|
+
})
|
135
|
+
}
|
136
|
+
|
137
|
+
assert_match 'This column will not be a link', fragment
|
138
|
+
assert_no_match 'href=', fragment
|
139
|
+
end
|
140
|
+
end
|
105
141
|
end
|