pageflow 15.1.0 → 15.2.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pageflow might be problematic. Click here for more details.

Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -161
  3. data/README.md +1 -2
  4. data/admins/pageflow/accounts.rb +94 -19
  5. data/app/assets/javascripts/pageflow/admin/accounts.js +3 -3
  6. data/app/assets/javascripts/pageflow/dist/frontend.js +2 -0
  7. data/app/assets/javascripts/pageflow/dist/ui.js +9 -0
  8. data/app/assets/stylesheets/pageflow/themes/default/logo/variant/background_image.scss +4 -0
  9. data/app/assets/stylesheets/pageflow/themes/default/logo/variant/watermark.scss +5 -0
  10. data/app/controllers/pageflow/editor/widgets_controller.rb +1 -1
  11. data/app/helpers/pageflow/pages_helper.rb +1 -0
  12. data/app/models/pageflow/account.rb +6 -0
  13. data/app/models/pageflow/draft_entry.rb +13 -3
  14. data/app/models/pageflow/entry.rb +8 -1
  15. data/app/models/pageflow/entry_template.rb +55 -0
  16. data/app/models/pageflow/published_entry.rb +13 -3
  17. data/app/models/pageflow/revision.rb +1 -1
  18. data/app/models/pageflow/theming.rb +8 -47
  19. data/app/policies/pageflow/entry_template_policy.rb +18 -0
  20. data/app/policies/pageflow/theming_policy.rb +0 -4
  21. data/app/views/admin/accounts/_configuration_label.html.erb +5 -0
  22. data/app/views/admin/accounts/_entry_template_details.html.arb +17 -0
  23. data/app/views/admin/accounts/_form.html.erb +43 -23
  24. data/app/views/admin/accounts/_share_providers_label.html.erb +5 -0
  25. data/app/views/admin/accounts/_theming_details.html.arb +0 -12
  26. data/app/views/pageflow/themes/_theme.json.jbuilder +1 -0
  27. data/config/locales/de.yml +12 -7
  28. data/config/locales/en.yml +12 -7
  29. data/db/migrate/20200122115400_create_pageflow_entry_templates.rb +75 -0
  30. data/db/migrate/20200206134400_convert_legacy_scrolled_content_element_types.rb +48 -0
  31. data/entry_types/paged/app/assets/javascripts/pageflow_paged/dist/editor.js +42 -4
  32. data/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/content_elements_controller.rb +1 -0
  33. data/entry_types/scrolled/app/helpers/pageflow_scrolled/editor/seed_html_helper.rb +4 -1
  34. data/entry_types/scrolled/app/helpers/pageflow_scrolled/entry_json_seed_helper.rb +2 -1
  35. data/entry_types/scrolled/app/helpers/pageflow_scrolled/i18n_helper.rb +35 -0
  36. data/entry_types/scrolled/app/views/pageflow_scrolled/editor/entries/_seed.json.jbuilder +1 -1
  37. data/entry_types/scrolled/app/views/pageflow_scrolled/entries/show.html.erb +14 -2
  38. data/entry_types/scrolled/app/views/pageflow_scrolled/entry_json_seed/_entry.json.jbuilder +10 -1
  39. data/entry_types/scrolled/config/locales/new/de.yml +231 -8
  40. data/entry_types/scrolled/config/locales/new/en.yml +228 -10
  41. data/entry_types/scrolled/lib/generators/pageflow_scrolled/install/install_generator.rb +3 -0
  42. data/entry_types/scrolled/lib/pageflow_scrolled/seeds.rb +9 -4
  43. data/entry_types/scrolled/lib/tasks/pageflow_scrolled_tasks.rake +96 -0
  44. data/entry_types/scrolled/package/contentElements-editor.js +410 -0
  45. data/entry_types/scrolled/package/contentElements-frontend.js +533 -0
  46. data/entry_types/scrolled/package/editor.js +498 -2561
  47. data/entry_types/scrolled/package/frontend.js +713 -1238
  48. data/entry_types/scrolled/package/package.json +12 -2
  49. data/lib/pageflow/ability_mixin.rb +3 -2
  50. data/lib/pageflow/seeds.rb +0 -2
  51. data/lib/pageflow/theme.rb +4 -0
  52. data/lib/pageflow/themes.rb +5 -1
  53. data/lib/pageflow/version.rb +1 -1
  54. data/packages/pageflow/config/jest/index.js +0 -1
  55. data/packages/pageflow/config/jest/transformers/jst.js +1 -1
  56. data/packages/pageflow/config/webpack.js +0 -1
  57. data/packages/pageflow/editor.js +33 -4
  58. data/packages/pageflow/package.json +2 -1
  59. data/packages/pageflow/testHelpers.js +367 -4
  60. data/packages/pageflow/ui.js +9 -0
  61. data/spec/factories/accounts.rb +6 -0
  62. data/spec/factories/entry_templates.rb +8 -0
  63. data/spec/factories/published_entries.rb +3 -1
  64. data/spec/factories/themings.rb +0 -1
  65. metadata +14 -4
  66. data/app/assets/javascripts/pageflow/dist/editor.js +0 -11881
  67. data/app/assets/javascripts/pageflow/dist/index.js +0 -3
@@ -1,8 +1,8 @@
1
1
  jQuery(function($) {
2
2
  $('.admin_accounts').filter('.new, .edit').find('form.pageflow_account').each(function() {
3
- var themeSelect = $('#account_default_theming_attributes_theme_name', this);
3
+ var themeSelect = $('#account_paged_entry_template_attributes_theme_name', this);
4
4
  var themeOptions = JSON.parse($('script#theme_options', this).text());
5
- var homeButtonCheckBox = $('#account_default_theming_attributes_home_button_enabled_by_default', this);
5
+ var homeButtonCheckBox = $('#account_paged_entry_template_attributes_configuration_home_button_enabled', this);
6
6
 
7
7
  updateThemeFeatures();
8
8
  themeSelect.on('change', updateThemeFeatures);
@@ -27,4 +27,4 @@ jQuery(function($) {
27
27
  }
28
28
  }
29
29
  });
30
- });
30
+ });
@@ -284,6 +284,7 @@
284
284
  updateCommonPageCssClasses: function updateCommonPageCssClasses(pageElement, configuration) {
285
285
  pageElement.toggleClass('invert', configuration.get('invert'));
286
286
  pageElement.toggleClass('hide_title', configuration.get('hide_title'));
287
+ pageElement.toggleClass('hide_logo', !!configuration.get('hide_logo'));
287
288
  toggleModeClass(pageflow.Page.textPositions, 'text_position');
288
289
  toggleModeClass(pageflow.Page.delayedTextFadeIn, 'delayed_text_fade_in');
289
290
  toggleModeClass(pageflow.Page.scrollIndicatorModes, 'scroll_indicator_mode');
@@ -4704,6 +4705,7 @@
4704
4705
  function updateClasses(page) {
4705
4706
  that.element.toggleClass('invert', page.hasClass('invert'));
4706
4707
  that.element.toggleClass('first_page', page.index() === 0);
4708
+ that.element.toggleClass('hide_logo', page.hasClass('hide_logo'));
4707
4709
  }
4708
4710
  }
4709
4711
  });
@@ -1141,6 +1141,7 @@ this.pageflow._uiGlobalInterop = (function (exports, Marionette, _, $, I18n$1, B
1141
1141
 
1142
1142
  var inputView = {
1143
1143
  ui: {
1144
+ label: 'label',
1144
1145
  labelText: 'label .name',
1145
1146
  inlineHelp: 'label .inline_help'
1146
1147
  },
@@ -1186,6 +1187,7 @@ this.pageflow._uiGlobalInterop = (function (exports, Marionette, _, $, I18n$1, B
1186
1187
  this.ui.inlineHelp.hide();
1187
1188
  }
1188
1189
 
1190
+ this.setLabelFor();
1189
1191
  this.updateDisabled();
1190
1192
  this.setupVisibleBinding();
1191
1193
  },
@@ -1221,6 +1223,13 @@ this.pageflow._uiGlobalInterop = (function (exports, Marionette, _, $, I18n$1, B
1221
1223
  html: true
1222
1224
  }), this.options.additionalInlineHelpText]).join(' ');
1223
1225
  },
1226
+ setLabelFor: function setLabelFor() {
1227
+ if (this.ui.input && this.ui.label.length === 1 && !this.ui.input.attr('id')) {
1228
+ var id = 'input_' + this.model.modelName + '_' + this.options.propertyName;
1229
+ this.ui.input.attr('id', id);
1230
+ this.ui.label.attr('for', id);
1231
+ }
1232
+ },
1224
1233
  updateDisabled: function updateDisabled() {
1225
1234
  if (this.ui.input) {
1226
1235
  this.updateDisabledAttribute(this.ui.input);
@@ -110,4 +110,8 @@ $logo-background-image-variant-banner-phone-height: null !default;
110
110
  .first_page .scroller > div:after {
111
111
  opacity: $logo-background-image-variant-first-page-opacity;
112
112
  }
113
+
114
+ .page.hide_logo .content_and_background .scroller > div:after {
115
+ display: none;
116
+ }
113
117
  }
@@ -105,6 +105,11 @@ $logo-watermark-mobile-height: null !default;
105
105
  opacity: 1;
106
106
  }
107
107
 
108
+ .header.hide_logo .header_logo:before,
109
+ .header.hide_logo .header_logo:after {
110
+ opacity: 0;
111
+ }
112
+
108
113
  .header.near_top .header_logo {
109
114
  pointer-events: all;
110
115
  }
@@ -45,7 +45,7 @@ module Pageflow
45
45
  if params[:collection_name] == 'entries'
46
46
  DraftEntry.find(params[:subject_id])
47
47
  else
48
- Theming.find(params[:subject_id])
48
+ EntryTemplate.find(params[:subject_id])
49
49
  end
50
50
  end
51
51
  end
@@ -19,6 +19,7 @@ module Pageflow
19
19
  classes << 'chapter_beginning' if page.position == 0
20
20
  classes << 'first_page' if page.is_first
21
21
  classes << 'no_text_content' if !page_has_content(page)
22
+ classes << 'hide_logo' if page.configuration['hide_logo']
22
23
  classes.join(' ')
23
24
  end
24
25
 
@@ -10,9 +10,11 @@ module Pageflow
10
10
  has_many :entry_memberships, through: :entries, source: :memberships
11
11
 
12
12
  has_many :themings, dependent: :destroy
13
+ has_many :entry_templates, dependent: :destroy
13
14
  belongs_to :default_theming, :class_name => 'Theming'
14
15
 
15
16
  validates :default_theming, :presence => true
17
+ validates_associated :entry_templates
16
18
 
17
19
  accepts_nested_attributes_for :default_theming, :update_only => true
18
20
 
@@ -24,6 +26,10 @@ module Pageflow
24
26
  end
25
27
  end
26
28
 
29
+ def first_paged_entry_template
30
+ EntryTemplate.find_or_initialize_by(account: self, entry_type: 'paged')
31
+ end
32
+
27
33
  def blacklist_for_serialization
28
34
  [:features_configuration]
29
35
  end
@@ -17,11 +17,9 @@ module Pageflow
17
17
  :type_name,
18
18
  :to => :entry)
19
19
 
20
- delegate(:title, :summary, :credits, :manual_start,
20
+ delegate(:title, :summary, :credits,
21
21
  :widgets,
22
22
  :storylines, :main_storyline_chapters, :chapters, :pages,
23
- :emphasize_chapter_beginning,
24
- :emphasize_new_pages,
25
23
  :share_url, :share_image_id, :share_image_x, :share_image_y,
26
24
  :share_providers, :active_share_providers,
27
25
  :find_files, :find_file, :find_file_by_perma_id,
@@ -116,6 +114,18 @@ module Pageflow
116
114
  OverviewButton.new(draft)
117
115
  end
118
116
 
117
+ def manual_start
118
+ revision.configuration['manual_start']
119
+ end
120
+
121
+ def emphasize_chapter_beginning
122
+ revision.configuration['emphasize_chapter_beginning']
123
+ end
124
+
125
+ def emphasize_new_pages
126
+ revision.configuration['emphasize_new_pages']
127
+ end
128
+
119
129
  def resolve_widgets(options = {})
120
130
  widgets.resolve(Pageflow.config_for(entry), options)
121
131
  end
@@ -51,7 +51,14 @@ module Pageflow
51
51
  after_create unless: :skip_draft_creation do
52
52
  create_draft!
53
53
  draft.storylines.create!(configuration: {main: true})
54
- theming.copy_defaults_to(draft)
54
+ entry_template.copy_defaults_to(draft)
55
+ end
56
+
57
+ def entry_template
58
+ @entry_template ||= EntryTemplate.find_or_initialize_by(
59
+ account_id: account.id,
60
+ entry_type: type_name
61
+ )
55
62
  end
56
63
 
57
64
  def entry_type
@@ -0,0 +1,55 @@
1
+ module Pageflow
2
+ class EntryTemplate < ApplicationRecord
3
+ include ThemeReferencer
4
+ include SerializedConfiguration
5
+ serialize :default_share_providers, JSON
6
+ belongs_to :account
7
+ has_many :widgets, as: :subject, dependent: :destroy
8
+
9
+ validates :account, presence: true
10
+
11
+ def resolve_widgets(options = {})
12
+ widgets.resolve(Pageflow.config_for(account), options)
13
+ end
14
+
15
+ def copy_defaults_to(revision)
16
+ widgets.copy_all_to(revision)
17
+ copy_attributes_to(revision)
18
+ end
19
+
20
+ def share_providers=(share_providers)
21
+ self.default_share_providers = share_providers
22
+ end
23
+
24
+ def share_providers
25
+ default_share_providers
26
+ end
27
+
28
+ def default_share_providers
29
+ self[:default_share_providers].presence ||
30
+ hashify_provider_array(Pageflow.config.default_share_providers)
31
+ end
32
+
33
+ private
34
+
35
+ def copy_attributes_to(revision)
36
+ revision.update(
37
+ author: default_author.presence || Pageflow.config.default_author_meta_tag,
38
+ publisher: default_publisher.presence || Pageflow.config.default_publisher_meta_tag,
39
+ keywords: default_keywords.presence || Pageflow.config.default_keywords_meta_tag,
40
+ share_providers: default_share_providers,
41
+ theme_name: theme_name,
42
+ configuration: configuration,
43
+ locale: default_locale
44
+ )
45
+ end
46
+
47
+ def available_themes
48
+ Pageflow.config_for(account).themes
49
+ end
50
+
51
+ def hashify_provider_array(arr)
52
+ Hash[arr.reject(&:blank?).map { |v| [v.to_s, true] }]
53
+ end
54
+ end
55
+ end
@@ -20,9 +20,7 @@ module Pageflow
20
20
  :storylines, :main_storyline_chapters, :chapters, :pages,
21
21
  :find_files, :find_file, :find_file_by_perma_id,
22
22
  :image_files, :video_files, :audio_files,
23
- :summary, :credits, :manual_start,
24
- :emphasize_chapter_beginning,
25
- :emphasize_new_pages,
23
+ :summary, :credits,
26
24
  :share_url, :share_image_id, :share_image_x, :share_image_y,
27
25
  :share_providers, :active_share_providers,
28
26
  :locale,
@@ -43,6 +41,18 @@ module Pageflow
43
41
  revision.title.presence || entry.title
44
42
  end
45
43
 
44
+ def manual_start
45
+ revision.configuration['manual_start']
46
+ end
47
+
48
+ def emphasize_chapter_beginning
49
+ revision.configuration['emphasize_chapter_beginning']
50
+ end
51
+
52
+ def emphasize_new_pages
53
+ revision.configuration['emphasize_new_pages']
54
+ end
55
+
46
56
  def stylesheet_model
47
57
  custom_revision? ? revision : entry
48
58
  end
@@ -102,7 +102,7 @@ module Pageflow
102
102
  end
103
103
 
104
104
  def share_providers
105
- self[:share_providers] || entry.theming.default_share_providers
105
+ self[:share_providers] || entry.entry_template.default_share_providers
106
106
  end
107
107
 
108
108
  def author
@@ -1,11 +1,6 @@
1
1
  module Pageflow
2
2
  class Theming < ApplicationRecord
3
- include ThemeReferencer
4
-
5
- serialize :default_share_providers, JSON
6
-
7
3
  belongs_to :account
8
- has_many :widgets, as: :subject, dependent: :destroy
9
4
 
10
5
  has_many :entries
11
6
 
@@ -14,55 +9,21 @@ module Pageflow
14
9
 
15
10
  validates :account, :presence => true
16
11
 
17
- def resolve_widgets(options = {})
18
- widgets.resolve(Pageflow.config_for(account), options)
19
- end
20
-
21
12
  def cname_domain
22
13
  cname.split('.').pop(2).join('.')
23
14
  end
24
15
 
25
16
  def name
26
- I18n.t('pageflow.admin.themings.name', :account_name => account.name, :theme_name => theme_name)
27
- end
28
-
29
- def copy_defaults_to(revision)
30
- widgets.copy_all_to(revision)
31
- copy_attributes_to(revision)
32
- end
33
-
34
- def share_providers=(share_providers_array)
35
- self.default_share_providers = hashify_provider_array(share_providers_array)
36
- end
37
-
38
- def share_providers
39
- default_share_providers.to_a
40
- end
41
-
42
- def default_share_providers
43
- self[:default_share_providers].presence || hashify_provider_array(Pageflow.config.default_share_providers)
44
- end
45
-
46
- private
47
-
48
- def copy_attributes_to(revision)
49
- revision.update(
50
- author: default_author.presence || Pageflow.config.default_author_meta_tag,
51
- publisher: default_publisher.presence || Pageflow.config.default_publisher_meta_tag,
52
- keywords: default_keywords.presence || Pageflow.config.default_keywords_meta_tag,
53
- share_providers: default_share_providers,
54
- theme_name: theme_name,
55
- home_button_enabled: home_button_enabled_by_default,
56
- locale: default_locale
57
- )
58
- end
59
-
60
- def available_themes
61
- Pageflow.config_for(account).themes
17
+ I18n.t('pageflow.admin.themings.name',
18
+ account_name: account.name,
19
+ theme_name: 'default')
62
20
  end
63
21
 
64
- def hashify_provider_array(arr)
65
- Hash[arr.reject(&:blank?).map { |v| [v.to_s, true] }]
22
+ # @deprecated Depending on what you need this for, consider
23
+ # scoping your code to an entry type or look at a specific entry's
24
+ # theme name.
25
+ def theme_name
26
+ account.first_paged_entry_template.theme_name
66
27
  end
67
28
  end
68
29
  end
@@ -0,0 +1,18 @@
1
+ module Pageflow
2
+ class EntryTemplatePolicy < ApplicationPolicy
3
+ def initialize(user, entry_template)
4
+ @user = user
5
+ @entry_template = entry_template
6
+ end
7
+
8
+ def edit?
9
+ allows?(%w(publisher manager))
10
+ end
11
+
12
+ private
13
+
14
+ def allows?(roles)
15
+ @user.memberships.where(role: roles, entity: @entry_template.account).any?
16
+ end
17
+ end
18
+ end
@@ -44,10 +44,6 @@ module Pageflow
44
44
  allows?(%w(publisher manager))
45
45
  end
46
46
 
47
- def index_widgets_for?
48
- @user.admin?
49
- end
50
-
51
47
  private
52
48
 
53
49
  def allows?(roles)
@@ -0,0 +1,5 @@
1
+ <li>
2
+ <label class='label'>
3
+ <%= t('activerecord.attributes.pageflow/entry_template.configuration.group_label') %>
4
+ </label>
5
+ </li>
@@ -0,0 +1,17 @@
1
+ extensible_attributes_table_for(account.first_paged_entry_template,
2
+ Pageflow.config_for(account)
3
+ .admin_attributes_table_rows.for(:entry_template)) do
4
+ row :theme, class: 'theme' do
5
+ account.first_paged_entry_template.theme_name
6
+ end
7
+ row :default_locale, class: 'default_locale' do
8
+ t('pageflow.public._language', locale: account.first_paged_entry_template.default_locale)
9
+ end
10
+ row :default_author, class: 'default_author'
11
+ row :default_publisher, class: 'default_publisher'
12
+ row :default_keywords, class: 'default_keywords'
13
+ row :default_share_providers, class: 'default_share_providers' do
14
+ account.first_paged_entry_template.default_share_providers
15
+ .select { |_k, v| v == true }.keys.map(&:camelize).join(', ')
16
+ end
17
+ end
@@ -18,48 +18,68 @@
18
18
  <% end %>
19
19
 
20
20
  <%= f.inputs do %>
21
- <%= theming.input :default_locale,
21
+ <%= theming.input :imprint_link_label %>
22
+ <%= theming.input :imprint_link_url %>
23
+ <%= theming.input :copyright_link_label %>
24
+ <%= theming.input :copyright_link_url %>
25
+ <%= theming.input :privacy_link_url %>
26
+
27
+ <% account_config.admin_form_inputs.find_all_for(:theming).each do |form_input| %>
28
+ <%= form_input.build(theming) %>
29
+ <% end %>
30
+ <% end %>
31
+ <% end %>
32
+
33
+ <%= f.semantic_fields_for 'paged_entry_template_attributes', @entry_template do |et| %>
34
+ <%= f.inputs do %>
35
+ <%= et.input :default_locale,
22
36
  as: :select,
23
37
  include_blank: false,
24
38
  collection: Pageflow.config.available_public_locales.map{ |locale|
25
39
  [t('pageflow.public._language', locale: locale), locale.to_s]
26
40
  },
27
41
  hint: t('pageflow.admin.themings.default_locale_hint') %>
28
- <%= theming.input :default_author,
42
+ <%= et.input :default_author,
29
43
  hint: t('pageflow.admin.themings.default_author_hint'),
30
44
  placeholder: Pageflow.config.default_author_meta_tag %>
31
- <%= theming.input :default_publisher,
45
+ <%= et.input :default_publisher,
32
46
  hint: t('pageflow.admin.themings.default_publisher_hint'),
33
47
  placeholder: Pageflow.config.default_publisher_meta_tag %>
34
- <%= theming.input :default_keywords,
48
+ <%= et.input :default_keywords,
35
49
  hint: t('pageflow.admin.themings.default_keywords_hint'),
36
50
  placeholder: Pageflow.config.default_keywords_meta_tag %>
37
- <%= theming.input :share_providers,
38
- as: :check_boxes,
39
- collection: Pageflow.config.available_share_providers,
40
- member_label: Proc.new { |s| s.to_s.camelize } %>
41
-
42
- <% end %>
43
-
44
- <%= f.inputs do %>
45
- <%= theming.input :imprint_link_label %>
46
- <%= theming.input :imprint_link_url %>
47
- <%= theming.input :copyright_link_label %>
48
- <%= theming.input :copyright_link_url %>
49
- <%= theming.input :privacy_link_url %>
50
-
51
- <% account_config.admin_form_inputs.find_all_for(:theming).each do |form_input| %>
52
- <%= form_input.build(theming) %>
51
+ <%= render('admin/accounts/share_providers_label') %>
52
+ <%= et.semantic_fields_for :share_providers,
53
+ OpenStruct.new(
54
+ @account.first_paged_entry_template.default_share_providers
55
+ ) do |providers| %>
56
+ <% Pageflow.config.available_share_providers.each do |provider| %>
57
+ <%= providers.input provider,
58
+ as: :boolean,
59
+ label: provider.to_s.camelize,
60
+ checked_value: 'true',
61
+ unchecked_value: 'false' %>
62
+ <% end %>
53
63
  <% end %>
54
64
  <% end %>
55
65
 
56
66
  <%= f.inputs do %>
57
67
  <%= render('admin/accounts/theming_defaults_inline_help') %>
58
- <%= theming.input :theme_name, include_blank: false, collection: account_config.themes.names %>
59
- <%= theming.input :home_button_enabled_by_default %>
60
- <%= admin_widgets_fields(theming, account_config) %>
68
+ <%= et.input :theme_name, include_blank: false, collection: account_config.themes.names %>
69
+ <%= render('admin/accounts/configuration_label') %>
70
+ <%= et.semantic_fields_for :configuration,
71
+ OpenStruct.new(
72
+ @account.first_paged_entry_template.configuration
73
+ ) do |config| %>
74
+ <%= config.input :home_button_enabled,
75
+ as: :boolean,
76
+ label: I18n.t('activerecord.attributes.pageflow/entry_template.'\
77
+ 'configuration.home_button_enabled') %>
78
+ <% end %>
79
+ <%= admin_widgets_fields(et, account_config) %>
61
80
  <% end %>
62
81
  <% end %>
82
+
63
83
  <%= f.actions do %>
64
84
  <%= f.action(:submit) %>
65
85
  <%= f.action(:cancel, :wrapper_html => {:class => 'cancel'}) %>