tenon 1.1.8 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (579) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +768 -69
  3. data/Rakefile +7 -13
  4. data/app/assets/javascripts/tenon/application.js +4 -0
  5. data/app/controllers/tenon/assets_controller.rb +22 -23
  6. data/app/controllers/tenon/base_controller.rb +22 -13
  7. data/app/controllers/tenon/index_controller.rb +0 -4
  8. data/app/controllers/tenon/item_versions_controller.rb +20 -8
  9. data/app/controllers/tenon/resources_controller.rb +100 -30
  10. data/app/controllers/tenon/simple_resources_controller.rb +7 -7
  11. data/app/controllers/tenon/styleguides_controller.rb +10 -0
  12. data/app/decorators/tenon/application_decorator.rb +4 -8
  13. data/app/decorators/tenon/asset_decorator.rb +0 -25
  14. data/app/filterers/tenon/generic_filterer.rb +24 -0
  15. data/app/form_builders/tenon/form_builder.rb +189 -19
  16. data/app/helpers/tenon/active_link_to_helper.rb +9 -0
  17. data/app/helpers/tenon/application_helper.rb +4 -23
  18. data/app/helpers/tenon/asset_helper.rb +2 -2
  19. data/app/helpers/tenon/back_to_index_path_helper.rb +13 -0
  20. data/app/helpers/tenon/breadcrumbs_helper.rb +33 -0
  21. data/app/helpers/tenon/flash_helper.rb +14 -0
  22. data/app/helpers/tenon/icon_helper.rb +7 -0
  23. data/app/helpers/tenon/input_block_helper.rb +25 -0
  24. data/app/helpers/tenon/item_version_helper.rb +22 -9
  25. data/app/helpers/tenon/nav_helper.rb +21 -17
  26. data/app/helpers/tenon/piece_helper.rb +2 -7
  27. data/app/helpers/tenon/platform_hints_helper.rb +3 -5
  28. data/app/helpers/tenon/styleguide_helper.rb +15 -0
  29. data/app/helpers/tenon/tenon_content_helper.rb +11 -6
  30. data/app/helpers/tenon/tenon_helper.rb +19 -9
  31. data/app/models/tenon/application_record.rb +5 -0
  32. data/app/models/tenon/asset.rb +2 -2
  33. data/app/models/tenon/item_asset.rb +1 -1
  34. data/app/models/tenon/item_version.rb +1 -1
  35. data/app/models/tenon/styleguide.rb +11 -0
  36. data/app/models/tenon/tenon_content/piece.rb +13 -13
  37. data/app/models/tenon/tenon_content/row.rb +1 -1
  38. data/app/models/tenon/tenon_content/row_types/four_column_image.rb +4 -4
  39. data/app/models/tenon/tenon_content/row_types/four_column_image_and_text.rb +8 -8
  40. data/app/models/tenon/tenon_content/row_types/four_column_text.rb +4 -4
  41. data/app/models/tenon/tenon_content/row_types/full_width_embedded_content.rb +1 -1
  42. data/app/models/tenon/tenon_content/row_types/full_width_image.rb +1 -1
  43. data/app/models/tenon/tenon_content/row_types/full_width_text.rb +1 -1
  44. data/app/models/tenon/tenon_content/row_types/left_image_right_text.rb +2 -2
  45. data/app/models/tenon/tenon_content/row_types/left_text_right_image.rb +2 -2
  46. data/app/models/tenon/tenon_content/row_types/left_wrapped_image_with_text.rb +2 -2
  47. data/app/models/tenon/tenon_content/row_types/right_wrapped_image_with_text.rb +2 -2
  48. data/app/models/tenon/tenon_content/row_types/six_column_image.rb +6 -6
  49. data/app/models/tenon/tenon_content/row_types/six_column_image_and_text.rb +12 -12
  50. data/app/models/tenon/tenon_content/row_types/three_column_image.rb +3 -3
  51. data/app/models/tenon/tenon_content/row_types/three_column_image_and_text.rb +6 -6
  52. data/app/models/tenon/tenon_content/row_types/three_column_text.rb +3 -3
  53. data/app/models/tenon/tenon_content/row_types/two_column_image.rb +2 -2
  54. data/app/models/tenon/tenon_content/row_types/two_column_image_and_text.rb +4 -4
  55. data/app/models/tenon/tenon_content/row_types/two_column_text.rb +2 -2
  56. data/app/models/tenon/tenon_content/row_types/two_column_with_background_image.rb +3 -3
  57. data/app/models/tenon/tenon_content/row_types.rb +17 -17
  58. data/app/policies/my_settings_policy.rb +2 -0
  59. data/app/policies/tenon/application_policy.rb +63 -0
  60. data/app/policies/tenon/asset_policy.rb +4 -0
  61. data/app/policies/tenon/item_version_policy.rb +4 -0
  62. data/app/serializers/item_version_serializer.rb +23 -0
  63. data/app/serializers/tenon/application_serializer.rb +37 -0
  64. data/app/serializers/tenon/asset_serializer.rb +5 -0
  65. data/app/serializers/tenon/paginating_serializer.rb +18 -0
  66. data/app/views/layouts/tenon/application.html.haml +28 -18
  67. data/app/views/tenon/assets/_form.html.haml +29 -12
  68. data/app/views/tenon/assets/crop.html.haml +13 -13
  69. data/app/views/tenon/assets/edit.html.haml +1 -12
  70. data/app/views/tenon/assets/index.html.haml +5 -19
  71. data/app/views/tenon/assets/new.html.haml +1 -0
  72. data/app/views/tenon/fields/_asset.html.haml +26 -27
  73. data/app/views/tenon/fields/_base.html.haml +7 -0
  74. data/app/views/tenon/fields/_base_inline.html.haml +4 -0
  75. data/app/views/tenon/fields/_check_box.html.haml +9 -0
  76. data/app/views/tenon/fields/_collection_select.html.haml +4 -0
  77. data/app/views/tenon/fields/_color_field.html.haml +3 -0
  78. data/app/views/tenon/fields/_date_picker.html.haml +2 -0
  79. data/app/views/tenon/fields/_date_time_picker.html.haml +6 -0
  80. data/app/views/tenon/fields/_email_field.html.haml +2 -0
  81. data/app/views/tenon/fields/_inline_check_box.html.haml +5 -0
  82. data/app/views/tenon/fields/_inline_radio_button.html.haml +5 -0
  83. data/app/views/tenon/fields/_number_field.html.haml +2 -0
  84. data/app/views/tenon/fields/_password_field.html.haml +2 -0
  85. data/app/views/tenon/fields/_phone_field.html.haml +2 -0
  86. data/app/views/tenon/fields/_radio_button.html.haml +9 -0
  87. data/app/views/tenon/fields/_rich_text.html.haml +3 -6
  88. data/app/views/tenon/fields/_select.html.haml +3 -0
  89. data/app/views/tenon/fields/_text_area.html.haml +2 -0
  90. data/app/views/tenon/fields/_text_field.html.haml +2 -0
  91. data/app/views/tenon/fields/_title_text_field.html.haml +2 -0
  92. data/app/views/tenon/fields/_url_field.html.haml +2 -0
  93. data/app/views/tenon/index/index.html.haml +8 -29
  94. data/app/views/tenon/item_assets/new.html.haml +20 -22
  95. data/app/views/tenon/item_versions/_item_version.json.jbuilder +4 -4
  96. data/app/views/tenon/item_versions/index.html.haml +14 -6
  97. data/app/views/tenon/item_versions/new.html.haml +8 -10
  98. data/app/views/tenon/settings/_contact.html.haml +14 -19
  99. data/app/views/tenon/settings/_general.html.haml +15 -11
  100. data/app/views/tenon/settings/_seo.html.haml +10 -9
  101. data/app/views/tenon/settings/show.html.haml +12 -31
  102. data/app/views/tenon/shared/_account_dropdown.html.haml +6 -0
  103. data/app/views/tenon/shared/_app_info.html.haml +5 -0
  104. data/app/views/tenon/shared/_breadcrumbs.html.haml +1 -0
  105. data/app/views/tenon/shared/_burger.html.haml +5 -0
  106. data/app/views/tenon/shared/_default_form_toolbar.html.haml +26 -0
  107. data/app/views/tenon/shared/_draft_controls.html.haml +18 -0
  108. data/app/views/tenon/shared/_i18n_language_nav.html.haml +2 -2
  109. data/app/views/tenon/shared/_main_nav.html.haml +3 -14
  110. data/app/views/tenon/shared/_platform_hints.html.haml +15 -15
  111. data/app/views/tenon/shared/_seo_fields.html.haml +8 -6
  112. data/app/views/tenon/shared/_site_title.html.haml +6 -0
  113. data/app/views/tenon/shared/_util_nav.html.haml +4 -27
  114. data/app/views/tenon/shared/_version_warning.html.haml +11 -11
  115. data/app/views/tenon/styleguides/_buttons.html.haml +118 -0
  116. data/app/views/tenon/styleguides/_colors.html.haml +59 -0
  117. data/app/views/tenon/styleguides/_forms.html.haml +4 -0
  118. data/app/views/tenon/styleguides/_icons.html.haml +21 -0
  119. data/app/views/tenon/styleguides/_typography.html.haml +27 -0
  120. data/app/views/tenon/styleguides/buttons/_colors.html.haml +43 -0
  121. data/app/views/tenon/styleguides/buttons/_styles.html.haml +59 -0
  122. data/app/views/tenon/styleguides/colors/_helpers.html.haml +48 -0
  123. data/app/views/tenon/styleguides/colors/_variables.html.haml +31 -0
  124. data/app/views/tenon/styleguides/forms/_default.html.haml +92 -0
  125. data/app/views/tenon/styleguides/forms/_html5.html.haml +31 -0
  126. data/app/views/tenon/styleguides/forms/_tenon_content.html.haml +15 -0
  127. data/app/views/tenon/styleguides/index.html.haml +52 -0
  128. data/app/views/tenon/styleguides/typography/_blockquote.html.haml +7 -0
  129. data/app/views/tenon/styleguides/typography/_inline.html.haml +8 -0
  130. data/app/views/tenon/styleguides/typography/_lists.html.haml +40 -0
  131. data/app/views/tenon/styleguides/typography/_paragraphs.html.haml +17 -0
  132. data/app/views/tenon/styleguides/typography/_small.html.haml +18 -0
  133. data/app/views/tenon/styleguides/typography/_styles.html.haml +14 -0
  134. data/app/views/tenon/styleguides/typography/_variables.html.haml +43 -0
  135. data/app/views/tenon/tenon_content/_display.html.haml +2 -2
  136. data/app/views/tenon/tenon_content/_embed_modal.html.haml +3 -3
  137. data/app/views/tenon/tenon_content/_fields.html.haml +21 -35
  138. data/app/views/tenon/tenon_content/_row.html.haml +14 -13
  139. data/app/views/tenon/tenon_content/{_builder.html.haml → _tenon_content.html.haml} +2 -2
  140. data/app/views/tenon/tenon_content/piece_types/form/_background_image.html.haml +6 -6
  141. data/app/views/tenon/tenon_content/piece_types/form/_embedded_content.html.haml +2 -1
  142. data/app/views/tenon/tenon_content/piece_types/form/_image.html.haml +37 -29
  143. data/app/views/tenon/tenon_content/row_types/display/_four_column_image.html.haml +4 -4
  144. data/app/views/tenon/tenon_content/row_types/display/_four_column_image_and_text.html.haml +3 -3
  145. data/app/views/tenon/tenon_content/row_types/display/_four_column_text.html.haml +4 -4
  146. data/app/views/tenon/tenon_content/row_types/display/_full_width_embedded_content.html.haml +3 -3
  147. data/app/views/tenon/tenon_content/row_types/display/_full_width_image.html.haml +3 -3
  148. data/app/views/tenon/tenon_content/row_types/display/_full_width_text.html.haml +3 -3
  149. data/app/views/tenon/tenon_content/row_types/display/_left_image_right_text.html.haml +8 -8
  150. data/app/views/tenon/tenon_content/row_types/display/_left_text_right_image.html.haml +7 -7
  151. data/app/views/tenon/tenon_content/row_types/display/_left_wrapped_image_with_text.html.haml +6 -6
  152. data/app/views/tenon/tenon_content/row_types/display/_right_wrapped_image_with_text.html.haml +7 -7
  153. data/app/views/tenon/tenon_content/row_types/display/_six_column_image.html.haml +4 -4
  154. data/app/views/tenon/tenon_content/row_types/display/_six_column_image_and_text.html.haml +5 -5
  155. data/app/views/tenon/tenon_content/row_types/display/_three_column_image.html.haml +4 -4
  156. data/app/views/tenon/tenon_content/row_types/display/_three_column_image_and_text.html.haml +4 -4
  157. data/app/views/tenon/tenon_content/row_types/display/_three_column_text.html.haml +4 -4
  158. data/app/views/tenon/tenon_content/row_types/display/_two_column_image.html.haml +3 -3
  159. data/app/views/tenon/tenon_content/row_types/display/_two_column_image_and_text.html.haml +4 -4
  160. data/app/views/tenon/tenon_content/row_types/display/_two_column_text.html.haml +3 -3
  161. data/app/views/tenon/tenon_content/row_types/display/_two_column_with_background_image.html.haml +3 -3
  162. data/app/views/tenon/tenon_content/row_types/form/_four_column_image.html.haml +2 -2
  163. data/app/views/tenon/tenon_content/row_types/form/_four_column_image_and_text.html.haml +3 -3
  164. data/app/views/tenon/tenon_content/row_types/form/_four_column_text.html.haml +3 -3
  165. data/app/views/tenon/tenon_content/row_types/form/_full_width_embedded_content.html.haml +3 -3
  166. data/app/views/tenon/tenon_content/row_types/form/_full_width_image.html.haml +3 -3
  167. data/app/views/tenon/tenon_content/row_types/form/_full_width_text.html.haml +3 -3
  168. data/app/views/tenon/tenon_content/row_types/form/_left_image_right_text.html.haml +9 -8
  169. data/app/views/tenon/tenon_content/row_types/form/_left_text_right_image.html.haml +6 -6
  170. data/app/views/tenon/tenon_content/row_types/form/_left_wrapped_image_with_text.html.haml +7 -7
  171. data/app/views/tenon/tenon_content/row_types/form/_right_wrapped_image_with_text.html.haml +7 -7
  172. data/app/views/tenon/tenon_content/row_types/form/_six_column_image.html.haml +2 -2
  173. data/app/views/tenon/tenon_content/row_types/form/_six_column_image_and_text.html.haml +3 -3
  174. data/app/views/tenon/tenon_content/row_types/form/_three_column_image.html.haml +2 -2
  175. data/app/views/tenon/tenon_content/row_types/form/_three_column_image_and_text.html.haml +3 -3
  176. data/app/views/tenon/tenon_content/row_types/form/_three_column_text.html.haml +3 -3
  177. data/app/views/tenon/tenon_content/row_types/form/_two_column_image.html.haml +2 -2
  178. data/app/views/tenon/tenon_content/row_types/form/_two_column_image_and_text.html.haml +3 -3
  179. data/app/views/tenon/tenon_content/row_types/form/_two_column_text.html.haml +3 -3
  180. data/app/views/tenon/tenon_content/row_types/form/_two_column_with_background_image.html.haml +2 -2
  181. data/config/initializers/class_extensions/hash.rb +1 -1
  182. data/config/initializers/client_side_validations.rb +27 -0
  183. data/config/initializers/tenon.rb +0 -14
  184. data/config/routes.rb +10 -46
  185. data/db/migrate/20151117171000_drop_default_tenon_resources.rb +15 -0
  186. data/db/migrate/20160119185645_remove_tenon_users.rb +7 -0
  187. data/db/migrate/20160126123154_remove_tenon_callouts_subscribers.rb +8 -0
  188. data/db/migrate/20160126194219_convert_piece_size_to_int.rb +6 -0
  189. data/lib/ckeditor/plugins/pastefromword/filter/default.js +1 -1
  190. data/lib/generators/tenon/scaffold/scaffold_generator.rb +52 -27
  191. data/lib/generators/tenon/scaffold/templates/controller.rb +7 -4
  192. data/lib/generators/tenon/scaffold/templates/policy.rb +2 -0
  193. data/lib/generators/tenon/scaffold/templates/serializer.rb +2 -0
  194. data/lib/generators/tenon/scaffold/templates/view__form.html.haml +49 -54
  195. data/lib/generators/tenon/scaffold/templates/view_edit.html.haml +1 -8
  196. data/lib/generators/tenon/scaffold/templates/view_index.html.haml +8 -24
  197. data/lib/generators/tenon/scaffold/templates/view_new.html.haml +1 -8
  198. data/lib/tasks/tenon_tasks.rake +2 -2
  199. data/lib/templates/active_record/model/model.rb +12 -13
  200. data/lib/templates/migration/templates/create_table_migration.rb +1 -1
  201. data/lib/tenon/can_be_foreign.rb +1 -1
  202. data/lib/tenon/engine.rb +26 -32
  203. data/lib/tenon/filterers/base_filterer.rb +64 -0
  204. data/lib/tenon/has_history/attr_serializer.rb +6 -6
  205. data/lib/tenon/proxy_attachment.rb +2 -2
  206. data/lib/tenon/version.rb +1 -1
  207. data/lib/tenon.rb +1 -3
  208. data/vendor/assets/javascripts/bootstrap.modal.js +1 -1
  209. data/vendor/assets/javascripts/date_picker/picker.date.js +1429 -0
  210. data/vendor/assets/javascripts/date_picker/picker.js +1122 -0
  211. data/vendor/assets/javascripts/number-to-words.es6 +35 -0
  212. data/vendor/assets/stylesheets/bootstrap.scss +1 -1
  213. metadata +206 -972
  214. data/app/assets/images/tenon/icons/delete.png +0 -0
  215. data/app/assets/images/tenon/icons/edit.png +0 -0
  216. data/app/assets/images/tenon/icons/thumbdown.png +0 -0
  217. data/app/assets/images/tenon/icons/thumbup.png +0 -0
  218. data/app/assets/images/tenon/select2-spinner.gif +0 -0
  219. data/app/assets/images/tenon/select2.png +0 -0
  220. data/app/assets/images/tenon/select2x2.png +0 -0
  221. data/app/assets/javascripts/tenon/controllers/assets.js.coffee +0 -3
  222. data/app/assets/javascripts/tenon/controllers/index.js.coffee +0 -6
  223. data/app/assets/javascripts/tenon/controllers/tenon.js.coffee +0 -1
  224. data/app/assets/javascripts/tenon/features/asset_attachment.js.coffee +0 -37
  225. data/app/assets/javascripts/tenon/features/asset_cropping.js.coffee +0 -49
  226. data/app/assets/javascripts/tenon/features/asset_detachment.js.coffee +0 -11
  227. data/app/assets/javascripts/tenon/features/asset_list_post_crop_handler.js.coffee +0 -15
  228. data/app/assets/javascripts/tenon/features/asset_uploader.js.coffee +0 -88
  229. data/app/assets/javascripts/tenon/features/box_toggles.js.coffee +0 -17
  230. data/app/assets/javascripts/tenon/features/cocoon_hooks.js.coffee +0 -12
  231. data/app/assets/javascripts/tenon/features/date_time_picker.js.coffee +0 -25
  232. data/app/assets/javascripts/tenon/features/editor.js.coffee +0 -24
  233. data/app/assets/javascripts/tenon/features/file_select_widget.js.erb +0 -39
  234. data/app/assets/javascripts/tenon/features/flash.js.coffee +0 -13
  235. data/app/assets/javascripts/tenon/features/focus_first_field.js.coffee +0 -8
  236. data/app/assets/javascripts/tenon/features/hamburger_navigation.js.coffee +0 -26
  237. data/app/assets/javascripts/tenon/features/header_menu.js.coffee +0 -20
  238. data/app/assets/javascripts/tenon/features/i18n_fields.js.coffee +0 -23
  239. data/app/assets/javascripts/tenon/features/infinite_loading.js.coffee +0 -12
  240. data/app/assets/javascripts/tenon/features/item_version_autosave.js.coffee +0 -35
  241. data/app/assets/javascripts/tenon/features/item_version_index_handler.js.coffee +0 -3
  242. data/app/assets/javascripts/tenon/features/main_menu.js.coffee +0 -11
  243. data/app/assets/javascripts/tenon/features/modal_forms.js.coffee +0 -44
  244. data/app/assets/javascripts/tenon/features/modal_windows.js.coffee +0 -104
  245. data/app/assets/javascripts/tenon/features/multiple_asset_attachment.js.coffee +0 -8
  246. data/app/assets/javascripts/tenon/features/nested_fields.js +0 -95
  247. data/app/assets/javascripts/tenon/features/nested_lists.js +0 -10
  248. data/app/assets/javascripts/tenon/features/nested_set_writer.js.coffee +0 -65
  249. data/app/assets/javascripts/tenon/features/new_item_version_handler.js.coffee +0 -32
  250. data/app/assets/javascripts/tenon/features/pagination.js.coffee +0 -21
  251. data/app/assets/javascripts/tenon/features/protect_changes.js.coffee +0 -25
  252. data/app/assets/javascripts/tenon/features/quick_search.js.coffee +0 -41
  253. data/app/assets/javascripts/tenon/features/record_approval.js.coffee +0 -33
  254. data/app/assets/javascripts/tenon/features/record_boolean_toggle.js.coffee +0 -37
  255. data/app/assets/javascripts/tenon/features/record_deletion.js.coffee +0 -24
  256. data/app/assets/javascripts/tenon/features/record_list.js.coffee +0 -86
  257. data/app/assets/javascripts/tenon/features/record_list_updater.js.coffee +0 -56
  258. data/app/assets/javascripts/tenon/features/s3_direct_upload.js.coffee +0 -43
  259. data/app/assets/javascripts/tenon/features/sidebar_active_links.js.coffee +0 -20
  260. data/app/assets/javascripts/tenon/features/sortable_nested_fields.js.coffee +0 -17
  261. data/app/assets/javascripts/tenon/features/tenon_content/aesthetics.js.coffee +0 -31
  262. data/app/assets/javascripts/tenon/features/tenon_content/asset_attachment.js.coffee +0 -15
  263. data/app/assets/javascripts/tenon/features/tenon_content/asset_link.js.coffee +0 -11
  264. data/app/assets/javascripts/tenon/features/tenon_content/base.js.coffee +0 -16
  265. data/app/assets/javascripts/tenon/features/tenon_content/bottombar_toggler.js.coffee +0 -15
  266. data/app/assets/javascripts/tenon/features/tenon_content/caption_toggler.js.coffee +0 -11
  267. data/app/assets/javascripts/tenon/features/tenon_content/column_sizing.js.coffee +0 -53
  268. data/app/assets/javascripts/tenon/features/tenon_content/column_swap.js.coffee +0 -44
  269. data/app/assets/javascripts/tenon/features/tenon_content/editor.js.coffee +0 -34
  270. data/app/assets/javascripts/tenon/features/tenon_content/embedded_content_modal_handler.js.coffee +0 -12
  271. data/app/assets/javascripts/tenon/features/tenon_content/image_asset_link.js.coffee +0 -5
  272. data/app/assets/javascripts/tenon/features/tenon_content/image_controls.js.coffee +0 -63
  273. data/app/assets/javascripts/tenon/features/tenon_content/image_links.js.coffee +0 -54
  274. data/app/assets/javascripts/tenon/features/tenon_content/library.js.coffee +0 -39
  275. data/app/assets/javascripts/tenon/features/tenon_content/library_filter.js.coffee +0 -17
  276. data/app/assets/javascripts/tenon/features/tenon_content/pop_out.js.coffee +0 -46
  277. data/app/assets/javascripts/tenon/features/tenon_content/post_crop_handler.js.coffee +0 -22
  278. data/app/assets/javascripts/tenon/features/tenon_content/sidebar_navigation.js.coffee +0 -26
  279. data/app/assets/javascripts/tenon/features/tenon_content/sortable.js.coffee +0 -18
  280. data/app/assets/javascripts/tenon/features/tenon_content/stretch_to_fill.js.coffee +0 -50
  281. data/app/assets/javascripts/tenon/features/tenon_content/wrapped_sizing.js.coffee +0 -48
  282. data/app/assets/javascripts/tenon/features/tenon_content.js.coffee +0 -1
  283. data/app/assets/javascripts/tenon/features/video_feeds.js.coffee +0 -4
  284. data/app/assets/javascripts/tenon/manifest.json +0 -44
  285. data/app/assets/javascripts/tenon/medium-on-tenon.js +0 -1465
  286. data/app/assets/javascripts/tenon/templates/assets/asset_field.jst.eco +0 -17
  287. data/app/assets/javascripts/tenon/templates/assets/asset_progress.jst.eco +0 -8
  288. data/app/assets/javascripts/tenon/templates/assets/asset_row.jst.eco +0 -21
  289. data/app/assets/javascripts/tenon/templates/assets/asset_select.jst.eco +0 -1
  290. data/app/assets/javascripts/tenon/templates/comments/comment_row.jst.eco +0 -23
  291. data/app/assets/javascripts/tenon/templates/contacts/contact_row.jst.eco +0 -56
  292. data/app/assets/javascripts/tenon/templates/errors.jst.eco +0 -13
  293. data/app/assets/javascripts/tenon/templates/events/event_row.jst.eco +0 -22
  294. data/app/assets/javascripts/tenon/templates/galleries/gallery_row.jst.eco +0 -12
  295. data/app/assets/javascripts/tenon/templates/item_versions/item_version_row.jst.eco +0 -22
  296. data/app/assets/javascripts/tenon/templates/modal.jst.eco +0 -14
  297. data/app/assets/javascripts/tenon/templates/pages/page_row.jst.eco +0 -21
  298. data/app/assets/javascripts/tenon/templates/post_categories/post_category_row.jst.eco +0 -12
  299. data/app/assets/javascripts/tenon/templates/posts/post_row.jst.eco +0 -30
  300. data/app/assets/javascripts/tenon/templates/redirects/redirect_row.jst.eco +0 -18
  301. data/app/assets/javascripts/tenon/templates/tenon_callouts/tenon_callout_row.jst.eco +0 -19
  302. data/app/assets/javascripts/tenon/templates/tenon_content/popped_out.jst.eco +0 -6
  303. data/app/assets/javascripts/tenon/templates/users/user_row.jst.eco +0 -21
  304. data/app/assets/javascripts/tenon/tenon.js +0 -68
  305. data/app/assets/javascripts/tenon/tenon_dispatcher.js.coffee +0 -25
  306. data/app/assets/javascripts/tenon/tenon_manifest.js +0 -45
  307. data/app/assets/stylesheets/tenon/colors-custom.scss +0 -25
  308. data/app/assets/stylesheets/tenon/colors-named.scss +0 -22
  309. data/app/assets/stylesheets/tenon/helpers.scss +0 -28
  310. data/app/assets/stylesheets/tenon/layout/assets.scss +0 -49
  311. data/app/assets/stylesheets/tenon/layout/breakpoints.scss +0 -48
  312. data/app/assets/stylesheets/tenon/layout/global.scss +0 -36
  313. data/app/assets/stylesheets/tenon/layout/grid.scss +0 -145
  314. data/app/assets/stylesheets/tenon/layout/main-nav.scss +0 -149
  315. data/app/assets/stylesheets/tenon/layout/margins.scss +0 -3
  316. data/app/assets/stylesheets/tenon/layout/mobile.scss +0 -5
  317. data/app/assets/stylesheets/tenon/layout/sidebar.scss +0 -102
  318. data/app/assets/stylesheets/tenon/layout/util-nav.scss +0 -51
  319. data/app/assets/stylesheets/tenon/mixins.scss +0 -81
  320. data/app/assets/stylesheets/tenon/styleguide.scss +0 -6
  321. data/app/assets/stylesheets/tenon/tenon.scss +0 -71
  322. data/app/assets/stylesheets/tenon/typography.scss +0 -65
  323. data/app/assets/stylesheets/tenon/ui/alerts.scss +0 -65
  324. data/app/assets/stylesheets/tenon/ui/asset-attachment.scss +0 -136
  325. data/app/assets/stylesheets/tenon/ui/asset-cropping.scss +0 -51
  326. data/app/assets/stylesheets/tenon/ui/asset-uploads.scss +0 -26
  327. data/app/assets/stylesheets/tenon/ui/buttons.scss +0 -65
  328. data/app/assets/stylesheets/tenon/ui/callouts.scss +0 -26
  329. data/app/assets/stylesheets/tenon/ui/comments.scss +0 -57
  330. data/app/assets/stylesheets/tenon/ui/forms.scss +0 -204
  331. data/app/assets/stylesheets/tenon/ui/generic-loader.scss +0 -21
  332. data/app/assets/stylesheets/tenon/ui/header-tools.scss +0 -116
  333. data/app/assets/stylesheets/tenon/ui/i18n.scss +0 -18
  334. data/app/assets/stylesheets/tenon/ui/list-style-toggle.scss +0 -39
  335. data/app/assets/stylesheets/tenon/ui/login.scss +0 -97
  336. data/app/assets/stylesheets/tenon/ui/medium-editor.scss +0 -114
  337. data/app/assets/stylesheets/tenon/ui/modals.scss +0 -179
  338. data/app/assets/stylesheets/tenon/ui/pagination.scss +0 -113
  339. data/app/assets/stylesheets/tenon/ui/progress-bars.scss +0 -131
  340. data/app/assets/stylesheets/tenon/ui/quick-search.scss +0 -20
  341. data/app/assets/stylesheets/tenon/ui/record-grids.scss +0 -95
  342. data/app/assets/stylesheets/tenon/ui/record-lists.scss +0 -154
  343. data/app/assets/stylesheets/tenon/ui/section-headers.scss +0 -30
  344. data/app/assets/stylesheets/tenon/ui/select2-custom.scss +0 -70
  345. data/app/assets/stylesheets/tenon/ui/tables.scss +0 -19
  346. data/app/assets/stylesheets/tenon/ui/tabs.scss +0 -7
  347. data/app/assets/stylesheets/tenon/ui/tenon-content-library.scss +0 -80
  348. data/app/assets/stylesheets/tenon/ui/tenon-content-popped-out.scss +0 -92
  349. data/app/assets/stylesheets/tenon/ui/tenon-content.scss +0 -318
  350. data/app/assets/stylesheets/tenon/ui/thinking.scss +0 -3
  351. data/app/assets/stylesheets/tenon/ui/toolbox.scss +0 -31
  352. data/app/assets/stylesheets/tenon/z-indexes.scss +0 -35
  353. data/app/assets/stylesheets/tenon_addons.scss +0 -1
  354. data/app/controllers/tenon/comments_controller.rb +0 -51
  355. data/app/controllers/tenon/contacts_controller.rb +0 -65
  356. data/app/controllers/tenon/events_controller.rb +0 -9
  357. data/app/controllers/tenon/galleries_controller.rb +0 -9
  358. data/app/controllers/tenon/pages_controller.rb +0 -51
  359. data/app/controllers/tenon/post_categories_controller.rb +0 -9
  360. data/app/controllers/tenon/posts_controller.rb +0 -28
  361. data/app/controllers/tenon/redirects_controller.rb +0 -25
  362. data/app/controllers/tenon/tenon_callouts_controller.rb +0 -7
  363. data/app/controllers/tenon/users_controller.rb +0 -64
  364. data/app/decorators/tenon/comment_decorator.rb +0 -22
  365. data/app/decorators/tenon/contact_decorator.rb +0 -13
  366. data/app/decorators/tenon/event_decorator.rb +0 -11
  367. data/app/decorators/tenon/gallery_decorator.rb +0 -4
  368. data/app/decorators/tenon/page_decorator.rb +0 -11
  369. data/app/decorators/tenon/post_category_decorator.rb +0 -4
  370. data/app/decorators/tenon/post_decorator.rb +0 -4
  371. data/app/decorators/tenon/redirect_decorator.rb +0 -2
  372. data/app/decorators/tenon/tenon_callout_decorator.rb +0 -4
  373. data/app/decorators/tenon/user_decorator.rb +0 -4
  374. data/app/mailers/tenon/comment_mailer.rb +0 -16
  375. data/app/mailers/tenon/contact_mailer.rb +0 -15
  376. data/app/models/ability.rb +0 -32
  377. data/app/models/tenon/comment.rb +0 -47
  378. data/app/models/tenon/comment_subscriber.rb +0 -7
  379. data/app/models/tenon/contact.rb +0 -46
  380. data/app/models/tenon/event.rb +0 -45
  381. data/app/models/tenon/gallery.rb +0 -15
  382. data/app/models/tenon/page.rb +0 -105
  383. data/app/models/tenon/photo.rb +0 -10
  384. data/app/models/tenon/post.rb +0 -35
  385. data/app/models/tenon/post_category.rb +0 -7
  386. data/app/models/tenon/redirect.rb +0 -17
  387. data/app/models/tenon/role.rb +0 -8
  388. data/app/models/tenon/role_assignment.rb +0 -6
  389. data/app/models/tenon/s3_direct_upload.rb +0 -75
  390. data/app/models/tenon/tenon_callout.rb +0 -16
  391. data/app/models/tenon/user.rb +0 -60
  392. data/app/views/devise/confirmations/new.html.erb +0 -12
  393. data/app/views/devise/confirmations/new.html.haml +0 -0
  394. data/app/views/devise/mailer/confirmation_instructions.html.haml +0 -7
  395. data/app/views/devise/mailer/reset_password_instructions.html.haml +0 -15
  396. data/app/views/devise/mailer/unlock_instructions.html.haml +0 -9
  397. data/app/views/devise/passwords/edit.html.haml +0 -22
  398. data/app/views/devise/passwords/new.html.haml +0 -17
  399. data/app/views/devise/registrations/edit.html.haml +0 -0
  400. data/app/views/devise/registrations/new.html.haml +0 -22
  401. data/app/views/devise/sessions/new.html.haml +0 -28
  402. data/app/views/devise/shared/_links.erb +0 -19
  403. data/app/views/devise/shared/_links.haml +0 -0
  404. data/app/views/devise/unlocks/new.html.erb +0 -12
  405. data/app/views/devise/unlocks/new.html.haml +0 -0
  406. data/app/views/layouts/tenon/login.html.haml +0 -19
  407. data/app/views/tenon/assets/_asset.json.jbuilder +0 -7
  408. data/app/views/tenon/assets/_sidebar_index.html.haml +0 -18
  409. data/app/views/tenon/assets/create.json.jbuilder +0 -1
  410. data/app/views/tenon/assets/index.json.jbuilder +0 -5
  411. data/app/views/tenon/assets/update.json.jbuilder +0 -1
  412. data/app/views/tenon/comment_mailer/comment_notification.html.haml +0 -7
  413. data/app/views/tenon/comments/_sidebar_index.html.haml +0 -22
  414. data/app/views/tenon/comments/index.html.haml +0 -19
  415. data/app/views/tenon/comments/index.json.jbuilder +0 -22
  416. data/app/views/tenon/contact_mailer/contact_notification.html.haml +0 -15
  417. data/app/views/tenon/contacts/_sidebar_index.html.haml +0 -32
  418. data/app/views/tenon/contacts/index.html.haml +0 -19
  419. data/app/views/tenon/contacts/index.json.jbuilder +0 -15
  420. data/app/views/tenon/events/_form.html.haml +0 -44
  421. data/app/views/tenon/events/edit.html.haml +0 -8
  422. data/app/views/tenon/events/index.html.haml +0 -23
  423. data/app/views/tenon/events/index.json.jbuilder +0 -16
  424. data/app/views/tenon/events/new.html.haml +0 -8
  425. data/app/views/tenon/galleries/_form.html.haml +0 -38
  426. data/app/views/tenon/galleries/_photo_fields.html.haml +0 -3
  427. data/app/views/tenon/galleries/edit.html.haml +0 -8
  428. data/app/views/tenon/galleries/index.html.haml +0 -22
  429. data/app/views/tenon/galleries/index.json.jbuilder +0 -15
  430. data/app/views/tenon/galleries/new.html.haml +0 -8
  431. data/app/views/tenon/pages/_form.html.haml +0 -58
  432. data/app/views/tenon/pages/edit.html.haml +0 -8
  433. data/app/views/tenon/pages/index.html.haml +0 -23
  434. data/app/views/tenon/pages/index.json.jbuilder +0 -22
  435. data/app/views/tenon/pages/new.html.haml +0 -8
  436. data/app/views/tenon/post_categories/_form.html.haml +0 -11
  437. data/app/views/tenon/post_categories/_post_category.json.jbuilder +0 -2
  438. data/app/views/tenon/post_categories/create.json.jbuilder +0 -1
  439. data/app/views/tenon/post_categories/edit.html.haml +0 -1
  440. data/app/views/tenon/post_categories/index.html.haml +0 -30
  441. data/app/views/tenon/post_categories/index.json.jbuilder +0 -5
  442. data/app/views/tenon/post_categories/update.json.jbuilder +0 -1
  443. data/app/views/tenon/posts/_form.html.haml +0 -65
  444. data/app/views/tenon/posts/edit.html.haml +0 -8
  445. data/app/views/tenon/posts/index.html.haml +0 -27
  446. data/app/views/tenon/posts/index.json.jbuilder +0 -20
  447. data/app/views/tenon/posts/new.html.haml +0 -8
  448. data/app/views/tenon/redirects/_form.html.haml +0 -42
  449. data/app/views/tenon/redirects/_redirect.json.jbuilder +0 -3
  450. data/app/views/tenon/redirects/edit.html.haml +0 -8
  451. data/app/views/tenon/redirects/index.html.haml +0 -29
  452. data/app/views/tenon/redirects/index.json.jbuilder +0 -5
  453. data/app/views/tenon/redirects/new.html.haml +0 -8
  454. data/app/views/tenon/shared/_posts_nav.html.haml +0 -8
  455. data/app/views/tenon/shared/menu_items/_assets.html.haml +0 -3
  456. data/app/views/tenon/shared/menu_items/_comments.html.haml +0 -2
  457. data/app/views/tenon/shared/menu_items/_events.html.haml +0 -6
  458. data/app/views/tenon/shared/menu_items/_galleries.html.haml +0 -6
  459. data/app/views/tenon/shared/menu_items/_pages.html.haml +0 -6
  460. data/app/views/tenon/shared/menu_items/_posts.html.haml +0 -7
  461. data/app/views/tenon/shared/menu_items/_settings.html.haml +0 -2
  462. data/app/views/tenon/shared/menu_items/_users.html.haml +0 -2
  463. data/app/views/tenon/shared/section_header/_quick_search.html.haml +0 -3
  464. data/app/views/tenon/shared/section_header/_sidebar_toggle.html.haml +0 -1
  465. data/app/views/tenon/tenon_callouts/_form.html.haml +0 -45
  466. data/app/views/tenon/tenon_callouts/_tenon_callout.json.jbuilder +0 -1
  467. data/app/views/tenon/tenon_callouts/edit.html.haml +0 -8
  468. data/app/views/tenon/tenon_callouts/index.html.haml +0 -24
  469. data/app/views/tenon/tenon_callouts/index.json.jbuilder +0 -5
  470. data/app/views/tenon/tenon_callouts/new.html.haml +0 -8
  471. data/app/views/tenon/users/_form.html.haml +0 -50
  472. data/app/views/tenon/users/edit.html.haml +0 -8
  473. data/app/views/tenon/users/index.html.haml +0 -23
  474. data/app/views/tenon/users/index.json.jbuilder +0 -28
  475. data/app/views/tenon/users/new.html.haml +0 -8
  476. data/config/initializers/devise.rb +0 -258
  477. data/config/locales/devise.en.yml +0 -60
  478. data/config/locales/tenon.en.yml +0 -313
  479. data/db/seeds.rb +0 -47
  480. data/lib/generators/tenon/scaffold/templates/view__item.json.jbuilder +0 -1
  481. data/lib/generators/tenon/scaffold/templates/view_index.json.jbuilder +0 -5
  482. data/lib/generators/tenon/scaffold/templates/view_item_row.jst.eco +0 -13
  483. data/lib/generators/tenon/scaffold_small/scaffold_small_generator.rb +0 -8
  484. data/lib/generators/tenon/scaffold_small/templates/controller.rb +0 -19
  485. data/lib/generators/tenon/scaffold_small/templates/decorator.rb +0 -2
  486. data/lib/generators/tenon/scaffold_small/templates/view__form.html.haml +0 -26
  487. data/lib/generators/tenon/scaffold_small/templates/view__item.json.jbuilder +0 -2
  488. data/lib/generators/tenon/scaffold_small/templates/view_create.json.jbuilder +0 -1
  489. data/lib/generators/tenon/scaffold_small/templates/view_index.html.haml +0 -27
  490. data/lib/generators/tenon/scaffold_small/templates/view_index.json.jbuilder +0 -5
  491. data/lib/generators/tenon/scaffold_small/templates/view_item_row.jst.eco +0 -12
  492. data/lib/generators/tenon/scaffold_small/templates/view_update.json.jbuilder +0 -1
  493. data/lib/tenon/can_have_comments.rb +0 -16
  494. data/lib/tenon/config/events.rb +0 -29
  495. data/lib/tenon/factories/comments.rb +0 -10
  496. data/lib/tenon/factories/contacts.rb +0 -9
  497. data/lib/tenon/factories/events.rb +0 -7
  498. data/lib/tenon/factories/galleries.rb +0 -5
  499. data/lib/tenon/factories/pages.rb +0 -5
  500. data/lib/tenon/factories/posts.rb +0 -10
  501. data/lib/tenon/factories/redirects.rb +0 -9
  502. data/lib/tenon/factories/tenon_callouts.rb +0 -9
  503. data/lib/tenon/factories/users.rb +0 -26
  504. data/lib/tenon/warning_generator.rb +0 -29
  505. data/spec/controllers/tenon/assets_controller_spec.rb +0 -262
  506. data/spec/controllers/tenon/comments_controller_spec.rb +0 -173
  507. data/spec/controllers/tenon/contacts_controller_spec.rb +0 -174
  508. data/spec/controllers/tenon/index_controller_spec.rb +0 -31
  509. data/spec/controllers/tenon/item_assets_controller_spec.rb +0 -29
  510. data/spec/controllers/tenon/pages_controller_spec.rb +0 -67
  511. data/spec/controllers/tenon/posts_controller_spec.rb +0 -31
  512. data/spec/controllers/tenon/resources_controller_spec.rb +0 -352
  513. data/spec/controllers/tenon/settings_controller_spec.rb +0 -65
  514. data/spec/controllers/tenon/simple_resources_controller_spec.rb +0 -42
  515. data/spec/controllers/tenon/users_controller_spec.rb +0 -112
  516. data/spec/decorators/tenon/application_decorator_spec.rb +0 -75
  517. data/spec/decorators/tenon/asset_decorator_spec.rb +0 -101
  518. data/spec/decorators/tenon/comment_decorator_spec.rb +0 -43
  519. data/spec/decorators/tenon/contact_decorator_spec.rb +0 -24
  520. data/spec/decorators/tenon/event_decorator_spec.rb +0 -26
  521. data/spec/decorators/tenon/page_decorator_spec.rb +0 -23
  522. data/spec/decorators/tenon/tenon_content/row_type_decorator_spec.rb +0 -20
  523. data/spec/features/settings_spec.rb +0 -75
  524. data/spec/features/tenon/assets_spec.rb +0 -88
  525. data/spec/features/tenon/comments_spec.rb +0 -51
  526. data/spec/features/tenon/contacts_spec.rb +0 -51
  527. data/spec/features/tenon/events_spec.rb +0 -104
  528. data/spec/features/tenon/galleries_spec.rb +0 -107
  529. data/spec/features/tenon/i18n_spec.rb +0 -106
  530. data/spec/features/tenon/pages_spec.rb +0 -97
  531. data/spec/features/tenon/post_categories_spec.rb +0 -91
  532. data/spec/features/tenon/posts_spec.rb +0 -93
  533. data/spec/features/tenon/tenon_callouts_spec.rb +0 -101
  534. data/spec/features/tenon/users_spec.rb +0 -98
  535. data/spec/fixtures/files/test.png +0 -0
  536. data/spec/lib/tenon/asset_style_generator_spec.rb +0 -84
  537. data/spec/lib/tenon/can_be_foreign_spec.rb +0 -20
  538. data/spec/lib/tenon/can_have_comments_spec.rb +0 -22
  539. data/spec/lib/tenon/has_asset_spec.rb +0 -46
  540. data/spec/lib/tenon/has_history/attr_serializer_spec.rb +0 -63
  541. data/spec/lib/tenon/i18n_lookup_spec.rb +0 -88
  542. data/spec/lib/tenon/proxy_attachment_spec.rb +0 -96
  543. data/spec/lib/tenon/reorderable_spec.rb +0 -25
  544. data/spec/lib/tenon/tenon_content_spec.rb +0 -22
  545. data/spec/lib/tenon/warning_generator_spec.rb +0 -153
  546. data/spec/models/tenon/asset_spec.rb +0 -98
  547. data/spec/models/tenon/comment_spec.rb +0 -81
  548. data/spec/models/tenon/contact_spec.rb +0 -62
  549. data/spec/models/tenon/event_spec.rb +0 -79
  550. data/spec/models/tenon/gallery_spec.rb +0 -12
  551. data/spec/models/tenon/item_asset_spec.rb +0 -19
  552. data/spec/models/tenon/my_settings_spec.rb +0 -27
  553. data/spec/models/tenon/page_spec.rb +0 -60
  554. data/spec/models/tenon/post_spec.rb +0 -48
  555. data/spec/models/tenon/redirect_spec.rb +0 -31
  556. data/spec/models/tenon/tenon_callout_spec.rb +0 -10
  557. data/spec/models/tenon/tenon_content/row_spec.rb +0 -34
  558. data/spec/models/tenon/user_spec.rb +0 -59
  559. data/spec/services/tenon/redirector_spec.rb +0 -30
  560. data/spec/spec_helper.rb +0 -79
  561. data/spec/support/integration_example_group.rb +0 -35
  562. data/spec/support/request_helpers.rb +0 -10
  563. data/vendor/assets/javascripts/backstretch.js +0 -4
  564. data/vendor/assets/javascripts/bootstrap.collapse.js +0 -179
  565. data/vendor/assets/javascripts/bootstrap.datetimepicker.js +0 -954
  566. data/vendor/assets/javascripts/bootstrap.tabs.js +0 -135
  567. data/vendor/assets/javascripts/cufon/Aller_400.font.js +0 -7
  568. data/vendor/assets/javascripts/cufon/Aller_700.font.js +0 -7
  569. data/vendor/assets/javascripts/cufon/cufon.js +0 -7
  570. data/vendor/assets/javascripts/jquery.corner.js +0 -249
  571. data/vendor/assets/javascripts/jquery.hoverIntent.js +0 -115
  572. data/vendor/assets/javascripts/jquery.radioSlider.js +0 -55
  573. data/vendor/assets/javascripts/jscrollpane.js +0 -1435
  574. data/vendor/assets/javascripts/select2.js +0 -3
  575. data/vendor/assets/javascripts/underscore.inflection.js +0 -177
  576. data/vendor/assets/stylesheets/bootstrap.datetimepicker.css +0 -152
  577. data/vendor/assets/stylesheets/bootstrap.tables.scss +0 -201
  578. data/vendor/assets/stylesheets/jscrollpane.scss +0 -20
  579. data/vendor/assets/stylesheets/select2.css +0 -1
data/README.rdoc CHANGED
@@ -1,5 +1,7 @@
1
1
  = Tenon
2
2
 
3
+ Tenon 2.0 runs on Rails 5 and you should probably use at least Ruby 2.3.0. Stay fresh.
4
+
3
5
  == License
4
6
 
5
7
  This project uses the MIT-LICENSE. Do whatever you want with it as long as you
@@ -10,58 +12,58 @@ depends.
10
12
 
11
13
  In your Gemfile
12
14
 
13
- gem 'tenon'
15
+ gem 'tenon'
14
16
 
15
17
  and then bundle install.
16
18
 
17
19
  in config/application.rb:
18
20
 
19
- require 'active_record/railtie'
21
+ require 'active_record/railtie'
20
22
 
21
23
 
22
24
  in config/routes.rb:
23
25
 
24
- mount Tenon::Engine => '/tenon'
26
+ mount Tenon::Engine => '/tenon'
25
27
 
26
28
 
27
29
  Run command:
28
30
 
29
- $ rake tenon:install:migrations
30
- $ rake db:migrate
31
+ $ rake tenon:install:migrations
32
+ $ rake db:migrate
31
33
 
32
34
 
33
35
  You will need to have a database set up at this point. Currently Tenon requires that you use postgres.
34
36
 
35
37
  Running rake db:migrate probably threw up a devise error. Create config/initializers/devise.rb and paste:
36
38
 
37
- Devise.setup do |config|
38
- ## paste the secret key line from the error output ##
39
- end
39
+ Devise.setup do |config|
40
+ ## paste the secret key line from the error output ##
41
+ end
40
42
 
41
43
 
42
44
  Run that command again:
43
45
 
44
- $ rake db:migrate
46
+ $ rake db:migrate
45
47
 
46
48
 
47
49
  Install the Tenon helpers in app/controllers/application_controller.rb:
48
50
 
49
- helper Tenon::Engine.helpers
51
+ helper Tenon::Engine.helpers
50
52
 
51
53
  Install the necessary files to run and customize Tenon (this is now required):
52
54
 
53
- $ rails generate tenon:install
55
+ $ rails generate tenon:install
54
56
 
55
57
  To run seed data (such as creating an admin user) from Tenon, open console and run:
56
58
 
57
- ENV['PASSWORD'] = 'password' # or something at least 8 chars long
58
- Tenon::Engine.load_seed
59
+ ENV['PASSWORD'] = 'password' # or something at least 8 chars long
60
+ Tenon::Engine.load_seed
59
61
 
60
62
  Restart your app and navigate to /tenon
61
63
 
62
64
  If you want to be able to use rspec, which would be good, you will also need to run:
63
65
 
64
- bundle exec rails generate rspec:install
66
+ bundle exec rails generate rspec:install
65
67
 
66
68
  ==== Note about CKEditor
67
69
 
@@ -69,12 +71,69 @@ Currently options for serving CKEditor via the asset pipeline are limited. For
69
71
 
70
72
  == Scaffolding
71
73
 
72
- TODO: Write this section
74
+ Tenon comes with a powerful scaffold generator that makes it easy for you to prototype new resources. The scaffold generator sits on top of Rails' built in resource generators and creates everything you need for a working CRUD interface in Tenon.
75
+
76
+ Let's create an imaginary +Post+ resource as an example:
77
+
78
+ $ rails generate tenon:scaffold Post title:string excerpt:text
79
+
80
+ This task will create the following files:
81
+
82
+ app/controllers/tenon/posts_controller.rb
83
+ app/decorators/post_decorator.rb
84
+ app/policies/post_policy.rb
85
+ app/serializers/post_serializer.rb
86
+ app/models/post.rb
87
+ app/views/tenon/posts/index.html.haml
88
+ app/views/tenon/posts/new.html.haml
89
+ app/views/tenon/posts/edit.html.haml
90
+ app/views/tenon/posts/_form.html.haml
91
+ db/migrate/<timestamp>_create_posts.rb
92
+
93
+ It will also add the necessary routes to <tt>config/routes.rb</tt>. If you navigate to <tt>/tenon/posts</tt> within your app you will see that you have a fully functioning section with the ability to index, search, add, edit, and delete posts.
94
+
95
+ On top of the typical Rails generator column types like +string+, +text+, or +integer+, Tenon adds a few new options.
96
+
97
+ +asset+:: Use this to create an asset field that integrates with Tenon's asset library.
98
+ +content+:: Use this to create a TenonContent field.
99
+ +date+:: Automatically links up a date picker widget in your form.
100
+ +datetime+:: Automatically links up a date and time picker widget in your form.
101
+
102
+ There are also a handful of column names that you can define to trigger special behaviour in the generator:
103
+
104
+ +title+:: This field is required for your scaffolded resource to work out of the box. You'll need to override part of the +ResourceIndex+ React component tree (more on this below) if you want to use a different field name.
105
+ +publish_at+:: Adds special publishing fields to your form and adds a scope called +published+ to your model.
106
+ +list_order+:: Includes <tt>Tenon::Reorderable</tt> in your model, adds a +reorder+ action to your controller, adds a default scope to your model to sort by +list_order+, and makes the items in your index view drag and drop sortable.
107
+ +seo_title+, +seo_keywords+, +seo_description+:: Adds a special SEO fields panel to your form with explanatory text. Useful for public-facing websites.
108
+
109
+ With these features in mind, let's regenerate our +Post+ resource with all of our special features.
110
+
111
+ $ rails generate tenon:scaffold Post title:string excerpt:text content:content banner_photo:asset written_on:date publish_at:datetime list_order:integer seo_title:string seo_keywords:string seo_description:text
112
+
113
+ Out of the box this will give us a working +Post+ model with functional views. Typically you'll want to reorganize the fields found in <tt>app/views/tenon/posts/_form.html.haml</tt>, but otherwise your work is done!
73
114
 
74
115
  == Item Revisions/History
75
116
 
76
117
  TODO: Write this section
77
118
 
119
+ == Access Control / ACL
120
+
121
+ Tenon uses Pundit for ACL, see https://github.com/elabs/pundit for documentation.
122
+
123
+ Have your policies inherit from <tt>Tenon::ApplicationPolicy</tt> to get default authorization. Override <tt>app/policies/tenon/application_policy.rb</tt> if you want to override the default authorization scheme. Write custom policies as per Pundit standards.
124
+
125
+ Any controller that inherits from <tt>Tenon::ResourcesController</tt> will have ACL applied on all CRUD methods. ACL is enforced on all actions so ensure that you +authorize+ any time you add a new action or override an existing one. Policy scoping is enforced on the index action, so ensure that if you override the +filterer+ method that you are using Pundit's +policy_scope+ method. For example:
126
+
127
+ module Tenon
128
+ class PostsController < ResourcesController
129
+ private
130
+
131
+ def filterer
132
+ PostFilterer.new(policy_scope(Post), params)
133
+ end
134
+ end
135
+ end
136
+
78
137
  == Internationalization
79
138
 
80
139
  Although Tenon is currently anglocentric it supports the inclusion of additional
@@ -84,76 +143,716 @@ To add internationalized fields, follow these steps:
84
143
 
85
144
  1. Add our 'translates' gem to your Gemfile and then bundle install
86
145
 
87
- gem 'translates', git: 'https://github.com/factore/translates.git'
146
+ gem 'translates', git: 'https://github.com/factore/translates.git'
88
147
 
89
148
  2. Tell Tenon which languages you want to support in config/initializers/tenon.rb (You don't need to add English, Tenon always assumes its in use.)
90
149
 
91
- config.languages = {
92
- "French" => :fr,
93
- "German" => :de
94
- # etc.
95
- }
150
+ config.languages = {
151
+ "French" => :fr,
152
+ "German" => :de
153
+ # etc.
154
+ }
96
155
 
97
156
  3. Add a language yml file in config/locales/ for each language defined above, or rails will have a fit, eg 'config/locales/fr.yml'
98
157
 
99
158
  4. Create or update config/i18n_fields.yml to tell Tenon which fields you would like to have internationalized.
100
159
 
101
- tables:
102
- cars:
103
- - title
104
- - description
105
-
106
- events:
107
- - title
108
- - location
109
- - description
110
-
111
- If you want to add internationalization to the default Tenon models you should make your i18n_fields.yml look like this:
112
-
113
- tables:
114
- tenon/events:
115
- - title
116
- - location
117
-
118
- tenon/pages:
119
- - title
120
- - seo_title
121
- - seo_keywords
122
- - seo_description
123
-
124
- tenon/posts:
125
- - title
126
- - excerpt
127
- - seo_title
128
- - seo_keywords
129
- - seo_description
130
-
160
+ tables:
161
+ cars:
162
+ - title
163
+ - description
164
+
165
+ events:
166
+ - title
167
+ - location
168
+ - description
169
+
170
+ If you want to add internationalization to the default Tenon models you should make your i18n_fields.yml look like this:
171
+
172
+ tables:
173
+ tenon/events:
174
+ - title
175
+ - location
176
+
177
+ tenon/pages:
178
+ - title
179
+ - seo_title
180
+ - seo_keywords
181
+ - seo_description
182
+
183
+ tenon/posts:
184
+ - title
185
+ - excerpt
186
+ - seo_title
187
+ - seo_keywords
188
+ - seo_description
189
+
131
190
  5. Generate and run the internationalization migration. The generator will only try to create columns that don't already exist, so you can use this generator multiple times throughout the development of your application.
132
191
 
133
- rails generate tenon:i18n_migrations
134
- rake db:migrate
192
+ rails generate tenon:i18n_migrations
193
+ rake db:migrate
135
194
 
136
195
  6. Update your models to make sure your attributes are translated
137
196
 
138
- class MyModel < ActiveRecord::Base
139
- include Translates
140
- # plain old rails attributes
141
- translates :title
142
- # tenon_content
143
- tenon_content :description, i18n: true
144
- end
145
-
197
+ class MyModel < ApplicationRecord
198
+ include Translates
199
+ # plain old rails attributes
200
+ translates :title
201
+ # tenon_content
202
+ tenon_content :description, i18n: true
203
+ end
204
+
146
205
  7. Update your tenon views to add the language navigation helper, where needed:
147
- # app/views/tenon/cars/_form.html.haml
148
- - content_for :sidebar do
149
- .sidebar
150
- .content
151
- ...
152
- = i18n_language_nav(:cars)
153
- ...
154
-
206
+ # app/views/tenon/cars/_form.html.haml
207
+ - content_for :sidebar do
208
+ .sidebar
209
+ .content
210
+ ...
211
+ = i18n_language_nav(:cars)
212
+ ...
213
+
155
214
  8. While there, make sure you are using 'autosaving_form_for' instead of 'form_for' to create your forms. By doing this, Tenon will automatically update the labels when the different languages are selected.
156
215
 
157
216
  9. Make sure your routes are configured according to your needs and the I18n.locale is being set somehow (see Rails documentation for more info: http://guides.rubyonrails.org/i18n.html)
158
217
 
159
218
  Once you've done this and restarted your app you will see a language selection nav in the sidebar of each Tenon form that has internationalized fields. On the front end, attributes on your Tenon models will be translated correctly, based on I18n.locale.
219
+
220
+ == Using and Customizing the ResourceIndex React App
221
+
222
+ One of the biggest changes in Tenon 2.0 is the replacement of the index view for each resource with a common ResourceIndex ReactJS/Redux app. Listing, paginating, filtering, sorting, deleting, editing, and all other tasks typically done on the index route of a resource are managed through this mounted React app.
223
+
224
+ Rather than scaffolding new code for every resource, code is shared for all resources. If you find yourself needing to customize the index view, individual components of the React app can easily be replaced with custom components. This allows for a high level of customization without creating a lot of repetitive code.
225
+
226
+ === The simplest thing that could possibly work
227
+
228
+ The bare minimum code to get a fully functioning resource index view is as follows. (For an imagined +Post+ resource this code would be the entirety of <tt>app/views/tenon/posts/index.html.haml</tt>)
229
+
230
+ = react_component 'ResourceIndexRoot',
231
+ title: 'Posts',
232
+ breadcrumbs: breadcrumb_links,
233
+ recordsPath: posts_path(format: 'json'),
234
+ newPath: new_post_path
235
+
236
+ This code will instantiate the +ResourceIndexRoot+ React component, and pass it the following required props:
237
+
238
+ +title+:: The pluralized title of the resource.
239
+ +breadcrumb_links+:: An array of Ruby hashes in the format of <tt>[{ title: 'A Title', path: '/path/to/somewhere' }]</tt>. Use the built-in +breadcrumb_links+ Rails helper to automatically generate this for your current resource, or supply your own.
240
+ +recordsPath+:: The path where the JSON dump of your resource can be found. Typically just the +index+ path with the +format+ of <tt>'json'</tt> specified.
241
+ +newPath+:: The path to the +new+ action for your resource.
242
+
243
+ === Customizing the app
244
+
245
+ At some point you will need to make changes to how the ResourceIndex app looks and behaves for a specific resource. Rather than copying the entire app and changing the relevant portions, the app can be instantiated with specific child-components swapped out for your own custom components.
246
+
247
+ In order to do this first we need to understand the composition of the app. The app is broken up into several smaller child-components, each of which can be swapped out when the app is instantiated in your +index+ view. The component tree is as follows:
248
+
249
+ - ResourceIndexRoot
250
+ - App
251
+ - QuickSearchToolbar
252
+ - QuickSearchInput
253
+ - ActionButtons
254
+ - FilterToggle
255
+ - SortOrder
256
+ - SortOrderItem
257
+ - QuickSearchOverlay
258
+ - (Same Children as QuickSearchToolbar)
259
+ - Filtering
260
+ - FilterDrawer
261
+ - FilterOverlay
262
+ - List
263
+ - Record
264
+ - RecordTitle
265
+ - RecordActions
266
+ - RecordExpandedContent
267
+ - LoadMoreButton
268
+
269
+
270
+ A common task when creating index views is changing the way the title of each individual record is displayed. Let's change our imagined +Post+ resource to display not only the post's title, but also its publish date.
271
+
272
+ The first step to replacing a child-component is changing the instantiation call to the +ResourceIndex+ component and passing in the name of our new component. In this case, we want to replace +RecordTitle+ with a custom component, which we'll call +PostsRecordTitle+. Pass it in as a prop like so:
273
+
274
+ = react_component 'ResourceIndexRoot',
275
+ title: 'Posts',
276
+ breadcrumbs: breadcrumb_links,
277
+ recordsPath: posts_path(format: 'json'),
278
+ newPath: new_post_path,
279
+ childComponentNames: { RecordTitle: 'PostsRecordTitle' }
280
+
281
+ This prop tells the top-level component to render +PostsRecordTitle+ instead of +DefaultRecordTitle+ in the component tree.
282
+
283
+ The next step is to create our +PostsRecordTitle+ component. Start by copying the code of the +DefaultRecordTitle+ component, found at <tt>app/assets/javascripts/tenon/components/resource-index/components/default/record-title.es6</tt>. The code will look something like this.
284
+
285
+ Tenon.RI.DefaultRecordTitle = ({ record }) => {
286
+ return (
287
+ <p className="record__title">{record.title}</p>
288
+ );
289
+ };
290
+
291
+ Copy this code and create the new component at <tt>app/assets/javascripts/tenon/components/posts-record-title.es6</tt>. In our case we simply want to change the name of the component, and add a second line with the record's +publish_at+ method. Our finished component looks like this:
292
+
293
+ Tenon.RI.PostsRecordTitle = ({ record }) => {
294
+ return (
295
+ <div>
296
+ <p className="record__title">
297
+ {record.title}
298
+ </p>
299
+
300
+ <p className="record__title--smallest">
301
+ Published on {record.publish_at}
302
+ </p>
303
+ </div>
304
+ );
305
+ };
306
+
307
+ Upon saving this component our imagined +Post+ resource's index page will now display a customized title including the publish date/time of the post.
308
+
309
+ === Triggering actions and making changes
310
+
311
+ It's not enough to just display custom information in the index view, often we need to give users the ability to make changes or interact with data as well. You can trigger actions in your custom components that allow you to change the state of the app and update the database. The two most common actions you will want to take are making updates to an individual record, and changing how your records are filtered and sorted. These two actions are known as +updateRecord+ and +updateQuery+ and are passed down as methods on the +handlers+ prop available in any custom component.
312
+
313
+ <tt>updateRecord(event, record, changeObject)</tt>:: Updates the record in question and sends the changes to the server.
314
+
315
+ * +record+ - The record object. At minimum it must have +id+, +update_path+, and +resource_type+ methods. (Any resource generated with a Tenon scaffold will have these.)
316
+
317
+ * +changeObject+ - An object describing the changes to the object, eg. <tt>{ title: 'My New Title', featured: true }</tt>
318
+
319
+ <tt>updateQuery(event, changeObject [, appendRecords])</tt>:: Changes the query sent to the server when fetching records, re-fetches records with new query, and updates query string in address bar.
320
+
321
+ * +changeObject+ - An object describing the changes to the query, eg. <tt>{ q: 'my search', page: 1 }</tt>. (You should always include <tt>page: 1</tt> in your query unless you are appending records.)
322
+
323
+ * +appendRecords+ - Boolean. True: Append new records to the bottom of the list. False: Clear record list before getting new records. Default: false.
324
+
325
+ Let's create a simple button on our imagined posts index that allows us to toggle whether a given post is featured or not.
326
+
327
+ The first thing we need to do is update our call to the +ResourceIndex+ component in <tt>index.html.haml</tt> to tell it that we're going to be passing in our custom set of +RecordActions+.
328
+
329
+ = react_component 'ResourceIndexRoot',
330
+ title: 'Posts',
331
+ breadcrumbs: breadcrumb_links,
332
+ recordsPath: posts_path(format: 'json'),
333
+ newPath: new_post_path,
334
+ childComponentNames: { RecordTitle: 'PostsRecordTitle',
335
+ RecordActions: 'PostsRecordActions' }
336
+
337
+ Next, we'll want to make a copy of the +DefaultRecordActions+ component found at <tt>app/assets/javascripts/components/resource-index/components/default/record-actions.es6</tt>. The default component looks like this:
338
+
339
+ Tenon.RI.DefaultRecordActions = (props) => {
340
+ const editPath = props.record.edit_path;
341
+ const onDelete = props.onDelete;
342
+
343
+ return (
344
+ <div className="record__actions">
345
+ <a
346
+ className="record__action-icon"
347
+ href={editPath}
348
+ title="Edit">
349
+ <i className="material-icon">edit</i>
350
+ </a>
351
+
352
+ <a
353
+ className="record__action-icon"
354
+ href="#!"
355
+ onClick={onDelete}
356
+ title="Delete">
357
+ <i className="material-icon">delete</i>
358
+ </a>
359
+ </div>
360
+ );
361
+ };
362
+
363
+ We'll make our new component at <tt>app/assets/javascripts/tenon/components/posts-record-actions.es6</tt> and add a new icon.
364
+
365
+ Tenon.RI.PostsRecordActions = (props) => {
366
+ const editPath = props.record.edit_path;
367
+ const onDelete = props.onDelete;
368
+
369
+ return (
370
+ <div className="record__actions">
371
+ <!-- copied edit and delete buttons -->
372
+
373
+ <a
374
+ className="record__action-icon"
375
+ href="#!"
376
+ onClick={<we need something here>}
377
+ title="Toggle Featured">
378
+ <i className="material-icon">star_border</i>
379
+ </a>
380
+ </div>
381
+ );
382
+ };
383
+
384
+ Next we need to tap into the +onClick+ action of the link to toggle the featured state of the record.
385
+
386
+ <a
387
+ className="record__action-icon"
388
+ href="#!"
389
+ onClick={(e) => {
390
+ props.handlers.updateRecord(e, props.record, !props.record.featured)
391
+ }}
392
+ title="Toggle Featured">
393
+ <i className="material-icon">star_border</i>
394
+ </a>
395
+
396
+ This is a little bit lengthy, so let's extract some constants up above.
397
+
398
+ Tenon.RI.PostsRecordActions = (props) => {
399
+ const editPath = props.record.edit_path;
400
+ const { onDelete, record } = props;
401
+ const { updateRecord } = props.handlers;
402
+
403
+ return (
404
+ <div className="record__actions">
405
+ <!-- copied edit and delete buttons -->
406
+
407
+ <a
408
+ className="record__action-icon"
409
+ href="#!"
410
+ onClick={(e) => {
411
+ updateRecord(e, record, { featured: !record.featured });
412
+ }}
413
+ title="Toggle Featured">
414
+ <i className="material-icon">star_border</i>
415
+ </a>
416
+ </div>
417
+ );
418
+ };
419
+
420
+ Finally, let's add some feedback to show the user that something happened. We'll have the component display an empty star for regular posts, and a full star for featured ones.
421
+
422
+ <a
423
+ className="record__action-icon"
424
+ href="#!"
425
+ onClick={(e) => {
426
+ updateRecord(e, record, { featured: !record.featured });
427
+ }}
428
+ title="Toggle Featured">
429
+ <i className="material-icon">
430
+ {record.featured ? 'star' : 'star_border'}
431
+ </i>
432
+ </a>
433
+
434
+ Your users can now click on the star to toggle the post's featured state.
435
+
436
+ Read on through the next section to understand how <tt>updateQuery()</tt> and the query object interacts with the server to filter and return records.
437
+
438
+ === Adding and editing using a modal window
439
+
440
+ Very basic resources, such as lists of categories, may be easier to manage if their add and edit actions are presented in a modal window rather than on a new page. This can be easily accomplished with the addition of two options and one custom component.
441
+
442
+ First, change the call to +ResourceIndexRoot+ to include the modal options, as well as the name of the custom form component you'll be providing. In this case we'll use an imagined +PostCategory+ list as our example:
443
+
444
+ = react_component 'ResourceIndexRoot',
445
+ title: 'Categories',
446
+ breadcrumbs: breadcrumb_links,
447
+ recordsPath: post_categories_path(format: 'json'),
448
+ newPath: new_post_category_path,
449
+ addWithModal: true,
450
+ editWithModal: true,
451
+ childComponentNames: { ModalFields: 'PostCategoryFields' }
452
+
453
+ Note the addition of +addWithModal+, +editWithModal+, and the name of the +ModalFields+ child component.
454
+
455
+ Next we need to create the +PostCategoryFields+ child component. This file can be created at <tt>app/assets/javascripts/tenon/components/post-category-fields.es6</tt> and should look something like this:
456
+
457
+ Tenon.RI.PostCategoryFields = (props) => {
458
+ const { currentRecord, currentRecordErrors } = props.data;
459
+ const { onChange } = props;
460
+
461
+ return (
462
+ <div>
463
+ <TextField
464
+ name="title"
465
+ value={currentRecord.title}
466
+ onChange={onChange}
467
+ errors={currentRecordErrors.title}
468
+ label="Title" />
469
+ <button type="submit" className="btn">Save</button>
470
+ </div>
471
+ );
472
+ };
473
+
474
+ The important things to note about this are as follows:
475
+
476
+ * It uses the handy +TextField+ component to generate standard Tenon <tt>input-block</tt> HTML. Other available components include +SelectField+, +CheckBoxField+, and +DatepickerField+. You can use standard HTML and supply the <tt>input-block</tt> tags yourself if you need something custom.
477
+ * It pulls +currentRecord+, +currentRecordErrors+, and +onChange+ out of the supplied props. These will always be available.
478
+ * It passes +name+, +value+, +onChange+, +errors+, and +label+ along to the +TextField+ component.
479
+ * +name+, +value+, and +errors+ are consistent with the field that's being presented (in this case they all reference +title+.)
480
+ * The save button is added, but other modal markup is handled automatically further up the chain.
481
+
482
+ By supplying these options and this custom component, our +PostCategory+ resource can now be managed completely from the index page without having to visit a secondary form page.
483
+
484
+ == Using the StandaloneList component
485
+
486
+ Occasionally you may want to render a list of records inside an existing view, for example if you wanted to embed a list of records inside/alongside a form. You can accomplish this by rendering the +StandaloneList+ component. It functions identically to the +ResourceIndexRoot+ component.
487
+
488
+ This is especially useful for lists that have in-place editing (eg. the feature toggle we just added to posts). You can replace any of the child components in the chain, just as with the +ResourceIndexRoot+ component.
489
+
490
+ = react_component 'StandaloneList',
491
+ recordsPath: posts_path(format: 'json')
492
+ childComponentNames: { RecordTitle: 'PostsRecordTitle',
493
+ RecordActions: 'PostsRecordActions' }
494
+
495
+ == Searching and Filtering Records
496
+
497
+ === Setting up your Rails Controllers and Filterers
498
+
499
+ Often you will need to provide various different ways to filter records that are returned
500
+ in your controllers' +index+ action. The standard <tt>Tenon::ResourcesController#index</tt>
501
+ action provides a hook to allow the returned records to pass through a Filterer.
502
+ Filterers receive, at minimum, a scope (eg. an <tt>ActiveRecord::Relation</tt>) and a set of params.
503
+ They can then apply their own internal logic to filter the passed scope. For example, consider the
504
+ following call to an imagined <tt>PostFilterer</tt>:
505
+
506
+ filterer = PostFilterer.new(Post.all, { q: 'Tenon' })
507
+ @posts = filterer.filter
508
+
509
+ The <tt>PostFilterer</tt> could use its internal logic to, for
510
+ example, return only posts that are called "Tenon":
511
+
512
+ class PostFilterer < Tenon::BaseFilterer
513
+ def filter
514
+ if params[:q].present?
515
+ @scope = scope.where(title: params[:q])
516
+ end
517
+ super # Returns the scope
518
+ end
519
+ end
520
+
521
+ or it could use its internal logic to return only posts that are
522
+ in a +Category+ called "Tenon":
523
+
524
+ class PostFilterer < Tenon::BaseFilterer
525
+ def filter
526
+ if params[:q].present?
527
+ @scope = scope.includes(:category)
528
+ @scope = scope.where(category: { title: params[:q] })
529
+ end
530
+ super # Returns scope
531
+ end
532
+ end
533
+
534
+ By default, records in the +index+ action of any controller that inherits from
535
+ <tt>Tenon::ResourcesController</tt> will be filtered by
536
+ <tt>Tenon::GenericFilterer</tt>. While <tt>Tenon::BaseFilterer</tt> takes
537
+ a scope and a params object as its initialization arguments,
538
+ <tt>Tenon::GenericFilterer</tt> also takes as a third argument a list of
539
+ fields to run a basic text search on. The <tt>#quick_search_fields</tt> method
540
+ on any controller is used to set these fields, like in the following example
541
+ of a basic controller for posts:
542
+
543
+ class PostsController < Tenon::ResourcesController
544
+ private
545
+
546
+ def quick_search_fields
547
+ ['posts.title', 'posts.excerpt', 'posts.content']
548
+ end
549
+ end
550
+
551
+ As it's a convention for all resources in Tenon to respond to a <tt>#title</tt>
552
+ method the default behaviour is to filter on this field.
553
+
554
+ In order to provide searching and filtering capabilities beyond what the
555
+ +GenericFilterer+ provides, simply create a a new filterer in the
556
+ <tt>app/filterers</tt> directory. It is usually best to have this custom
557
+ filterer inherit from <tt>Tenon::GenericFilterer</tt> in order to keep the quick search
558
+ functionality, but a filterer can also inherit from <tt>Tenon::BaseFilterer</tt>.
559
+
560
+ After creating the new filterer, it can be inserted into the controller
561
+ by defining the <tt>#filterer</tt> method.
562
+
563
+ class PostsController < Tenon::ResourcesController
564
+ private
565
+
566
+ def quick_search_fields
567
+ ['posts.title', 'posts.excerpt', 'posts.content']
568
+ end
569
+
570
+ def filterer
571
+ PostFilterer.new(Post.all, params, quick_search_fields)
572
+ end
573
+ end
574
+
575
+ (Note that in reality you would want to perform an ACL check on the scope you pass into the filterer, replacing <tt>Post.all</tt> with <tt>policy_scope(Post)</tt>.)
576
+
577
+ Here is an example of what an imagined +PostFilterer+ that inherits from
578
+ <tt>Tenon::GenericFilterer</tt> with some date-filtering logic might look like:
579
+
580
+ class PostFilterer < Tenon::GenericFilterer
581
+ def filter
582
+ @scope = filter_start_date
583
+ @scope = filter_end_date
584
+ super
585
+ end
586
+
587
+ private
588
+
589
+ def filter_start_date
590
+ return scope unless params[:start_date].present?
591
+ scope.where('publish_at >= ?', params[:start_date])
592
+ end
593
+
594
+ def filter_end_date
595
+ return scope unless params[:end_date].present?
596
+ scope.where('publish_at <= ?', params[:end_date])
597
+ end
598
+ end
599
+
600
+ The +filter_start_date+ and +filter_end_date+ methods allow custom filtering
601
+ of the collection that's passed in, while the call to +super+ on the
602
+ <tt>#filter</tt> method also allows for the +quick_search_fields+ to be
603
+ searched.
604
+
605
+ Because many filtering tasks are similar, filterers that inherit from <tt>Tenon::BaseFilterer</tt> (and thus <tt>Tenon::GenericFilterer</tt>) have access to a few convenience methods for easier filtering. These methods are:
606
+
607
+ <tt>eq(field, value)</tt>:: Used to check if +field+ is equal to +value+
608
+ <tt>ilike(field, value)</tt>:: Used to check if +field+ <em>ILIKE matches</em> +value+
609
+ <tt>gt(field, value)</tt>:: Used to check if +field+ is greater than +value+
610
+ <tt>lt(field, value)</tt>:: Used to check if +field+ is less than +value+
611
+ <tt>gte(field, value)</tt>:: Used to check if +field+ is greater than or equal to +value+
612
+ <tt>lte(field, value)</tt>:: Used to check if +field+ is less than or equal to +value+
613
+ <tt>order(field, direction)</tt>:: Used to order your scope by +field+ in +direction+, eg. <tt>order('books.title', 'asc')</tt>. Define a method called +allowed_order_fields+ on your Filterer and return an array of allowed fields, eg. <tt>['books.title', 'created_at', 'authors.title']</tt>. Direction must be <tt>'asc'</tt> or <tt>'desc'</tt>.
614
+ <tt>reorder(field, direction)</tt>:: Same as +order+ but uses <tt>#reorder</tt> instead of <tt>#order</tt> on the scope.
615
+
616
+ These methods will always simply return the current scope if +value+ is not +.present?+, so there's no need to check for the presence of a param.
617
+
618
+ Here is an example of the imagined +PostFilterer+ rewritten using these convenience methods:
619
+
620
+ class PostFilterer < Tenon::GenericFilterer #:nodoc:
621
+ def filter
622
+ @scope = gte('posts.publish_at', params[:start_date])
623
+ @scope = lte('posts.publish_at', params[:end_date])
624
+ super
625
+ end
626
+ end
627
+
628
+ A custom filterer is just a plain old Ruby object and can use any kind of
629
+ internal logic to filter a collection. The only requirement is that the
630
+ <tt>#filter</tt> method returns a chainable <tt>ActiveRecord::Relation</tt>.
631
+
632
+ === Creating the Filtering UI for your Resource
633
+
634
+ The ResourceIndex component's toolbar contains a search input that automatically sends its value as <tt>params[:q]</tt> when a user types in it. This hook into <tt>Tenon::GenericFilterer</tt> on the Rails end and provide basic filtering of a resource. For many resources this all the filtering that's required, and no customization is necessary.
635
+
636
+ However, it's often necessary to build more advanced filtering features, as demonstrated in the above example using the PostFilterer to filter posts on params like <tt>:start_date</tt> and <tt>:end_date</tt>. In order to expose these options to the end user, we need to create a React component and inject it into our ResourceIndex component. These custom components are called <b>Filter Drawers</b>.
637
+
638
+ Here is an example of what an imagined +PostsFilterDrawer+ component, living at <tt>/app/assets/javascripts/tenon/components/posts-filter-drawer.es6</tt>, might look like.
639
+
640
+ Tenon.RI.PostsFilterDrawer = (props) => {
641
+ const query = props.data.query;
642
+ const onChange = props.onChange;
643
+
644
+ return (
645
+ <div className="panel--block">
646
+ <TextField
647
+ label="Keywords"
648
+ name="q"
649
+ value={query.q}
650
+ onChange={onChange} />
651
+
652
+ <DatepickerField
653
+ label="Start Date"
654
+ name="start_date"
655
+ value={query.start_date}
656
+ onChange={onChange} />
657
+
658
+ <DatepickerField
659
+ label="End"
660
+ name="end_date"
661
+ value={query.end_date}
662
+ onChange={onChange} />
663
+ </div>
664
+ );
665
+ };
666
+
667
+ This stateless React component (https://toddmotto.com/stateless-react-components/) is passed the entire state tree from the top-level +ResourceIndex+ component, but only uses the <tt>data.query</tt> object (responsible for which params are passed to the server when fetching records) and an +onChange+ function passed down from the parent component. Also, notice that the component is set within the <tt>Tenon.RI</tt> object. All custom components intended to be passed into the ResourceIndex component tree must be set this way.
668
+
669
+ The component uses JSX needed to build three simple form controls: a text field for a general query, a datepicker for the start date, and a datepicker for the end date. Each input is passed four props:
670
+
671
+ label:: The visible label for the field
672
+ name:: The name of the +param+ being changed (eg. <tt>name="start_date"</tt> -> <tt>params[:start_date]</tt>)
673
+ value:: The initial value of input, almost always <tt>query.<param_name></tt>
674
+ onChange:: The +onChange+ prop passed in from the parent component.
675
+
676
+ As long as +name+, +value+, and +onChange+ are present you can use any HTML elements and form inputs you like to build your Filter Drawer. There are a handful of simple pre-built components available as conveniences for building form elements, including:
677
+
678
+ * +TextField+
679
+ * +DatepickerField+
680
+ * +SelectField+
681
+ * +CheckBoxField+
682
+
683
+ To inject this component into the top-level +ResourceIndex+ component for your particular resource its name needs to be passed in as part of the +childComponentNames+ prop in your index view. Here is an example of what it might look like in an imagined posts index, located at <tt>app/views/tenon/posts/index.html.haml</tt>:
684
+
685
+ = react_component 'ResourceIndexRoot',
686
+ title: 'Posts',
687
+ breadcrumbs: breadcrumb_links,
688
+ recordsPath: posts_path(format: 'json'),
689
+ newPath: new_post_path,
690
+ childComponentNames: { FilterDrawer: 'PostsFilterDrawer' }
691
+
692
+ When the top-level +ResourceIndex+ is rendered with a +FilterDrawer+, the Filter Drawer will be available to your users, and you can provide as much or as little advanced filtering as you like.
693
+
694
+ === Creating the Ordering UI for your Resource
695
+
696
+ (This section glosses over the process of adding and editing components to the ResourceIndex app. Make sure you read <b>Using and Customizing the ResourceIndex React App</b> before reading this.)
697
+
698
+ While it's certainly possible to add the UI for ordering the returned records into the +FilterDrawer+ component, the recommend approach is to separate it from filtering and instead use a button with a dropdown menu in the toolbar. To add this button, we're going to supply our own custom +ActionButtons+ child-component to the +ResourceIndex+ app.
699
+
700
+ The first step is initializing the +ResourceIndex+ app with the name of our custom +ActionButtons+ component. We'll continue working on our imagined +Post+ resource, and so this component will be called +PostsActionButtons+.
701
+
702
+ = react_component 'ResourceIndexRoot',
703
+ title: 'Posts',
704
+ breadcrumbs: breadcrumb_links,
705
+ recordsPath: posts_path(format: 'json'),
706
+ newPath: new_post_path,
707
+ childComponentNames: { FilterDrawer: 'PostsFilterDrawer',
708
+ ActionButtons: 'PostsActionButtons' }
709
+
710
+
711
+ Next, copy the +DefaultActionButtons+ component, found at <tt>app/assets/javascripts/tenon/components/resource-index/components/default/action-buttons.es6</tt>. The default component looks like this:
712
+
713
+ Tenon.RI.DefaultActionButtons = (props) => {
714
+ const { FilterDrawerToggle } = props.childComponents;
715
+
716
+ return (
717
+ <div className="toolbar__actions toolbar__actions--right">
718
+ <FilterDrawerToggle {...props} />
719
+ </div>
720
+ );
721
+ };
722
+
723
+ We'll create our new component at <tt>app/assets/javascripts/tenon/components/posts-action-buttons.es6</tt>. We want to be able to order our posts from oldest to newest, and from newest to oldest, so let's paste the code from the +DefaultActionButtons+ component and add some markup that creates a dropdown menu with these options.
724
+
725
+ Tenon.RI.PostsActionButtons = (props) => {
726
+ const { FilterDrawerToggle } = props.childComponents;
727
+
728
+ return (
729
+ <div className="toolbar__actions toolbar__actions--right">
730
+ <FilterDrawerToggle {...props} />
731
+
732
+ <div className="toolbar__action">
733
+ <a
734
+ className="action-icon dropdown-button"
735
+ href="#!"
736
+ title="Sort Order">
737
+ <i className="material-icons">tune</i>
738
+ </a>
739
+
740
+ <ul className="dropdown">
741
+ <li className="dropdown__item dropdown__item--label">Order By</li>
742
+ <li className="dropdown__item">
743
+ <a
744
+ href="#!"
745
+ className="dropdown__action action-icon">
746
+ <span>Oldest to Newest</span>
747
+ </a>
748
+ </li>
749
+ <li className="dropdown__item">
750
+ <a href="#!" className="dropdown__action action-icon">
751
+ <span>Newest to Oldest</span>
752
+ </a>
753
+ </li>
754
+ </ul>
755
+ </div>
756
+
757
+
758
+ </div>
759
+ );
760
+ };
761
+
762
+ Next we need to set up the links to update the query. There is an +orderBy+ handler that takes +field+ and +direction+ as arguments available in our props. This handler is just a convenient wrapper around the +updateQuery+ wrapper. We'll extract it and then call it in the +onClick+ prop of our links.
763
+
764
+ Tenon.RI.PostsActionButtons = (props) => {
765
+ const { FilterDrawerToggle } = props.childComponents;
766
+ const { orderBy } = props.handlers;
767
+
768
+ return (
769
+ <div className="toolbar__actions toolbar__actions--right">
770
+ <FilterDrawerToggle {...props} />
771
+
772
+ <div className="toolbar__action">
773
+ <a
774
+ className="action-icon dropdown-button"
775
+ href="#!"
776
+ title="Sort Order">
777
+ <i className="material-icons">sort</i>
778
+ </a>
779
+
780
+ <ul className="dropdown">
781
+ <li className="dropdown__item dropdown__item--label">Order By</li>
782
+ <li className="dropdown__item">
783
+ <a
784
+ href="#!"
785
+ className="dropdown__action"
786
+ onClick={(e) => orderBy(e, 'publish_at', 'asc')}>
787
+ <span>Oldest to Newest</span>
788
+ </a>
789
+ </li>
790
+ <li className="dropdown__item">
791
+ <a
792
+ href="#!"
793
+ className="dropdown__action"
794
+ onClick={(e) => orderBy(e, 'publish_at', 'desc')}>
795
+ <span>Newest to Oldest</span>
796
+ </a>
797
+ </li>
798
+ </ul>
799
+ </div>
800
+ </div>
801
+ );
802
+ };
803
+
804
+ Finally, let's give some feedback to the user so they can see which item is currently selected. We'll pull the +order_direction+ out from the +query+ object and then use it to set the +active+ class on the correct <tt><li></tt>.
805
+
806
+ Tenon.RI.PostsActionButtons = (props) => {
807
+ const { FilterDrawerToggle } = props.childComponents;
808
+ const { orderBy } = props.handlers;
809
+ const { order_direction } = props.data.query;
810
+
811
+ return (
812
+ <div className="toolbar__actions toolbar__actions--right">
813
+ <FilterDrawerToggle {...props} />
814
+
815
+ <div className="toolbar__action">
816
+ <a
817
+ className="action-icon dropdown-button"
818
+ href="#!"
819
+ title="Sort Order">
820
+ <i className="material-icons">sort</i>
821
+ </a>
822
+
823
+ <ul className="dropdown">
824
+ <li className="dropdown__item dropdown__item--label">Order By:</li>
825
+ <li
826
+ className={order_direction === 'asc' ? 'dropdown__item active' : 'dropdown__item'}>
827
+ <a
828
+ href="#!"
829
+ className="dropdown__action action-icon"
830
+ onClick={(e) => orderBy(e, 'publish_at', 'asc')}>
831
+ <span>Oldest to Newest</span>
832
+ </a>
833
+ </li>
834
+ <li
835
+ className={order_direction === 'desc' ? 'active dropdown__item' : 'dropdown__item'}>
836
+ <a
837
+ href="#!"
838
+ className="dropdown__action action-icon"
839
+ onClick={(e) => orderBy(e, 'publish_at', 'desc')}>
840
+ <span>Newest to Oldest</span>
841
+ </a>
842
+ </li>
843
+ </ul>
844
+ </div>
845
+ </div>
846
+ );
847
+ };
848
+
849
+ If you think all this seems a bit effortful for a very common requirement, you're not wrong. This same behaviour can be replicated by going back to the +DefaultActionButtons+ component and passing an +orderOptions+ array as a prop to the +ResourceIndex+, like this:
850
+
851
+ = react_component 'ResourceIndexRoot',
852
+ title: 'Posts',
853
+ breadcrumbs: breadcrumb_links,
854
+ recordsPath: posts_path(format: 'json'),
855
+ newPath: new_post_path,
856
+ childComponentNames: { FilterDrawer: 'PostsFilterDrawer' }
857
+ orderOptions: [ { title: 'Oldest to Newest', order: 'publish_at:asc' },
858
+ { title: 'Newest to Oldest', order: 'publish_at:desc'} ]