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.
@@ -34,6 +34,7 @@
34
34
  @import 'shared/alerts'
35
35
  @import 'shared/progress'
36
36
  @import 'shared/reveal'
37
+ @import 'shared/buttons'
37
38
 
38
39
  @import 'components/command_button'
39
40
 
@@ -0,0 +1,2 @@
1
+ .button-login
2
+ margin: .5em
@@ -39,4 +39,4 @@
39
39
  background: #fff
40
40
  border: 1px solid #ddd
41
41
  margin: 100px 0
42
- padding: 40px 20px 0 20px
42
+ padding: 40px 20px
@@ -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(/(\w+)_link$/).try(:[], 1)
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(/(\w+)_checkmark/).try(:[], 1)
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(/(\w+)_in_words/).try(:[], 1)
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
- div(class: 'services container') {
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
+ }
@@ -26,19 +26,19 @@ components.each {|component| require component }
26
26
  module ExpressAdmin
27
27
  class Engine < ::Rails::Engine
28
28
 
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
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
- self.instance_variable_set(collection_ivar, instance_variable(scope))
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
@@ -1,3 +1,3 @@
1
1
  module ExpressAdmin
2
- VERSION = "1.7.8"
2
+ VERSION = "1.7.9"
3
3
  end
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
- end
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