├── .gitignore ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── activeadmin_ancestry_view.gemspec ├── app ├── assets │ ├── javascripts │ │ └── activeadmin_ancestry_view │ │ │ ├── base.coffee │ │ │ └── vanilla-base.js │ └── stylesheets │ │ └── activeadmin_ancestry_view │ │ ├── .keep │ │ └── base.scss ├── helpers │ ├── .keep │ └── active_admin │ │ └── activeadmin_ancestry_view │ │ └── nodes_helper.rb ├── models │ └── concerns │ │ └── activeadmin_ancestry_view │ │ └── model_methods.rb └── views │ ├── .keep │ └── activeadmin_ancestry_view │ ├── _content_table.slim │ ├── _main.slim │ └── _panel_header_links.slim ├── bin └── rails ├── config └── locales │ └── activeadmin_ancestry_view.en.yml ├── docs └── images │ └── resource_tree.gif └── lib ├── activeadmin_ancestry_view.rb ├── activeadmin_ancestry_view ├── action_builder │ ├── action_builder.rb │ ├── index_action_builder.rb │ └── show_action_builder.rb ├── action_generator.rb ├── ancestry_view.rb ├── controller_builder │ ├── controller_builder.rb │ ├── index_controller_builder.rb │ └── show_controller_builder.rb ├── controller_generator.rb ├── engine.rb ├── exceptions.rb ├── finder.rb └── version.rb └── generators └── activeadmin_ancestry_view └── install └── install_generator.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | test/dummy/.sass-cache 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in activeadmin_ancestry_view.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | # gem 'byebug', group: [:development, :test] 15 | 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | activeadmin_ancestry_view (0.0.2) 5 | activeadmin (>= 1.0.0.pre4) 6 | ancestry 7 | jquery-rails 8 | rails (>= 4.2) 9 | slim-rails 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | actioncable (5.2.6) 15 | actionpack (= 5.2.6) 16 | nio4r (~> 2.0) 17 | websocket-driver (>= 0.6.1) 18 | actionmailer (5.2.6) 19 | actionpack (= 5.2.6) 20 | actionview (= 5.2.6) 21 | activejob (= 5.2.6) 22 | mail (~> 2.5, >= 2.5.4) 23 | rails-dom-testing (~> 2.0) 24 | actionpack (5.2.6) 25 | actionview (= 5.2.6) 26 | activesupport (= 5.2.6) 27 | rack (~> 2.0, >= 2.0.8) 28 | rack-test (>= 0.6.3) 29 | rails-dom-testing (~> 2.0) 30 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 31 | actionview (5.2.6) 32 | activesupport (= 5.2.6) 33 | builder (~> 3.1) 34 | erubi (~> 1.4) 35 | rails-dom-testing (~> 2.0) 36 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 37 | activeadmin (2.0.0) 38 | arbre (~> 1.2, >= 1.2.1) 39 | formtastic (~> 3.1) 40 | formtastic_i18n (~> 0.4) 41 | inherited_resources (~> 1.7) 42 | jquery-rails (~> 4.2) 43 | kaminari (~> 1.0, >= 1.0.1) 44 | railties (>= 5.0, < 6.0) 45 | ransack (~> 2.1, >= 2.1.1) 46 | sass (~> 3.4) 47 | sprockets (>= 3.0, < 4.1) 48 | sprockets-es6 (~> 0.9, >= 0.9.2) 49 | activejob (5.2.6) 50 | activesupport (= 5.2.6) 51 | globalid (>= 0.3.6) 52 | activemodel (5.2.6) 53 | activesupport (= 5.2.6) 54 | activerecord (5.2.6) 55 | activemodel (= 5.2.6) 56 | activesupport (= 5.2.6) 57 | arel (>= 9.0) 58 | activestorage (5.2.6) 59 | actionpack (= 5.2.6) 60 | activerecord (= 5.2.6) 61 | marcel (~> 1.0.0) 62 | activesupport (5.2.6) 63 | concurrent-ruby (~> 1.0, >= 1.0.2) 64 | i18n (>= 0.7, < 2) 65 | minitest (~> 5.1) 66 | tzinfo (~> 1.1) 67 | ancestry (4.0.0) 68 | activerecord (>= 5.2.4.5) 69 | arbre (1.2.1) 70 | activesupport (>= 3.0.0) 71 | arel (9.0.0) 72 | babel-source (5.8.35) 73 | babel-transpiler (0.7.0) 74 | babel-source (>= 4.0, < 6) 75 | execjs (~> 2.0) 76 | builder (3.2.4) 77 | concurrent-ruby (1.1.8) 78 | crass (1.0.6) 79 | erubi (1.10.0) 80 | execjs (2.7.0) 81 | ffi (1.15.0) 82 | formtastic (3.1.5) 83 | actionpack (>= 3.2.13) 84 | formtastic_i18n (0.6.0) 85 | globalid (0.4.2) 86 | activesupport (>= 4.2.0) 87 | has_scope (0.7.2) 88 | actionpack (>= 4.1) 89 | activesupport (>= 4.1) 90 | i18n (1.8.10) 91 | concurrent-ruby (~> 1.0) 92 | inherited_resources (1.10.0) 93 | actionpack (>= 5.0, < 6.0) 94 | has_scope (~> 0.6) 95 | railties (>= 5.0, < 6.0) 96 | responders (~> 2.0) 97 | jquery-rails (4.4.0) 98 | rails-dom-testing (>= 1, < 3) 99 | railties (>= 4.2.0) 100 | thor (>= 0.14, < 2.0) 101 | kaminari (1.2.1) 102 | activesupport (>= 4.1.0) 103 | kaminari-actionview (= 1.2.1) 104 | kaminari-activerecord (= 1.2.1) 105 | kaminari-core (= 1.2.1) 106 | kaminari-actionview (1.2.1) 107 | actionview 108 | kaminari-core (= 1.2.1) 109 | kaminari-activerecord (1.2.1) 110 | activerecord 111 | kaminari-core (= 1.2.1) 112 | kaminari-core (1.2.1) 113 | loofah (2.9.1) 114 | crass (~> 1.0.2) 115 | nokogiri (>= 1.5.9) 116 | mail (2.7.1) 117 | mini_mime (>= 0.1.1) 118 | marcel (1.0.1) 119 | method_source (1.0.0) 120 | mini_mime (1.1.0) 121 | mini_portile2 (2.4.0) 122 | minitest (5.14.4) 123 | nio4r (2.5.2) 124 | nokogiri (1.10.10) 125 | mini_portile2 (~> 2.4.0) 126 | rack (2.2.3) 127 | rack-test (1.1.0) 128 | rack (>= 1.0, < 3) 129 | rails (5.2.6) 130 | actioncable (= 5.2.6) 131 | actionmailer (= 5.2.6) 132 | actionpack (= 5.2.6) 133 | actionview (= 5.2.6) 134 | activejob (= 5.2.6) 135 | activemodel (= 5.2.6) 136 | activerecord (= 5.2.6) 137 | activestorage (= 5.2.6) 138 | activesupport (= 5.2.6) 139 | bundler (>= 1.3.0) 140 | railties (= 5.2.6) 141 | sprockets-rails (>= 2.0.0) 142 | rails-dom-testing (2.0.3) 143 | activesupport (>= 4.2.0) 144 | nokogiri (>= 1.6) 145 | rails-html-sanitizer (1.3.0) 146 | loofah (~> 2.3) 147 | railties (5.2.6) 148 | actionpack (= 5.2.6) 149 | activesupport (= 5.2.6) 150 | method_source 151 | rake (>= 0.8.7) 152 | thor (>= 0.19.0, < 2.0) 153 | rake (13.0.3) 154 | ransack (2.4.1) 155 | activerecord (>= 5.2.4) 156 | activesupport (>= 5.2.4) 157 | i18n 158 | rb-fsevent (0.10.4) 159 | rb-inotify (0.10.1) 160 | ffi (~> 1.0) 161 | responders (2.4.1) 162 | actionpack (>= 4.2.0, < 6.0) 163 | railties (>= 4.2.0, < 6.0) 164 | sass (3.7.4) 165 | sass-listen (~> 4.0.0) 166 | sass-listen (4.0.0) 167 | rb-fsevent (~> 0.9, >= 0.9.4) 168 | rb-inotify (~> 0.9, >= 0.9.7) 169 | slim (4.1.0) 170 | temple (>= 0.7.6, < 0.9) 171 | tilt (>= 2.0.6, < 2.1) 172 | slim-rails (3.2.0) 173 | actionpack (>= 3.1) 174 | railties (>= 3.1) 175 | slim (>= 3.0, < 5.0) 176 | sprockets (3.7.2) 177 | concurrent-ruby (~> 1.0) 178 | rack (> 1, < 3) 179 | sprockets-es6 (0.9.2) 180 | babel-source (>= 5.8.11) 181 | babel-transpiler 182 | sprockets (>= 3.0.0) 183 | sprockets-rails (3.2.2) 184 | actionpack (>= 4.0) 185 | activesupport (>= 4.0) 186 | sprockets (>= 3.0.0) 187 | temple (0.8.2) 188 | thor (1.1.0) 189 | thread_safe (0.3.6) 190 | tilt (2.0.10) 191 | tzinfo (1.2.9) 192 | thread_safe (~> 0.1) 193 | websocket-driver (0.7.3) 194 | websocket-extensions (>= 0.1.0) 195 | websocket-extensions (0.1.5) 196 | 197 | PLATFORMS 198 | ruby 199 | 200 | DEPENDENCIES 201 | activeadmin_ancestry_view! 202 | bundler (~> 1.13) 203 | 204 | BUNDLED WITH 205 | 1.13.5 206 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Dimkarodinz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveadminAncestryView 2 | This gem allows to vizualize tree of [Ancestry](https://github.com/stefankroes/ancestry) model in [ActiveAdmin](https://github.com/activeadmin/activeadmin) resource. 3 | 4 | 5 | 6 | ## Usage 7 | 8 | *NOTE: Resource model should have `has_ancestry`.* 9 | 10 | Add to your ActiveAdmin resource 11 | ```ruby 12 | ActiveAdmin.register YourModel do 13 | ancestry_view(:index) do 14 | # Some of your code for the 'index' view (optional). 15 | # It will been executed before templates rendering. 16 | end 17 | 18 | ancestry_view(:show) do 19 | # Some of your code for the 'show' view (optional). 20 | # It will been executed before templates rendering. 21 | end 22 | end 23 | 24 | ``` 25 | 26 | ## Options 27 | Options - it's an extended version of `ancestry_views(:index)` or `ancestry_view(:show)`, actually just optional hash. 28 | So please, try to include only *ONE* `ancestry_view` per action (*index* or *show*). Otherwise it will result in the error loop with "stack level too deep" message. 29 | 30 | ```ruby 31 | # Same options available for ancestry_view(:show) 32 | ancestry_view :index, 33 | headers: { 34 | title: :some_model_instance_method, # Node title link name 35 | info: 'Info', # 'Info' link name 36 | expand: 'Expand', # 'Expand' link name 37 | }, 38 | table: { 39 | 'Name' => :first_name, # Key - any string or symbol, value - model instance method. 40 | :email => :email # Table, as well as 'Info' and 'Expand' links, 41 | }, # shows only if :table has some values. 42 | 43 | no_childless_link: true, # Hide link to #show for childless nodes. Default is false 44 | no_color: true, # On/off color nodes. Default is false 45 | shift_depth: 4 # In 'parrots'; looks fine if >= 3. Default is 4 46 | ``` 47 | 48 | ## Model methods 49 | 50 | ```ruby 51 | YourModel.first.full_ancestry # Same as #ancestry, but includes instance id 52 | 53 | YourModel.ordered_collection(ids) # Return ActiveRecord::Relation in order equal to 54 | # ids order. If ids equal to [2,1,3], relation will be 55 | # sorted as [2,1,3] agaist default [1,2,3] way. 56 | 57 | ``` 58 | ## Installation 59 | 60 | Add to your Gemfile 61 | 62 | ```ruby 63 | gem 'activeadmin', github: 'activeadmin' 64 | gem 'ancestry' 65 | 66 | gem 'activeadmin_ancestry_view' 67 | ``` 68 | 69 | And then execute 70 | 71 | ``` 72 | $ bundle 73 | ``` 74 | 75 | For adding required assets and concerns please run 76 | ``` 77 | $ rails g activeadmin_ancestry_view:install 78 | ``` 79 | 80 | ## Customization 81 | For index resource you are still able to use `scoped_collection`. 82 | For example, to not displaying nodes with depth > 2: 83 | 84 | ```ruby 85 | ActiveAdmin.register SomeModel do 86 | ... 87 | controller do 88 | def scoped_collection 89 | resource_class.to_depth(2) 90 | end 91 | end 92 | end 93 | ``` 94 | 95 | Also, you're able to change default locale setting. In `config/locales`: 96 | ``` 97 | uk: 98 | activeadmin_ancestry_view: 99 | expand: Розгорнути 100 | info: Інфо 101 | errors: 102 | wrong_action: "Не той екшн. Дозволені: %{actions}" 103 | ``` 104 | -------------------------------------------------------------------------------- /activeadmin_ancestry_view.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "activeadmin_ancestry_view/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "activeadmin_ancestry_view" 9 | s.version = ActiveadminAncestryView::VERSION 10 | s.authors = ["Dimkarodinz"] 11 | s.email = ["dimkarodin@gmail.com"] 12 | s.homepage = "https://github.com/Dimkarodinz/activeadmin_ancestry_view" 13 | s.summary = "Ancestry tree view in ActiveAdmin resource" 14 | s.description = "Ancestry tree view in ActiveAdmin resource" 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,lib}/**/*", "MIT-LICENSE", "Rakefile"] 18 | 19 | s.add_dependency "rails", ">= 4.2" 20 | 21 | s.add_dependency 'activeadmin', '>= 1.0.0.pre4' 22 | s.add_dependency 'ancestry' 23 | s.add_dependency 'jquery-rails' 24 | s.add_dependency 'slim-rails' 25 | s.add_development_dependency "bundler", "~> 1.13" 26 | 27 | s.required_ruby_version = '>= 2.3.0' 28 | end 29 | -------------------------------------------------------------------------------- /app/assets/javascripts/activeadmin_ancestry_view/base.coffee: -------------------------------------------------------------------------------- 1 | $(document).on 'ready page:load turbolinks:load', -> 2 | CSSRule = 3 | get: (ruleName, deleteFlag) -> 4 | ruleName = ruleName.toLowerCase() 5 | if document.styleSheets 6 | i = 0 7 | while i < document.styleSheets.length 8 | styleSheet = document.styleSheets[i] 9 | ii = 0 10 | cssRule = false 11 | loop 12 | if styleSheet.cssRules 13 | cssRule = styleSheet.cssRules[ii] 14 | else 15 | cssRule = styleSheet.rules[ii] 16 | if cssRule 17 | if cssRule.selectorText.toLowerCase() == ruleName 18 | if deleteFlag == 'delete' 19 | if styleSheet.cssRules 20 | styleSheet.deleteRule ii 21 | else 22 | styleSheet.removeRule ii 23 | return true 24 | else 25 | return cssRule 26 | ii++ 27 | unless cssRule 28 | break 29 | i++ 30 | false 31 | 32 | add: (ruleName, properies) -> 33 | if document.styleSheets 34 | if !this.get(ruleName) 35 | if document.styleSheets[0].addRule 36 | document.styleSheets[0].addRule ruleName, properies, 0 37 | else 38 | document.styleSheets[0].insertRule ruleName + " { #{properies} }", 0 39 | 40 | distance = 41 | middleHeight: (node) -> 42 | Math.abs $(node).height() / 2 43 | 44 | betweenTop: (sourceNode, targetNode) -> 45 | Math.abs $(sourceNode).offset().top - $(targetNode).offset().top 46 | 47 | verticalBranch: (node, lastChild) -> 48 | @betweenTop(node, lastChild) + @middleHeight(lastChild) 49 | 50 | branchProp = 51 | baseMargin: 2 # em 52 | 53 | base: 54 | "transition: 150ms ease; " + 55 | "content: ''; " + 56 | "position: absolute; " + 57 | "border: 0.1em solid gray; " 58 | 59 | vertical: (height) -> 60 | "margin-left: #{@baseMargin}em; " + 61 | "height: #{height}px; " + 62 | "z-index: -1; " + 63 | @base 64 | 65 | horizontal: (marginTop, marginLeft) -> 66 | "margin-top: -#{marginTop}px; " + 67 | "margin-left: -#{marginLeft - @baseMargin}em; " + 68 | "width: #{marginLeft - @baseMargin}em; " + 69 | "z-index: -2; " + 70 | @base 71 | 72 | pseudoElement = 73 | addHorizontal: (node) -> 74 | id = $(node).attr('id') 75 | height = distance.middleHeight(node) 76 | marginLeft = $(node).attr('data-shift-multiplicator') 77 | CSSRule.add( 78 | "[id='#{id}']::after", 79 | branchProp.horizontal(height, marginLeft) 80 | ) 81 | 82 | addVertical: (node, purposeNode) -> 83 | dist = distance.verticalBranch(node, purposeNode) 84 | id = $(node).attr('id') 85 | CSSRule.add "[id='#{id}']::before", branchProp.vertical(dist) 86 | 87 | updHorizontal: (node) -> 88 | if $(node).hasClass('panel-root') then false 89 | else 90 | id = $(node).attr 'id' 91 | newHeight = -Math.abs distance.middleHeight(node) 92 | line = CSSRule.get '[id="' + id + '"]::after' 93 | line.style.marginTop = newHeight.toString().concat('px') 94 | 95 | updVertical: (node) -> 96 | nodeId = $(node).attr('id') 97 | lastChild = $(node).parent().find( 98 | ".panel-container[data-last-child=#{nodeId}]") 99 | 100 | if $(lastChild).length 101 | newDistance = distance.verticalBranch(node, lastChild) 102 | line = CSSRule.get('[id="' + nodeId + '"]::before') 103 | line.style.height = newDistance.toString().concat('px') 104 | 105 | updEachVertical: (node) -> 106 | # find actual parents 107 | nodeClasses = $(node).attr('class').split(' ') 108 | allParents = $(node).prevAll('.panel-parent') 109 | parentIds = $.map(allParents, (el) -> $(el).attr 'id') 110 | actualIds = similarItems(nodeClasses, parentIds) 111 | 112 | # add current node id 113 | actualIds.push $(node).attr('id') 114 | # get array of jquery elements from actual ids 115 | actualCollection = getElements(actualIds) 116 | # update vertical pseudoelement of actual elements 117 | that = this 118 | $.each(actualCollection, (i, element) -> 119 | that.updVertical(element) 120 | ) 121 | 122 | findPanelHeader = (object) -> 123 | object.find('.panel').find('.panel-header') 124 | 125 | clearAllColors = -> 126 | $('.panel-header').css('background-color', '') 127 | $('.panel-header').each -> 128 | if $(this).hasClass('selectable') 129 | $(this).removeClass('selectable') 130 | 131 | similarItems = (arr1, arr2) -> 132 | item for item in arr2 when item in arr1 133 | 134 | getElements = (arrayOfIds) -> 135 | $.map arrayOfIds, (id) -> $("##{id}").get() 136 | 137 | # inherit color from parent panel on load 138 | # if parent has no color, inherit from parent of parent 139 | $('.panel-parent').each -> 140 | id = $(this).attr('id') 141 | parentColor = findPanelHeader($(this)).css('background-color') 142 | 143 | $('.panel-childless').each -> 144 | if $(this).hasClass(id) 145 | findPanelHeader($(this)).css('background-color', parentColor) 146 | 147 | # show-hide content table of single div 148 | $('.show-content').click -> 149 | node = $(this).parents('.panel-container') 150 | nodeId = $(node).attr 'id' 151 | content = $(this).parent().next('.panel_contents') 152 | 153 | oldNodeHeight = $(node).height() 154 | verticalLine = CSSRule.get('[id="' + $(node).attr('id') + '"]::before') 155 | 156 | if content.is(':hidden') 157 | content.show 0, -> pseudoElement.updHorizontal(node) 158 | else 159 | content.hide 0, -> pseudoElement.updHorizontal(node) 160 | pseudoElement.updEachVertical(node) 161 | 162 | # show-hide content table for bunch of nodes 163 | $('.show-childrens').click -> 164 | nodeId = $(this).parents('.panel-container').attr('id') 165 | nodeContent = $(this).parent().next('.panel_contents') 166 | lastChild = $(".panel-container[data-last-child=#{nodeId}]") 167 | lastChildId = $(lastChild).attr('id') 168 | 169 | # NOTE: do not use .toggleClass here 170 | if nodeContent.is(':hidden') then nodeContent.show() else nodeContent.hide() 171 | 172 | $('.panel').each -> 173 | subNode = $(this).parent('.panel-container') 174 | if subNode.hasClass(nodeId) 175 | content = $(this).find('.panel_contents') 176 | 177 | if nodeContent.is(':visible') 178 | content.show 0, -> pseudoElement.updHorizontal(subNode) 179 | else 180 | content.hide 0, -> pseudoElement.updHorizontal(subNode) 181 | subLastChildId = $(subNode).attr 'data-last-child' 182 | pseudoElement.updVertical $("##{subLastChildId}") if subLastChildId? 183 | 184 | pseudoElement.updEachVertical(lastChild) 185 | 186 | # select user 187 | $('.panel-header').on 'click', -> 188 | clearAllColors() 189 | $(this).addClass('selectable') 190 | parentId = $(this).parents('.panel-container').attr('id') 191 | 192 | $('.panel-header').each -> 193 | if ($(this)).parents('.panel-container').hasClass(parentId) 194 | $(this).addClass('selectable') 195 | 196 | # add vertical line to each parent node 197 | $('.panel-parent').each -> 198 | nodeId = $(this).attr('id') 199 | lastChild = $(this).parent().find( 200 | ".panel-container[data-last-child=#{nodeId}]") 201 | 202 | pseudoElement.addVertical(this, lastChild) if $(lastChild).length 203 | 204 | # add horizontal line to each node except root 205 | $('.panel-container').each -> 206 | pseudoElement.addHorizontal(this) unless $(this).hasClass('panel-root') -------------------------------------------------------------------------------- /app/assets/javascripts/activeadmin_ancestry_view/vanilla-base.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 3 | 4 | $(document).ready(function() { 5 | var CSSRule, branchProp, clearAllColors, distance, findPanelHeader, getElements, pseudoElement, similarItems; 6 | CSSRule = { 7 | get: function(ruleName, deleteFlag) { 8 | var cssRule, i, ii, styleSheet; 9 | ruleName = ruleName.toLowerCase(); 10 | if (document.styleSheets) { 11 | i = 0; 12 | while (i < document.styleSheets.length) { 13 | styleSheet = document.styleSheets[i]; 14 | ii = 0; 15 | cssRule = false; 16 | while (true) { 17 | if (styleSheet.cssRules) { 18 | cssRule = styleSheet.cssRules[ii]; 19 | } else { 20 | cssRule = styleSheet.rules[ii]; 21 | } 22 | if (cssRule) { 23 | if (cssRule.selectorText.toLowerCase() === ruleName) { 24 | if (deleteFlag === 'delete') { 25 | if (styleSheet.cssRules) { 26 | styleSheet.deleteRule(ii); 27 | } else { 28 | styleSheet.removeRule(ii); 29 | } 30 | return true; 31 | } else { 32 | return cssRule; 33 | } 34 | } 35 | } 36 | ii++; 37 | if (!cssRule) { 38 | break; 39 | } 40 | } 41 | i++; 42 | } 43 | } 44 | return false; 45 | }, 46 | add: function(ruleName, properies) { 47 | if (document.styleSheets) { 48 | if (!this.get(ruleName)) { 49 | if (document.styleSheets[0].addRule) { 50 | return document.styleSheets[0].addRule(ruleName, properies, 0); 51 | } else { 52 | return document.styleSheets[0].insertRule(ruleName + (" { " + properies + " }"), 0); 53 | } 54 | } 55 | } 56 | } 57 | }; 58 | distance = { 59 | middleHeight: function(node) { 60 | return Math.abs($(node).height() / 2); 61 | }, 62 | betweenTop: function(sourceNode, targetNode) { 63 | return Math.abs($(sourceNode).offset().top - $(targetNode).offset().top); 64 | }, 65 | verticalBranch: function(node, lastChild) { 66 | return this.betweenTop(node, lastChild) + this.middleHeight(lastChild); 67 | } 68 | }; 69 | branchProp = { 70 | baseMargin: 2, 71 | base: "transition: 150ms ease; " + "content: ''; " + "position: absolute; " + "border: 0.1em solid gray; ", 72 | vertical: function(height) { 73 | return ("margin-left: " + this.baseMargin + "em; ") + ("height: " + height + "px; ") + "z-index: -1; " + this.base; 74 | }, 75 | horizontal: function(marginTop, marginLeft) { 76 | return ("margin-top: -" + marginTop + "px; ") + ("margin-left: -" + (marginLeft - this.baseMargin) + "em; ") + ("width: " + (marginLeft - this.baseMargin) + "em; ") + "z-index: -2; " + this.base; 77 | } 78 | }; 79 | pseudoElement = { 80 | addHorizontal: function(node) { 81 | var height, id, marginLeft; 82 | id = $(node).attr('id'); 83 | height = distance.middleHeight(node); 84 | marginLeft = $(node).attr('data-shift-multiplicator'); 85 | return CSSRule.add("[id='" + id + "']::after", branchProp.horizontal(height, marginLeft)); 86 | }, 87 | addVertical: function(node, purposeNode) { 88 | var dist, id; 89 | dist = distance.verticalBranch(node, purposeNode); 90 | id = $(node).attr('id'); 91 | return CSSRule.add("[id='" + id + "']::before", branchProp.vertical(dist)); 92 | }, 93 | updHorizontal: function(node) { 94 | var id, line, newHeight; 95 | if ($(node).hasClass('panel-root')) { 96 | return false; 97 | } else { 98 | id = $(node).attr('id'); 99 | newHeight = -Math.abs(distance.middleHeight(node)); 100 | line = CSSRule.get('[id="' + id + '"]::after'); 101 | return line.style.marginTop = newHeight.toString().concat('px'); 102 | } 103 | }, 104 | updVertical: function(node) { 105 | var lastChild, line, newDistance, nodeId; 106 | nodeId = $(node).attr('id'); 107 | lastChild = $(node).parent().find(".panel-container[data-last-child=" + nodeId + "]"); 108 | if ($(lastChild).length) { 109 | newDistance = distance.verticalBranch(node, lastChild); 110 | line = CSSRule.get('[id="' + nodeId + '"]::before'); 111 | return line.style.height = newDistance.toString().concat('px'); 112 | } 113 | }, 114 | updEachVertical: function(node) { 115 | var actualCollection, actualIds, allParents, nodeClasses, parentIds, that; 116 | nodeClasses = $(node).attr('class').split(' '); 117 | allParents = $(node).prevAll('.panel-parent'); 118 | parentIds = $.map(allParents, function(el) { 119 | return $(el).attr('id'); 120 | }); 121 | actualIds = similarItems(nodeClasses, parentIds); 122 | actualIds.push($(node).attr('id')); 123 | actualCollection = getElements(actualIds); 124 | that = this; 125 | return $.each(actualCollection, function(i, element) { 126 | return that.updVertical(element); 127 | }); 128 | } 129 | }; 130 | findPanelHeader = function(object) { 131 | return object.find('.panel').find('.panel-header'); 132 | }; 133 | clearAllColors = function() { 134 | $('.panel-header').css('background-color', ''); 135 | return $('.panel-header').each(function() { 136 | if ($(this).hasClass('selectable')) { 137 | return $(this).removeClass('selectable'); 138 | } 139 | }); 140 | }; 141 | similarItems = function(arr1, arr2) { 142 | var item, _i, _len, _results; 143 | _results = []; 144 | for (_i = 0, _len = arr2.length; _i < _len; _i++) { 145 | item = arr2[_i]; 146 | if (__indexOf.call(arr1, item) >= 0) { 147 | _results.push(item); 148 | } 149 | } 150 | return _results; 151 | }; 152 | getElements = function(arrayOfIds) { 153 | return $.map(arrayOfIds, function(id) { 154 | return $("#" + id).get(); 155 | }); 156 | }; 157 | $('.panel-parent').each(function() { 158 | var id, parentColor; 159 | id = $(this).attr('id'); 160 | parentColor = findPanelHeader($(this)).css('background-color'); 161 | return $('.panel-childless').each(function() { 162 | if ($(this).hasClass(id)) { 163 | return findPanelHeader($(this)).css('background-color', parentColor); 164 | } 165 | }); 166 | }); 167 | $('.show-content').click(function() { 168 | var content, node, nodeId, oldNodeHeight, verticalLine; 169 | node = $(this).parents('.panel-container'); 170 | nodeId = $(node).attr('id'); 171 | content = $(this).parent().next('.panel_contents'); 172 | oldNodeHeight = $(node).height(); 173 | verticalLine = CSSRule.get('[id="' + $(node).attr('id') + '"]::before'); 174 | if (content.is(':hidden')) { 175 | content.show(0, function() { 176 | return pseudoElement.updHorizontal(node); 177 | }); 178 | } else { 179 | content.hide(0, function() { 180 | return pseudoElement.updHorizontal(node); 181 | }); 182 | } 183 | return pseudoElement.updEachVertical(node); 184 | }); 185 | $('.show-childrens').click(function() { 186 | var lastChild, lastChildId, nodeContent, nodeId; 187 | nodeId = $(this).parents('.panel-container').attr('id'); 188 | nodeContent = $(this).parent().next('.panel_contents'); 189 | lastChild = $(".panel-container[data-last-child=" + nodeId + "]"); 190 | lastChildId = $(lastChild).attr('id'); 191 | if (nodeContent.is(':hidden')) { 192 | nodeContent.show(); 193 | } else { 194 | nodeContent.hide(); 195 | } 196 | $('.panel').each(function() { 197 | var content, subLastChildId, subNode; 198 | subNode = $(this).parent('.panel-container'); 199 | if (subNode.hasClass(nodeId)) { 200 | content = $(this).find('.panel_contents'); 201 | if (nodeContent.is(':visible')) { 202 | content.show(0, function() { 203 | return pseudoElement.updHorizontal(subNode); 204 | }); 205 | } else { 206 | content.hide(0, function() { 207 | return pseudoElement.updHorizontal(subNode); 208 | }); 209 | } 210 | subLastChildId = $(subNode).attr('data-last-child'); 211 | if (subLastChildId != null) { 212 | return pseudoElement.updVertical($("#" + subLastChildId)); 213 | } 214 | } 215 | }); 216 | return pseudoElement.updEachVertical(lastChild); 217 | }); 218 | $('.panel-header').on('click', function() { 219 | var parentId; 220 | clearAllColors(); 221 | $(this).addClass('selectable'); 222 | parentId = $(this).parents('.panel-container').attr('id'); 223 | return $('.panel-header').each(function() { 224 | if (($(this)).parents('.panel-container').hasClass(parentId)) { 225 | return $(this).addClass('selectable'); 226 | } 227 | }); 228 | }); 229 | $('.panel-parent').each(function() { 230 | var lastChild, nodeId; 231 | nodeId = $(this).attr('id'); 232 | lastChild = $(this).parent().find(".panel-container[data-last-child=" + nodeId + "]"); 233 | if ($(lastChild).length) { 234 | return pseudoElement.addVertical(this, lastChild); 235 | } 236 | }); 237 | return $('.panel-container').each(function() { 238 | if (!$(this).hasClass('panel-root')) { 239 | return pseudoElement.addHorizontal(this); 240 | } 241 | }); 242 | }); 243 | 244 | }).call(this); -------------------------------------------------------------------------------- /app/assets/stylesheets/activeadmin_ancestry_view/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimkarodinz/activeadmin_ancestry_view/d0bb57a978546f837be2f73d208e21c16799b629/app/assets/stylesheets/activeadmin_ancestry_view/.keep -------------------------------------------------------------------------------- /app/assets/stylesheets/activeadmin_ancestry_view/base.scss: -------------------------------------------------------------------------------- 1 | $panel-header-color: #5E6469; 2 | $selectable-color: rgba(122, 158, 255, 0.7); 3 | 4 | @mixin true_transition { 5 | transition: 0.1s ease; 6 | } 7 | 8 | div[class*='panel-'] { 9 | @include true_transition 10 | } 11 | 12 | .panel_contents table { 13 | margin-bottom: 0.1em !important; 14 | } 15 | 16 | .selectable { 17 | background-color: $selectable-color !important; 18 | } 19 | 20 | .hided, #collection_selection_toggle_panel { 21 | display: none; 22 | } 23 | 24 | .show-content, .to-structure, .show-childrens { 25 | cursor: pointer; 26 | float: right; 27 | } 28 | 29 | .panel-header span { 30 | color: $panel-header-color; 31 | font-weight: bold; 32 | font-size: 1.1em !important; 33 | } 34 | 35 | .panel-container { 36 | padding-bottom: 0.4em !important; 37 | } 38 | 39 | .panel-margin-bottom { 40 | margin-bottom: 0em !important; 41 | } -------------------------------------------------------------------------------- /app/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimkarodinz/activeadmin_ancestry_view/d0bb57a978546f837be2f73d208e21c16799b629/app/helpers/.keep -------------------------------------------------------------------------------- /app/helpers/active_admin/activeadmin_ancestry_view/nodes_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveAdmin 2 | module ActiveadminAncestryView 3 | module NodesHelper 4 | CLASSES = { 5 | child: 'panel-childless', 6 | parent: 'panel-parent', 7 | root: 'panel-root' 8 | } 9 | 10 | PRETTY_COLORS = { 11 | red: [255, 102, 102], yellow: [255, 178, 102], 12 | green: [102, 255, 102], blue: [102, 178, 255], 13 | violet: [178, 102, 255], gray: [192, 192, 192] 14 | } 15 | 16 | def generate_panel_classes(resource) 17 | attributes = [] 18 | attributes << resource.path_ids if resource.ancestry 19 | attributes << child_class(resource) 20 | attributes << CLASSES[:root] if resource.root? 21 | attributes.flatten.join(' ') 22 | end 23 | 24 | # for 'data-last-child' attribute 25 | def last_child(resource) 26 | "#{resource.parent.id}" if last_child?(resource) 27 | end 28 | 29 | def panel_shift(resource, multiplicator = 4) 30 | unless resource.depth.zero? 31 | "margin-left: #{multiplicator * resource.depth}em;" 32 | end 33 | end 34 | 35 | def random_bckgr_color(resource, no_color = false) 36 | return if no_color 37 | return if childless_child?(resource) 38 | color = random_color.join(', ') 39 | "background-color: rgba(#{color}, 0.7) !important" 40 | end 41 | 42 | private 43 | 44 | def last_child?(resource) 45 | parent_last_child_id = resource&.parent&.children&.last&.id 46 | parent_last_child_id == resource.id 47 | end 48 | 49 | def childless_child?(resource) 50 | resource.is_childless? && resource.is_root? == false 51 | end 52 | 53 | def child_class(resource) 54 | childless_child?(resource) ? CLASSES[:child] : CLASSES[:parent] 55 | end 56 | 57 | def random_color(range = -50...50) 58 | sample_color.map do |light| 59 | light + Random.rand(range) 60 | end 61 | end 62 | 63 | def sample_color 64 | PRETTY_COLORS.values.sample 65 | end 66 | end 67 | end 68 | end -------------------------------------------------------------------------------- /app/models/concerns/activeadmin_ancestry_view/model_methods.rb: -------------------------------------------------------------------------------- 1 | module Concerns 2 | module ActiveadminAncestryView::ModelMethods 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | # Return ActiveRecord::Relation with same order as passed to method. 7 | # For expamle, .ordered_collection([2,1,3]) will return 8 | # relation with ids [2,1,3] against standart [1,2,3] order. 9 | def self.ordered_collection(ids) 10 | order_clause = "CASE id " 11 | ids.each_with_index do |id, index| 12 | order_clause << sanitize_sql_array(["WHEN ? THEN ? ", id, index]) 13 | end 14 | order_clause << sanitize_sql_array(["ELSE ? END", ids.length]) 15 | 16 | where(id: ids).order(order_clause) 17 | end 18 | 19 | # Like #ancestry, but with self.id 20 | def full_ancestry 21 | return id.to_s unless ancestry 22 | path_ids.join('/') 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/views/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimkarodinz/activeadmin_ancestry_view/d0bb57a978546f837be2f73d208e21c16799b629/app/views/.keep -------------------------------------------------------------------------------- /app/views/activeadmin_ancestry_view/_content_table.slim: -------------------------------------------------------------------------------- 1 | table 2 | tbody 3 | tr 4 | - unless table.blank? 5 | - table.each do |header, content| 6 | th 7 | =header.to_s + ': ' 8 | =resource.public_send(content) -------------------------------------------------------------------------------- /app/views/activeadmin_ancestry_view/_main.slim: -------------------------------------------------------------------------------- 1 | div.panel-container class=generate_panel_classes(resource) id=resource.id data-last-child=last_child(resource) data-shift-multiplicator=shift_depth style=panel_shift(resource, shift_depth) 2 | div.panel.panel-margin-bottom 3 | div.panel-header style=random_bckgr_color(resource, no_color) 4 | = render 'activeadmin_ancestry_view/panel_header_links', resource: resource, headers: headers, table_status: table.present?, no_childless_link: no_childless_link 5 | - if table.present? 6 | div.panel_contents class='hided' 7 | = render 'activeadmin_ancestry_view/content_table', resource: resource, table: table -------------------------------------------------------------------------------- /app/views/activeadmin_ancestry_view/_panel_header_links.slim: -------------------------------------------------------------------------------- 1 | / left link 2 | span 3 | - title = headers[:title] ? resource.public_send(headers[:title]) : resource.id 4 | - link_to_resource = link_to(title, resource_path(resource.id)) 5 | 6 | - if no_childless_link 7 | = resource.is_childless? ? title : link_to_resource 8 | - else 9 | = link_to_resource 10 | 11 | - if table_status 12 | / links on right side 13 | a.view_link.member_link.show-content 14 | = headers[:info] || t('activeadmin_ancestry_view.info') 15 | - unless resource.is_childless? 16 | a.view_link.member_link.show-childrens 17 | = headers[:expand] || t('activeadmin_ancestry_view.expand') 18 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/activeadmin_ancestry_view/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /config/locales/activeadmin_ancestry_view.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activeadmin_ancestry_view: 3 | expand: Expand 4 | info: Info 5 | errors: 6 | wrong_action: "Wrong action name. Allowed actions are: %{actions}" -------------------------------------------------------------------------------- /docs/images/resource_tree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimkarodinz/activeadmin_ancestry_view/d0bb57a978546f837be2f73d208e21c16799b629/docs/images/resource_tree.gif -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view.rb: -------------------------------------------------------------------------------- 1 | require "activeadmin_ancestry_view/engine" 2 | require "activeadmin_ancestry_view/version" 3 | require "activeadmin_ancestry_view/exceptions" 4 | require "activeadmin_ancestry_view/ancestry_view" 5 | 6 | require "activeadmin_ancestry_view/finder" 7 | require "activeadmin_ancestry_view/action_generator" 8 | require "activeadmin_ancestry_view/controller_generator" 9 | 10 | require "activeadmin_ancestry_view/controller_builder/controller_builder" 11 | require "activeadmin_ancestry_view/controller_builder/index_controller_builder" 12 | require "activeadmin_ancestry_view/controller_builder/show_controller_builder" 13 | 14 | require "activeadmin_ancestry_view/action_builder/action_builder" 15 | require "activeadmin_ancestry_view/action_builder/index_action_builder" 16 | require "activeadmin_ancestry_view/action_builder/show_action_builder" 17 | 18 | module ActiveadminAncestryView 19 | ALLOWED_ACTIONS = %w(index show) 20 | end 21 | -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/action_builder/action_builder.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class ActionBuilder 3 | DEFAULT_OPTIONS = { 4 | headers: '{ title: :id }', 5 | table: '{}', 6 | no_color: 'false', 7 | no_childless_link: 'false', 8 | shift: '4' 9 | } 10 | 11 | def call(opt = {}, &block) 12 | end 13 | 14 | private 15 | 16 | def render_partial(opt) 17 | %{render partial: '#{template_path}', 18 | locals: { 19 | resource: res, 20 | headers: #{opt[:headers] || DEFAULT_OPTIONS[:headers]}, 21 | table: #{opt[:table] || DEFAULT_OPTIONS[:table] }, 22 | no_color: #{opt[:no_color] || DEFAULT_OPTIONS[:no_color] }, 23 | no_childless_link: #{opt[:no_childless_link] || DEFAULT_OPTIONS[:no_childless_link]}, 24 | shift_depth: #{opt[:shift_depth] || DEFAULT_OPTIONS[:shift] } 25 | } 26 | } 27 | end 28 | 29 | def template_path 30 | 'activeadmin_ancestry_view/main' 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/action_builder/index_action_builder.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class IndexActionBuilder < ActionBuilder 3 | def call(opt = {}, &block) 4 | %{index as: :block do |res| 5 | #{block.call if block_given?} 6 | #{render_partial(opt)} 7 | end} 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/action_builder/show_action_builder.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class ShowActionBuilder < ActionBuilder 3 | def call(opt = {}, &block) 4 | %{show do 5 | #{block.call if block_given?} 6 | sorted_subtree = resource.subtree.sort_by(&:full_ancestry) 7 | sorted_subtree.each do |res| 8 | #{render_partial(opt)} 9 | end 10 | end} 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/action_generator.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class ActionGenerator 3 | def initialize(action_builder, opt = {}, &block) 4 | @action_builder = action_builder 5 | @opt = opt 6 | @block = block if block_given? 7 | end 8 | 9 | def call 10 | action_builder.call(opt, &block) 11 | end 12 | 13 | private 14 | attr_reader :action_builder, :opt, :block 15 | end 16 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/ancestry_view.rb: -------------------------------------------------------------------------------- 1 | module AncestryView 2 | def ancestry_view(action_name, opt = {}, &block) 3 | eval active_admin_action(action_name, opt, &block) 4 | eval active_admin_controller(action_name) 5 | end 6 | 7 | private 8 | 9 | def active_admin_controller(action_name) 10 | builder = ActiveadminAncestryView::Finder.get_controller_builder(action_name) 11 | ActiveadminAncestryView::ControllerGenerator.new(builder).call 12 | end 13 | 14 | def active_admin_action(action_name, opt = {}, &block) 15 | builder = ActiveadminAncestryView::Finder.get_action_builder(action_name) 16 | ActiveadminAncestryView::ActionGenerator.new(builder, opt, &block).call 17 | end 18 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/controller_builder/controller_builder.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class ControllerBuilder 3 | def call 4 | %{controller do 5 | private 6 | #{build_attr_accessor} 7 | #{build_before_action} 8 | #{build_after_action} 9 | #{build_methods} 10 | end} 11 | end 12 | 13 | private 14 | 15 | def build_attr_accessor 16 | end 17 | 18 | def build_before_action 19 | end 20 | 21 | def build_after_action 22 | end 23 | 24 | def build_methods 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/controller_builder/index_controller_builder.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class IndexControllerBuilder < ControllerBuilder 3 | private 4 | 5 | ORDER_VAR_NAME = 'i_am_storing_sort_order' 6 | OLD_SCOPED_COL_VAR_NAME = 'i_am_storing_old_scoped_collection' 7 | 8 | def build_attr_accessor 9 | %{attr_accessor :#{ORDER_VAR_NAME}} 10 | end 11 | 12 | def build_before_action 13 | %{send(:before_action, only: :index) do 14 | #{save_and_clean_sort_order} 15 | #{save_old_scoped_collection} 16 | #{sort_scoped_collection} 17 | end} 18 | end 19 | 20 | def build_after_action 21 | %{send(:after_action, only: :index) do 22 | #{restore_sort_order} 23 | #{restore_old_scoped_collection} 24 | end} 25 | end 26 | 27 | def save_old_scoped_collection 28 | "self.class.send :alias_method, :#{OLD_SCOPED_COL_VAR_NAME}, :scoped_collection\n" 29 | end 30 | 31 | def sort_scoped_collection 32 | %{self.class.redefine_method(:scoped_collection) do 33 | ids = #{OLD_SCOPED_COL_VAR_NAME}.all.sort_by(&:full_ancestry).map(&:id) 34 | resource_class.ordered_collection(ids) 35 | end} 36 | end 37 | 38 | def restore_old_scoped_collection 39 | "self.class.send :alias_method, :scoped_collection, :#{OLD_SCOPED_COL_VAR_NAME}\n" 40 | end 41 | 42 | def save_and_clean_sort_order 43 | %{#{ORDER_VAR_NAME} = active_admin_config.sort_order 44 | active_admin_config.sort_order = ''} 45 | end 46 | 47 | def restore_sort_order 48 | %{if #{ORDER_VAR_NAME} 49 | active_admin_config.sort_order = #{ORDER_VAR_NAME} 50 | #{ORDER_VAR_NAME} = nil 51 | end} 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/controller_builder/show_controller_builder.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class ShowControllerBuilder < ControllerBuilder 3 | # Nothing to do 4 | def call 5 | '' 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/controller_generator.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class ControllerGenerator 3 | def initialize(controller_builder) 4 | @controller_builder = controller_builder 5 | end 6 | 7 | def call 8 | @controller_builder.call 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/engine.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class Engine < ::Rails::Engine 3 | config.to_prepare do 4 | ActiveAdmin::ResourceDSL.send :include, AncestryView 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/exceptions.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class ActionError < ArgumentError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/finder.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class Finder 3 | class << self 4 | def get_action_builder(name) 5 | if_action_valid(name) do 6 | ActiveadminAncestryView.const_get("#{name.to_s.camelize}ActionBuilder").new 7 | end 8 | end 9 | 10 | def get_controller_builder(name) 11 | if_action_valid(name) do 12 | ActiveadminAncestryView.const_get("#{name.to_s.camelize}ControllerBuilder").new 13 | end 14 | end 15 | 16 | private 17 | 18 | def if_action_valid(action_name) 19 | if ALLOWED_ACTIONS.include? action_name.to_s 20 | yield 21 | else 22 | raise ActionError.new( 23 | I18n.t 'activeadmin_ancestry_view.errors.wrong_action', 24 | actions: ALLOWED_ACTIONS.join(', ') 25 | ) 26 | end 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /lib/activeadmin_ancestry_view/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | VERSION = "0.0.5" 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/activeadmin_ancestry_view/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module ActiveadminAncestryView 2 | class Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | def add_javascripts 5 | target_file_path = 'app/assets/javascripts/active_admin' 6 | ref = "#= require active_admin/base\n" 7 | vanilla_ref = "//= require active_admin/base\n" 8 | begin 9 | inject_into_file("#{target_file_path}.coffee", js_to_add, after: ref) 10 | rescue 11 | begin 12 | inject_into_file("#{target_file_path}.js.coffee", js_to_add, after: ref) 13 | rescue 14 | inject_into_file("#{target_file_path}.js", vanilla_js_to_add, after: vanilla_ref) 15 | end 16 | end 17 | end 18 | 19 | def add_stylesheets 20 | target_file_path = 'app/assets/stylesheets/active_admin' 21 | begin 22 | prepend_file("#{target_file_path}.scss", css_to_add) 23 | rescue Errno::ENOENT 24 | prepend_file("#{target_file_path}.css.scss", css_to_add) 25 | end 26 | end 27 | 28 | def add_concerns 29 | ref = 'has_ancestry' 30 | Dir['app/models/**/*.rb'].each do |model_file| 31 | if File.readlines(model_file).grep(/has_ancestry/).any? 32 | inject_into_file(model_file, concern_to_add, before: ref) 33 | end 34 | end 35 | end 36 | 37 | private 38 | 39 | def concern_to_add 40 | "include Concerns::ActiveadminAncestryView::ModelMethods\n\t" 41 | end 42 | 43 | def js_to_add 44 | "#= require activeadmin_ancestry_view/base\n" 45 | end 46 | 47 | def vanilla_js_to_add 48 | "//= require activeadmin_ancestry_view/vanilla-base\n" 49 | end 50 | 51 | def css_to_add 52 | "@import \"activeadmin_ancestry_view/base\";\n" 53 | end 54 | end 55 | end 56 | end --------------------------------------------------------------------------------