tenon 1.1.8 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.rdoc +768 -69
- data/Rakefile +7 -13
- data/app/assets/javascripts/tenon/application.js +4 -0
- data/app/controllers/tenon/assets_controller.rb +22 -23
- data/app/controllers/tenon/base_controller.rb +22 -13
- data/app/controllers/tenon/index_controller.rb +0 -4
- data/app/controllers/tenon/item_versions_controller.rb +20 -8
- data/app/controllers/tenon/resources_controller.rb +100 -30
- data/app/controllers/tenon/simple_resources_controller.rb +7 -7
- data/app/controllers/tenon/styleguides_controller.rb +10 -0
- data/app/decorators/tenon/application_decorator.rb +4 -8
- data/app/decorators/tenon/asset_decorator.rb +0 -25
- data/app/filterers/tenon/generic_filterer.rb +24 -0
- data/app/form_builders/tenon/form_builder.rb +189 -19
- data/app/helpers/tenon/active_link_to_helper.rb +9 -0
- data/app/helpers/tenon/application_helper.rb +4 -23
- data/app/helpers/tenon/asset_helper.rb +2 -2
- data/app/helpers/tenon/back_to_index_path_helper.rb +13 -0
- data/app/helpers/tenon/breadcrumbs_helper.rb +33 -0
- data/app/helpers/tenon/flash_helper.rb +14 -0
- data/app/helpers/tenon/icon_helper.rb +7 -0
- data/app/helpers/tenon/input_block_helper.rb +25 -0
- data/app/helpers/tenon/item_version_helper.rb +22 -9
- data/app/helpers/tenon/nav_helper.rb +21 -17
- data/app/helpers/tenon/piece_helper.rb +2 -7
- data/app/helpers/tenon/platform_hints_helper.rb +3 -5
- data/app/helpers/tenon/styleguide_helper.rb +15 -0
- data/app/helpers/tenon/tenon_content_helper.rb +11 -6
- data/app/helpers/tenon/tenon_helper.rb +19 -9
- data/app/models/tenon/application_record.rb +5 -0
- data/app/models/tenon/asset.rb +2 -2
- data/app/models/tenon/item_asset.rb +1 -1
- data/app/models/tenon/item_version.rb +1 -1
- data/app/models/tenon/styleguide.rb +11 -0
- data/app/models/tenon/tenon_content/piece.rb +13 -13
- data/app/models/tenon/tenon_content/row.rb +1 -1
- data/app/models/tenon/tenon_content/row_types/four_column_image.rb +4 -4
- data/app/models/tenon/tenon_content/row_types/four_column_image_and_text.rb +8 -8
- data/app/models/tenon/tenon_content/row_types/four_column_text.rb +4 -4
- data/app/models/tenon/tenon_content/row_types/full_width_embedded_content.rb +1 -1
- data/app/models/tenon/tenon_content/row_types/full_width_image.rb +1 -1
- data/app/models/tenon/tenon_content/row_types/full_width_text.rb +1 -1
- data/app/models/tenon/tenon_content/row_types/left_image_right_text.rb +2 -2
- data/app/models/tenon/tenon_content/row_types/left_text_right_image.rb +2 -2
- data/app/models/tenon/tenon_content/row_types/left_wrapped_image_with_text.rb +2 -2
- data/app/models/tenon/tenon_content/row_types/right_wrapped_image_with_text.rb +2 -2
- data/app/models/tenon/tenon_content/row_types/six_column_image.rb +6 -6
- data/app/models/tenon/tenon_content/row_types/six_column_image_and_text.rb +12 -12
- data/app/models/tenon/tenon_content/row_types/three_column_image.rb +3 -3
- data/app/models/tenon/tenon_content/row_types/three_column_image_and_text.rb +6 -6
- data/app/models/tenon/tenon_content/row_types/three_column_text.rb +3 -3
- data/app/models/tenon/tenon_content/row_types/two_column_image.rb +2 -2
- data/app/models/tenon/tenon_content/row_types/two_column_image_and_text.rb +4 -4
- data/app/models/tenon/tenon_content/row_types/two_column_text.rb +2 -2
- data/app/models/tenon/tenon_content/row_types/two_column_with_background_image.rb +3 -3
- data/app/models/tenon/tenon_content/row_types.rb +17 -17
- data/app/policies/my_settings_policy.rb +2 -0
- data/app/policies/tenon/application_policy.rb +63 -0
- data/app/policies/tenon/asset_policy.rb +4 -0
- data/app/policies/tenon/item_version_policy.rb +4 -0
- data/app/serializers/item_version_serializer.rb +23 -0
- data/app/serializers/tenon/application_serializer.rb +37 -0
- data/app/serializers/tenon/asset_serializer.rb +5 -0
- data/app/serializers/tenon/paginating_serializer.rb +18 -0
- data/app/views/layouts/tenon/application.html.haml +28 -18
- data/app/views/tenon/assets/_form.html.haml +29 -12
- data/app/views/tenon/assets/crop.html.haml +13 -13
- data/app/views/tenon/assets/edit.html.haml +1 -12
- data/app/views/tenon/assets/index.html.haml +5 -19
- data/app/views/tenon/assets/new.html.haml +1 -0
- data/app/views/tenon/fields/_asset.html.haml +26 -27
- data/app/views/tenon/fields/_base.html.haml +7 -0
- data/app/views/tenon/fields/_base_inline.html.haml +4 -0
- data/app/views/tenon/fields/_check_box.html.haml +9 -0
- data/app/views/tenon/fields/_collection_select.html.haml +4 -0
- data/app/views/tenon/fields/_color_field.html.haml +3 -0
- data/app/views/tenon/fields/_date_picker.html.haml +2 -0
- data/app/views/tenon/fields/_date_time_picker.html.haml +6 -0
- data/app/views/tenon/fields/_email_field.html.haml +2 -0
- data/app/views/tenon/fields/_inline_check_box.html.haml +5 -0
- data/app/views/tenon/fields/_inline_radio_button.html.haml +5 -0
- data/app/views/tenon/fields/_number_field.html.haml +2 -0
- data/app/views/tenon/fields/_password_field.html.haml +2 -0
- data/app/views/tenon/fields/_phone_field.html.haml +2 -0
- data/app/views/tenon/fields/_radio_button.html.haml +9 -0
- data/app/views/tenon/fields/_rich_text.html.haml +3 -6
- data/app/views/tenon/fields/_select.html.haml +3 -0
- data/app/views/tenon/fields/_text_area.html.haml +2 -0
- data/app/views/tenon/fields/_text_field.html.haml +2 -0
- data/app/views/tenon/fields/_title_text_field.html.haml +2 -0
- data/app/views/tenon/fields/_url_field.html.haml +2 -0
- data/app/views/tenon/index/index.html.haml +8 -29
- data/app/views/tenon/item_assets/new.html.haml +20 -22
- data/app/views/tenon/item_versions/_item_version.json.jbuilder +4 -4
- data/app/views/tenon/item_versions/index.html.haml +14 -6
- data/app/views/tenon/item_versions/new.html.haml +8 -10
- data/app/views/tenon/settings/_contact.html.haml +14 -19
- data/app/views/tenon/settings/_general.html.haml +15 -11
- data/app/views/tenon/settings/_seo.html.haml +10 -9
- data/app/views/tenon/settings/show.html.haml +12 -31
- data/app/views/tenon/shared/_account_dropdown.html.haml +6 -0
- data/app/views/tenon/shared/_app_info.html.haml +5 -0
- data/app/views/tenon/shared/_breadcrumbs.html.haml +1 -0
- data/app/views/tenon/shared/_burger.html.haml +5 -0
- data/app/views/tenon/shared/_default_form_toolbar.html.haml +26 -0
- data/app/views/tenon/shared/_draft_controls.html.haml +18 -0
- data/app/views/tenon/shared/_i18n_language_nav.html.haml +2 -2
- data/app/views/tenon/shared/_main_nav.html.haml +3 -14
- data/app/views/tenon/shared/_platform_hints.html.haml +15 -15
- data/app/views/tenon/shared/_seo_fields.html.haml +8 -6
- data/app/views/tenon/shared/_site_title.html.haml +6 -0
- data/app/views/tenon/shared/_util_nav.html.haml +4 -27
- data/app/views/tenon/shared/_version_warning.html.haml +11 -11
- data/app/views/tenon/styleguides/_buttons.html.haml +118 -0
- data/app/views/tenon/styleguides/_colors.html.haml +59 -0
- data/app/views/tenon/styleguides/_forms.html.haml +4 -0
- data/app/views/tenon/styleguides/_icons.html.haml +21 -0
- data/app/views/tenon/styleguides/_typography.html.haml +27 -0
- data/app/views/tenon/styleguides/buttons/_colors.html.haml +43 -0
- data/app/views/tenon/styleguides/buttons/_styles.html.haml +59 -0
- data/app/views/tenon/styleguides/colors/_helpers.html.haml +48 -0
- data/app/views/tenon/styleguides/colors/_variables.html.haml +31 -0
- data/app/views/tenon/styleguides/forms/_default.html.haml +92 -0
- data/app/views/tenon/styleguides/forms/_html5.html.haml +31 -0
- data/app/views/tenon/styleguides/forms/_tenon_content.html.haml +15 -0
- data/app/views/tenon/styleguides/index.html.haml +52 -0
- data/app/views/tenon/styleguides/typography/_blockquote.html.haml +7 -0
- data/app/views/tenon/styleguides/typography/_inline.html.haml +8 -0
- data/app/views/tenon/styleguides/typography/_lists.html.haml +40 -0
- data/app/views/tenon/styleguides/typography/_paragraphs.html.haml +17 -0
- data/app/views/tenon/styleguides/typography/_small.html.haml +18 -0
- data/app/views/tenon/styleguides/typography/_styles.html.haml +14 -0
- data/app/views/tenon/styleguides/typography/_variables.html.haml +43 -0
- data/app/views/tenon/tenon_content/_display.html.haml +2 -2
- data/app/views/tenon/tenon_content/_embed_modal.html.haml +3 -3
- data/app/views/tenon/tenon_content/_fields.html.haml +21 -35
- data/app/views/tenon/tenon_content/_row.html.haml +14 -13
- data/app/views/tenon/tenon_content/{_builder.html.haml → _tenon_content.html.haml} +2 -2
- data/app/views/tenon/tenon_content/piece_types/form/_background_image.html.haml +6 -6
- data/app/views/tenon/tenon_content/piece_types/form/_embedded_content.html.haml +2 -1
- data/app/views/tenon/tenon_content/piece_types/form/_image.html.haml +37 -29
- data/app/views/tenon/tenon_content/row_types/display/_four_column_image.html.haml +4 -4
- data/app/views/tenon/tenon_content/row_types/display/_four_column_image_and_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/display/_four_column_text.html.haml +4 -4
- data/app/views/tenon/tenon_content/row_types/display/_full_width_embedded_content.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/display/_full_width_image.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/display/_full_width_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/display/_left_image_right_text.html.haml +8 -8
- data/app/views/tenon/tenon_content/row_types/display/_left_text_right_image.html.haml +7 -7
- data/app/views/tenon/tenon_content/row_types/display/_left_wrapped_image_with_text.html.haml +6 -6
- data/app/views/tenon/tenon_content/row_types/display/_right_wrapped_image_with_text.html.haml +7 -7
- data/app/views/tenon/tenon_content/row_types/display/_six_column_image.html.haml +4 -4
- data/app/views/tenon/tenon_content/row_types/display/_six_column_image_and_text.html.haml +5 -5
- data/app/views/tenon/tenon_content/row_types/display/_three_column_image.html.haml +4 -4
- data/app/views/tenon/tenon_content/row_types/display/_three_column_image_and_text.html.haml +4 -4
- data/app/views/tenon/tenon_content/row_types/display/_three_column_text.html.haml +4 -4
- data/app/views/tenon/tenon_content/row_types/display/_two_column_image.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/display/_two_column_image_and_text.html.haml +4 -4
- data/app/views/tenon/tenon_content/row_types/display/_two_column_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/display/_two_column_with_background_image.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_four_column_image.html.haml +2 -2
- data/app/views/tenon/tenon_content/row_types/form/_four_column_image_and_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_four_column_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_full_width_embedded_content.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_full_width_image.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_full_width_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_left_image_right_text.html.haml +9 -8
- data/app/views/tenon/tenon_content/row_types/form/_left_text_right_image.html.haml +6 -6
- data/app/views/tenon/tenon_content/row_types/form/_left_wrapped_image_with_text.html.haml +7 -7
- data/app/views/tenon/tenon_content/row_types/form/_right_wrapped_image_with_text.html.haml +7 -7
- data/app/views/tenon/tenon_content/row_types/form/_six_column_image.html.haml +2 -2
- data/app/views/tenon/tenon_content/row_types/form/_six_column_image_and_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_three_column_image.html.haml +2 -2
- data/app/views/tenon/tenon_content/row_types/form/_three_column_image_and_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_three_column_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_two_column_image.html.haml +2 -2
- data/app/views/tenon/tenon_content/row_types/form/_two_column_image_and_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_two_column_text.html.haml +3 -3
- data/app/views/tenon/tenon_content/row_types/form/_two_column_with_background_image.html.haml +2 -2
- data/config/initializers/class_extensions/hash.rb +1 -1
- data/config/initializers/client_side_validations.rb +27 -0
- data/config/initializers/tenon.rb +0 -14
- data/config/routes.rb +10 -46
- data/db/migrate/20151117171000_drop_default_tenon_resources.rb +15 -0
- data/db/migrate/20160119185645_remove_tenon_users.rb +7 -0
- data/db/migrate/20160126123154_remove_tenon_callouts_subscribers.rb +8 -0
- data/db/migrate/20160126194219_convert_piece_size_to_int.rb +6 -0
- data/lib/ckeditor/plugins/pastefromword/filter/default.js +1 -1
- data/lib/generators/tenon/scaffold/scaffold_generator.rb +52 -27
- data/lib/generators/tenon/scaffold/templates/controller.rb +7 -4
- data/lib/generators/tenon/scaffold/templates/policy.rb +2 -0
- data/lib/generators/tenon/scaffold/templates/serializer.rb +2 -0
- data/lib/generators/tenon/scaffold/templates/view__form.html.haml +49 -54
- data/lib/generators/tenon/scaffold/templates/view_edit.html.haml +1 -8
- data/lib/generators/tenon/scaffold/templates/view_index.html.haml +8 -24
- data/lib/generators/tenon/scaffold/templates/view_new.html.haml +1 -8
- data/lib/tasks/tenon_tasks.rake +2 -2
- data/lib/templates/active_record/model/model.rb +12 -13
- data/lib/templates/migration/templates/create_table_migration.rb +1 -1
- data/lib/tenon/can_be_foreign.rb +1 -1
- data/lib/tenon/engine.rb +26 -32
- data/lib/tenon/filterers/base_filterer.rb +64 -0
- data/lib/tenon/has_history/attr_serializer.rb +6 -6
- data/lib/tenon/proxy_attachment.rb +2 -2
- data/lib/tenon/version.rb +1 -1
- data/lib/tenon.rb +1 -3
- data/vendor/assets/javascripts/bootstrap.modal.js +1 -1
- data/vendor/assets/javascripts/date_picker/picker.date.js +1429 -0
- data/vendor/assets/javascripts/date_picker/picker.js +1122 -0
- data/vendor/assets/javascripts/number-to-words.es6 +35 -0
- data/vendor/assets/stylesheets/bootstrap.scss +1 -1
- metadata +206 -972
- data/app/assets/images/tenon/icons/delete.png +0 -0
- data/app/assets/images/tenon/icons/edit.png +0 -0
- data/app/assets/images/tenon/icons/thumbdown.png +0 -0
- data/app/assets/images/tenon/icons/thumbup.png +0 -0
- data/app/assets/images/tenon/select2-spinner.gif +0 -0
- data/app/assets/images/tenon/select2.png +0 -0
- data/app/assets/images/tenon/select2x2.png +0 -0
- data/app/assets/javascripts/tenon/controllers/assets.js.coffee +0 -3
- data/app/assets/javascripts/tenon/controllers/index.js.coffee +0 -6
- data/app/assets/javascripts/tenon/controllers/tenon.js.coffee +0 -1
- data/app/assets/javascripts/tenon/features/asset_attachment.js.coffee +0 -37
- data/app/assets/javascripts/tenon/features/asset_cropping.js.coffee +0 -49
- data/app/assets/javascripts/tenon/features/asset_detachment.js.coffee +0 -11
- data/app/assets/javascripts/tenon/features/asset_list_post_crop_handler.js.coffee +0 -15
- data/app/assets/javascripts/tenon/features/asset_uploader.js.coffee +0 -88
- data/app/assets/javascripts/tenon/features/box_toggles.js.coffee +0 -17
- data/app/assets/javascripts/tenon/features/cocoon_hooks.js.coffee +0 -12
- data/app/assets/javascripts/tenon/features/date_time_picker.js.coffee +0 -25
- data/app/assets/javascripts/tenon/features/editor.js.coffee +0 -24
- data/app/assets/javascripts/tenon/features/file_select_widget.js.erb +0 -39
- data/app/assets/javascripts/tenon/features/flash.js.coffee +0 -13
- data/app/assets/javascripts/tenon/features/focus_first_field.js.coffee +0 -8
- data/app/assets/javascripts/tenon/features/hamburger_navigation.js.coffee +0 -26
- data/app/assets/javascripts/tenon/features/header_menu.js.coffee +0 -20
- data/app/assets/javascripts/tenon/features/i18n_fields.js.coffee +0 -23
- data/app/assets/javascripts/tenon/features/infinite_loading.js.coffee +0 -12
- data/app/assets/javascripts/tenon/features/item_version_autosave.js.coffee +0 -35
- data/app/assets/javascripts/tenon/features/item_version_index_handler.js.coffee +0 -3
- data/app/assets/javascripts/tenon/features/main_menu.js.coffee +0 -11
- data/app/assets/javascripts/tenon/features/modal_forms.js.coffee +0 -44
- data/app/assets/javascripts/tenon/features/modal_windows.js.coffee +0 -104
- data/app/assets/javascripts/tenon/features/multiple_asset_attachment.js.coffee +0 -8
- data/app/assets/javascripts/tenon/features/nested_fields.js +0 -95
- data/app/assets/javascripts/tenon/features/nested_lists.js +0 -10
- data/app/assets/javascripts/tenon/features/nested_set_writer.js.coffee +0 -65
- data/app/assets/javascripts/tenon/features/new_item_version_handler.js.coffee +0 -32
- data/app/assets/javascripts/tenon/features/pagination.js.coffee +0 -21
- data/app/assets/javascripts/tenon/features/protect_changes.js.coffee +0 -25
- data/app/assets/javascripts/tenon/features/quick_search.js.coffee +0 -41
- data/app/assets/javascripts/tenon/features/record_approval.js.coffee +0 -33
- data/app/assets/javascripts/tenon/features/record_boolean_toggle.js.coffee +0 -37
- data/app/assets/javascripts/tenon/features/record_deletion.js.coffee +0 -24
- data/app/assets/javascripts/tenon/features/record_list.js.coffee +0 -86
- data/app/assets/javascripts/tenon/features/record_list_updater.js.coffee +0 -56
- data/app/assets/javascripts/tenon/features/s3_direct_upload.js.coffee +0 -43
- data/app/assets/javascripts/tenon/features/sidebar_active_links.js.coffee +0 -20
- data/app/assets/javascripts/tenon/features/sortable_nested_fields.js.coffee +0 -17
- data/app/assets/javascripts/tenon/features/tenon_content/aesthetics.js.coffee +0 -31
- data/app/assets/javascripts/tenon/features/tenon_content/asset_attachment.js.coffee +0 -15
- data/app/assets/javascripts/tenon/features/tenon_content/asset_link.js.coffee +0 -11
- data/app/assets/javascripts/tenon/features/tenon_content/base.js.coffee +0 -16
- data/app/assets/javascripts/tenon/features/tenon_content/bottombar_toggler.js.coffee +0 -15
- data/app/assets/javascripts/tenon/features/tenon_content/caption_toggler.js.coffee +0 -11
- data/app/assets/javascripts/tenon/features/tenon_content/column_sizing.js.coffee +0 -53
- data/app/assets/javascripts/tenon/features/tenon_content/column_swap.js.coffee +0 -44
- data/app/assets/javascripts/tenon/features/tenon_content/editor.js.coffee +0 -34
- data/app/assets/javascripts/tenon/features/tenon_content/embedded_content_modal_handler.js.coffee +0 -12
- data/app/assets/javascripts/tenon/features/tenon_content/image_asset_link.js.coffee +0 -5
- data/app/assets/javascripts/tenon/features/tenon_content/image_controls.js.coffee +0 -63
- data/app/assets/javascripts/tenon/features/tenon_content/image_links.js.coffee +0 -54
- data/app/assets/javascripts/tenon/features/tenon_content/library.js.coffee +0 -39
- data/app/assets/javascripts/tenon/features/tenon_content/library_filter.js.coffee +0 -17
- data/app/assets/javascripts/tenon/features/tenon_content/pop_out.js.coffee +0 -46
- data/app/assets/javascripts/tenon/features/tenon_content/post_crop_handler.js.coffee +0 -22
- data/app/assets/javascripts/tenon/features/tenon_content/sidebar_navigation.js.coffee +0 -26
- data/app/assets/javascripts/tenon/features/tenon_content/sortable.js.coffee +0 -18
- data/app/assets/javascripts/tenon/features/tenon_content/stretch_to_fill.js.coffee +0 -50
- data/app/assets/javascripts/tenon/features/tenon_content/wrapped_sizing.js.coffee +0 -48
- data/app/assets/javascripts/tenon/features/tenon_content.js.coffee +0 -1
- data/app/assets/javascripts/tenon/features/video_feeds.js.coffee +0 -4
- data/app/assets/javascripts/tenon/manifest.json +0 -44
- data/app/assets/javascripts/tenon/medium-on-tenon.js +0 -1465
- data/app/assets/javascripts/tenon/templates/assets/asset_field.jst.eco +0 -17
- data/app/assets/javascripts/tenon/templates/assets/asset_progress.jst.eco +0 -8
- data/app/assets/javascripts/tenon/templates/assets/asset_row.jst.eco +0 -21
- data/app/assets/javascripts/tenon/templates/assets/asset_select.jst.eco +0 -1
- data/app/assets/javascripts/tenon/templates/comments/comment_row.jst.eco +0 -23
- data/app/assets/javascripts/tenon/templates/contacts/contact_row.jst.eco +0 -56
- data/app/assets/javascripts/tenon/templates/errors.jst.eco +0 -13
- data/app/assets/javascripts/tenon/templates/events/event_row.jst.eco +0 -22
- data/app/assets/javascripts/tenon/templates/galleries/gallery_row.jst.eco +0 -12
- data/app/assets/javascripts/tenon/templates/item_versions/item_version_row.jst.eco +0 -22
- data/app/assets/javascripts/tenon/templates/modal.jst.eco +0 -14
- data/app/assets/javascripts/tenon/templates/pages/page_row.jst.eco +0 -21
- data/app/assets/javascripts/tenon/templates/post_categories/post_category_row.jst.eco +0 -12
- data/app/assets/javascripts/tenon/templates/posts/post_row.jst.eco +0 -30
- data/app/assets/javascripts/tenon/templates/redirects/redirect_row.jst.eco +0 -18
- data/app/assets/javascripts/tenon/templates/tenon_callouts/tenon_callout_row.jst.eco +0 -19
- data/app/assets/javascripts/tenon/templates/tenon_content/popped_out.jst.eco +0 -6
- data/app/assets/javascripts/tenon/templates/users/user_row.jst.eco +0 -21
- data/app/assets/javascripts/tenon/tenon.js +0 -68
- data/app/assets/javascripts/tenon/tenon_dispatcher.js.coffee +0 -25
- data/app/assets/javascripts/tenon/tenon_manifest.js +0 -45
- data/app/assets/stylesheets/tenon/colors-custom.scss +0 -25
- data/app/assets/stylesheets/tenon/colors-named.scss +0 -22
- data/app/assets/stylesheets/tenon/helpers.scss +0 -28
- data/app/assets/stylesheets/tenon/layout/assets.scss +0 -49
- data/app/assets/stylesheets/tenon/layout/breakpoints.scss +0 -48
- data/app/assets/stylesheets/tenon/layout/global.scss +0 -36
- data/app/assets/stylesheets/tenon/layout/grid.scss +0 -145
- data/app/assets/stylesheets/tenon/layout/main-nav.scss +0 -149
- data/app/assets/stylesheets/tenon/layout/margins.scss +0 -3
- data/app/assets/stylesheets/tenon/layout/mobile.scss +0 -5
- data/app/assets/stylesheets/tenon/layout/sidebar.scss +0 -102
- data/app/assets/stylesheets/tenon/layout/util-nav.scss +0 -51
- data/app/assets/stylesheets/tenon/mixins.scss +0 -81
- data/app/assets/stylesheets/tenon/styleguide.scss +0 -6
- data/app/assets/stylesheets/tenon/tenon.scss +0 -71
- data/app/assets/stylesheets/tenon/typography.scss +0 -65
- data/app/assets/stylesheets/tenon/ui/alerts.scss +0 -65
- data/app/assets/stylesheets/tenon/ui/asset-attachment.scss +0 -136
- data/app/assets/stylesheets/tenon/ui/asset-cropping.scss +0 -51
- data/app/assets/stylesheets/tenon/ui/asset-uploads.scss +0 -26
- data/app/assets/stylesheets/tenon/ui/buttons.scss +0 -65
- data/app/assets/stylesheets/tenon/ui/callouts.scss +0 -26
- data/app/assets/stylesheets/tenon/ui/comments.scss +0 -57
- data/app/assets/stylesheets/tenon/ui/forms.scss +0 -204
- data/app/assets/stylesheets/tenon/ui/generic-loader.scss +0 -21
- data/app/assets/stylesheets/tenon/ui/header-tools.scss +0 -116
- data/app/assets/stylesheets/tenon/ui/i18n.scss +0 -18
- data/app/assets/stylesheets/tenon/ui/list-style-toggle.scss +0 -39
- data/app/assets/stylesheets/tenon/ui/login.scss +0 -97
- data/app/assets/stylesheets/tenon/ui/medium-editor.scss +0 -114
- data/app/assets/stylesheets/tenon/ui/modals.scss +0 -179
- data/app/assets/stylesheets/tenon/ui/pagination.scss +0 -113
- data/app/assets/stylesheets/tenon/ui/progress-bars.scss +0 -131
- data/app/assets/stylesheets/tenon/ui/quick-search.scss +0 -20
- data/app/assets/stylesheets/tenon/ui/record-grids.scss +0 -95
- data/app/assets/stylesheets/tenon/ui/record-lists.scss +0 -154
- data/app/assets/stylesheets/tenon/ui/section-headers.scss +0 -30
- data/app/assets/stylesheets/tenon/ui/select2-custom.scss +0 -70
- data/app/assets/stylesheets/tenon/ui/tables.scss +0 -19
- data/app/assets/stylesheets/tenon/ui/tabs.scss +0 -7
- data/app/assets/stylesheets/tenon/ui/tenon-content-library.scss +0 -80
- data/app/assets/stylesheets/tenon/ui/tenon-content-popped-out.scss +0 -92
- data/app/assets/stylesheets/tenon/ui/tenon-content.scss +0 -318
- data/app/assets/stylesheets/tenon/ui/thinking.scss +0 -3
- data/app/assets/stylesheets/tenon/ui/toolbox.scss +0 -31
- data/app/assets/stylesheets/tenon/z-indexes.scss +0 -35
- data/app/assets/stylesheets/tenon_addons.scss +0 -1
- data/app/controllers/tenon/comments_controller.rb +0 -51
- data/app/controllers/tenon/contacts_controller.rb +0 -65
- data/app/controllers/tenon/events_controller.rb +0 -9
- data/app/controllers/tenon/galleries_controller.rb +0 -9
- data/app/controllers/tenon/pages_controller.rb +0 -51
- data/app/controllers/tenon/post_categories_controller.rb +0 -9
- data/app/controllers/tenon/posts_controller.rb +0 -28
- data/app/controllers/tenon/redirects_controller.rb +0 -25
- data/app/controllers/tenon/tenon_callouts_controller.rb +0 -7
- data/app/controllers/tenon/users_controller.rb +0 -64
- data/app/decorators/tenon/comment_decorator.rb +0 -22
- data/app/decorators/tenon/contact_decorator.rb +0 -13
- data/app/decorators/tenon/event_decorator.rb +0 -11
- data/app/decorators/tenon/gallery_decorator.rb +0 -4
- data/app/decorators/tenon/page_decorator.rb +0 -11
- data/app/decorators/tenon/post_category_decorator.rb +0 -4
- data/app/decorators/tenon/post_decorator.rb +0 -4
- data/app/decorators/tenon/redirect_decorator.rb +0 -2
- data/app/decorators/tenon/tenon_callout_decorator.rb +0 -4
- data/app/decorators/tenon/user_decorator.rb +0 -4
- data/app/mailers/tenon/comment_mailer.rb +0 -16
- data/app/mailers/tenon/contact_mailer.rb +0 -15
- data/app/models/ability.rb +0 -32
- data/app/models/tenon/comment.rb +0 -47
- data/app/models/tenon/comment_subscriber.rb +0 -7
- data/app/models/tenon/contact.rb +0 -46
- data/app/models/tenon/event.rb +0 -45
- data/app/models/tenon/gallery.rb +0 -15
- data/app/models/tenon/page.rb +0 -105
- data/app/models/tenon/photo.rb +0 -10
- data/app/models/tenon/post.rb +0 -35
- data/app/models/tenon/post_category.rb +0 -7
- data/app/models/tenon/redirect.rb +0 -17
- data/app/models/tenon/role.rb +0 -8
- data/app/models/tenon/role_assignment.rb +0 -6
- data/app/models/tenon/s3_direct_upload.rb +0 -75
- data/app/models/tenon/tenon_callout.rb +0 -16
- data/app/models/tenon/user.rb +0 -60
- data/app/views/devise/confirmations/new.html.erb +0 -12
- data/app/views/devise/confirmations/new.html.haml +0 -0
- data/app/views/devise/mailer/confirmation_instructions.html.haml +0 -7
- data/app/views/devise/mailer/reset_password_instructions.html.haml +0 -15
- data/app/views/devise/mailer/unlock_instructions.html.haml +0 -9
- data/app/views/devise/passwords/edit.html.haml +0 -22
- data/app/views/devise/passwords/new.html.haml +0 -17
- data/app/views/devise/registrations/edit.html.haml +0 -0
- data/app/views/devise/registrations/new.html.haml +0 -22
- data/app/views/devise/sessions/new.html.haml +0 -28
- data/app/views/devise/shared/_links.erb +0 -19
- data/app/views/devise/shared/_links.haml +0 -0
- data/app/views/devise/unlocks/new.html.erb +0 -12
- data/app/views/devise/unlocks/new.html.haml +0 -0
- data/app/views/layouts/tenon/login.html.haml +0 -19
- data/app/views/tenon/assets/_asset.json.jbuilder +0 -7
- data/app/views/tenon/assets/_sidebar_index.html.haml +0 -18
- data/app/views/tenon/assets/create.json.jbuilder +0 -1
- data/app/views/tenon/assets/index.json.jbuilder +0 -5
- data/app/views/tenon/assets/update.json.jbuilder +0 -1
- data/app/views/tenon/comment_mailer/comment_notification.html.haml +0 -7
- data/app/views/tenon/comments/_sidebar_index.html.haml +0 -22
- data/app/views/tenon/comments/index.html.haml +0 -19
- data/app/views/tenon/comments/index.json.jbuilder +0 -22
- data/app/views/tenon/contact_mailer/contact_notification.html.haml +0 -15
- data/app/views/tenon/contacts/_sidebar_index.html.haml +0 -32
- data/app/views/tenon/contacts/index.html.haml +0 -19
- data/app/views/tenon/contacts/index.json.jbuilder +0 -15
- data/app/views/tenon/events/_form.html.haml +0 -44
- data/app/views/tenon/events/edit.html.haml +0 -8
- data/app/views/tenon/events/index.html.haml +0 -23
- data/app/views/tenon/events/index.json.jbuilder +0 -16
- data/app/views/tenon/events/new.html.haml +0 -8
- data/app/views/tenon/galleries/_form.html.haml +0 -38
- data/app/views/tenon/galleries/_photo_fields.html.haml +0 -3
- data/app/views/tenon/galleries/edit.html.haml +0 -8
- data/app/views/tenon/galleries/index.html.haml +0 -22
- data/app/views/tenon/galleries/index.json.jbuilder +0 -15
- data/app/views/tenon/galleries/new.html.haml +0 -8
- data/app/views/tenon/pages/_form.html.haml +0 -58
- data/app/views/tenon/pages/edit.html.haml +0 -8
- data/app/views/tenon/pages/index.html.haml +0 -23
- data/app/views/tenon/pages/index.json.jbuilder +0 -22
- data/app/views/tenon/pages/new.html.haml +0 -8
- data/app/views/tenon/post_categories/_form.html.haml +0 -11
- data/app/views/tenon/post_categories/_post_category.json.jbuilder +0 -2
- data/app/views/tenon/post_categories/create.json.jbuilder +0 -1
- data/app/views/tenon/post_categories/edit.html.haml +0 -1
- data/app/views/tenon/post_categories/index.html.haml +0 -30
- data/app/views/tenon/post_categories/index.json.jbuilder +0 -5
- data/app/views/tenon/post_categories/update.json.jbuilder +0 -1
- data/app/views/tenon/posts/_form.html.haml +0 -65
- data/app/views/tenon/posts/edit.html.haml +0 -8
- data/app/views/tenon/posts/index.html.haml +0 -27
- data/app/views/tenon/posts/index.json.jbuilder +0 -20
- data/app/views/tenon/posts/new.html.haml +0 -8
- data/app/views/tenon/redirects/_form.html.haml +0 -42
- data/app/views/tenon/redirects/_redirect.json.jbuilder +0 -3
- data/app/views/tenon/redirects/edit.html.haml +0 -8
- data/app/views/tenon/redirects/index.html.haml +0 -29
- data/app/views/tenon/redirects/index.json.jbuilder +0 -5
- data/app/views/tenon/redirects/new.html.haml +0 -8
- data/app/views/tenon/shared/_posts_nav.html.haml +0 -8
- data/app/views/tenon/shared/menu_items/_assets.html.haml +0 -3
- data/app/views/tenon/shared/menu_items/_comments.html.haml +0 -2
- data/app/views/tenon/shared/menu_items/_events.html.haml +0 -6
- data/app/views/tenon/shared/menu_items/_galleries.html.haml +0 -6
- data/app/views/tenon/shared/menu_items/_pages.html.haml +0 -6
- data/app/views/tenon/shared/menu_items/_posts.html.haml +0 -7
- data/app/views/tenon/shared/menu_items/_settings.html.haml +0 -2
- data/app/views/tenon/shared/menu_items/_users.html.haml +0 -2
- data/app/views/tenon/shared/section_header/_quick_search.html.haml +0 -3
- data/app/views/tenon/shared/section_header/_sidebar_toggle.html.haml +0 -1
- data/app/views/tenon/tenon_callouts/_form.html.haml +0 -45
- data/app/views/tenon/tenon_callouts/_tenon_callout.json.jbuilder +0 -1
- data/app/views/tenon/tenon_callouts/edit.html.haml +0 -8
- data/app/views/tenon/tenon_callouts/index.html.haml +0 -24
- data/app/views/tenon/tenon_callouts/index.json.jbuilder +0 -5
- data/app/views/tenon/tenon_callouts/new.html.haml +0 -8
- data/app/views/tenon/users/_form.html.haml +0 -50
- data/app/views/tenon/users/edit.html.haml +0 -8
- data/app/views/tenon/users/index.html.haml +0 -23
- data/app/views/tenon/users/index.json.jbuilder +0 -28
- data/app/views/tenon/users/new.html.haml +0 -8
- data/config/initializers/devise.rb +0 -258
- data/config/locales/devise.en.yml +0 -60
- data/config/locales/tenon.en.yml +0 -313
- data/db/seeds.rb +0 -47
- data/lib/generators/tenon/scaffold/templates/view__item.json.jbuilder +0 -1
- data/lib/generators/tenon/scaffold/templates/view_index.json.jbuilder +0 -5
- data/lib/generators/tenon/scaffold/templates/view_item_row.jst.eco +0 -13
- data/lib/generators/tenon/scaffold_small/scaffold_small_generator.rb +0 -8
- data/lib/generators/tenon/scaffold_small/templates/controller.rb +0 -19
- data/lib/generators/tenon/scaffold_small/templates/decorator.rb +0 -2
- data/lib/generators/tenon/scaffold_small/templates/view__form.html.haml +0 -26
- data/lib/generators/tenon/scaffold_small/templates/view__item.json.jbuilder +0 -2
- data/lib/generators/tenon/scaffold_small/templates/view_create.json.jbuilder +0 -1
- data/lib/generators/tenon/scaffold_small/templates/view_index.html.haml +0 -27
- data/lib/generators/tenon/scaffold_small/templates/view_index.json.jbuilder +0 -5
- data/lib/generators/tenon/scaffold_small/templates/view_item_row.jst.eco +0 -12
- data/lib/generators/tenon/scaffold_small/templates/view_update.json.jbuilder +0 -1
- data/lib/tenon/can_have_comments.rb +0 -16
- data/lib/tenon/config/events.rb +0 -29
- data/lib/tenon/factories/comments.rb +0 -10
- data/lib/tenon/factories/contacts.rb +0 -9
- data/lib/tenon/factories/events.rb +0 -7
- data/lib/tenon/factories/galleries.rb +0 -5
- data/lib/tenon/factories/pages.rb +0 -5
- data/lib/tenon/factories/posts.rb +0 -10
- data/lib/tenon/factories/redirects.rb +0 -9
- data/lib/tenon/factories/tenon_callouts.rb +0 -9
- data/lib/tenon/factories/users.rb +0 -26
- data/lib/tenon/warning_generator.rb +0 -29
- data/spec/controllers/tenon/assets_controller_spec.rb +0 -262
- data/spec/controllers/tenon/comments_controller_spec.rb +0 -173
- data/spec/controllers/tenon/contacts_controller_spec.rb +0 -174
- data/spec/controllers/tenon/index_controller_spec.rb +0 -31
- data/spec/controllers/tenon/item_assets_controller_spec.rb +0 -29
- data/spec/controllers/tenon/pages_controller_spec.rb +0 -67
- data/spec/controllers/tenon/posts_controller_spec.rb +0 -31
- data/spec/controllers/tenon/resources_controller_spec.rb +0 -352
- data/spec/controllers/tenon/settings_controller_spec.rb +0 -65
- data/spec/controllers/tenon/simple_resources_controller_spec.rb +0 -42
- data/spec/controllers/tenon/users_controller_spec.rb +0 -112
- data/spec/decorators/tenon/application_decorator_spec.rb +0 -75
- data/spec/decorators/tenon/asset_decorator_spec.rb +0 -101
- data/spec/decorators/tenon/comment_decorator_spec.rb +0 -43
- data/spec/decorators/tenon/contact_decorator_spec.rb +0 -24
- data/spec/decorators/tenon/event_decorator_spec.rb +0 -26
- data/spec/decorators/tenon/page_decorator_spec.rb +0 -23
- data/spec/decorators/tenon/tenon_content/row_type_decorator_spec.rb +0 -20
- data/spec/features/settings_spec.rb +0 -75
- data/spec/features/tenon/assets_spec.rb +0 -88
- data/spec/features/tenon/comments_spec.rb +0 -51
- data/spec/features/tenon/contacts_spec.rb +0 -51
- data/spec/features/tenon/events_spec.rb +0 -104
- data/spec/features/tenon/galleries_spec.rb +0 -107
- data/spec/features/tenon/i18n_spec.rb +0 -106
- data/spec/features/tenon/pages_spec.rb +0 -97
- data/spec/features/tenon/post_categories_spec.rb +0 -91
- data/spec/features/tenon/posts_spec.rb +0 -93
- data/spec/features/tenon/tenon_callouts_spec.rb +0 -101
- data/spec/features/tenon/users_spec.rb +0 -98
- data/spec/fixtures/files/test.png +0 -0
- data/spec/lib/tenon/asset_style_generator_spec.rb +0 -84
- data/spec/lib/tenon/can_be_foreign_spec.rb +0 -20
- data/spec/lib/tenon/can_have_comments_spec.rb +0 -22
- data/spec/lib/tenon/has_asset_spec.rb +0 -46
- data/spec/lib/tenon/has_history/attr_serializer_spec.rb +0 -63
- data/spec/lib/tenon/i18n_lookup_spec.rb +0 -88
- data/spec/lib/tenon/proxy_attachment_spec.rb +0 -96
- data/spec/lib/tenon/reorderable_spec.rb +0 -25
- data/spec/lib/tenon/tenon_content_spec.rb +0 -22
- data/spec/lib/tenon/warning_generator_spec.rb +0 -153
- data/spec/models/tenon/asset_spec.rb +0 -98
- data/spec/models/tenon/comment_spec.rb +0 -81
- data/spec/models/tenon/contact_spec.rb +0 -62
- data/spec/models/tenon/event_spec.rb +0 -79
- data/spec/models/tenon/gallery_spec.rb +0 -12
- data/spec/models/tenon/item_asset_spec.rb +0 -19
- data/spec/models/tenon/my_settings_spec.rb +0 -27
- data/spec/models/tenon/page_spec.rb +0 -60
- data/spec/models/tenon/post_spec.rb +0 -48
- data/spec/models/tenon/redirect_spec.rb +0 -31
- data/spec/models/tenon/tenon_callout_spec.rb +0 -10
- data/spec/models/tenon/tenon_content/row_spec.rb +0 -34
- data/spec/models/tenon/user_spec.rb +0 -59
- data/spec/services/tenon/redirector_spec.rb +0 -30
- data/spec/spec_helper.rb +0 -79
- data/spec/support/integration_example_group.rb +0 -35
- data/spec/support/request_helpers.rb +0 -10
- data/vendor/assets/javascripts/backstretch.js +0 -4
- data/vendor/assets/javascripts/bootstrap.collapse.js +0 -179
- data/vendor/assets/javascripts/bootstrap.datetimepicker.js +0 -954
- data/vendor/assets/javascripts/bootstrap.tabs.js +0 -135
- data/vendor/assets/javascripts/cufon/Aller_400.font.js +0 -7
- data/vendor/assets/javascripts/cufon/Aller_700.font.js +0 -7
- data/vendor/assets/javascripts/cufon/cufon.js +0 -7
- data/vendor/assets/javascripts/jquery.corner.js +0 -249
- data/vendor/assets/javascripts/jquery.hoverIntent.js +0 -115
- data/vendor/assets/javascripts/jquery.radioSlider.js +0 -55
- data/vendor/assets/javascripts/jscrollpane.js +0 -1435
- data/vendor/assets/javascripts/select2.js +0 -3
- data/vendor/assets/javascripts/underscore.inflection.js +0 -177
- data/vendor/assets/stylesheets/bootstrap.datetimepicker.css +0 -152
- data/vendor/assets/stylesheets/bootstrap.tables.scss +0 -201
- data/vendor/assets/stylesheets/jscrollpane.scss +0 -20
- 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
|
-
|
15
|
+
gem 'tenon'
|
14
16
|
|
15
17
|
and then bundle install.
|
16
18
|
|
17
19
|
in config/application.rb:
|
18
20
|
|
19
|
-
|
21
|
+
require 'active_record/railtie'
|
20
22
|
|
21
23
|
|
22
24
|
in config/routes.rb:
|
23
25
|
|
24
|
-
|
26
|
+
mount Tenon::Engine => '/tenon'
|
25
27
|
|
26
28
|
|
27
29
|
Run command:
|
28
30
|
|
29
|
-
|
30
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
46
|
+
$ rake db:migrate
|
45
47
|
|
46
48
|
|
47
49
|
Install the Tenon helpers in app/controllers/application_controller.rb:
|
48
50
|
|
49
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
134
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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'} ]
|