├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .rubocop.yml
├── Gemfile
├── MIT-LICENSE
├── README.rdoc
├── Rakefile
├── VERSION
├── app
├── assets
│ ├── images
│ │ └── actions
│ │ │ ├── add.png
│ │ │ ├── delete.png
│ │ │ ├── edit.png
│ │ │ ├── list.png
│ │ │ └── show.png
│ └── stylesheets
│ │ ├── crud.scss
│ │ └── sample.scss
├── controllers
│ ├── crud_controller.rb
│ ├── dry_crud
│ │ ├── generic_model.rb
│ │ ├── nestable.rb
│ │ ├── rememberable.rb
│ │ ├── render_callbacks.rb
│ │ ├── searchable.rb
│ │ └── sortable.rb
│ └── list_controller.rb
├── helpers
│ ├── actions_helper.rb
│ ├── dry_crud
│ │ ├── form
│ │ │ ├── builder.rb
│ │ │ └── control.rb
│ │ └── table
│ │ │ ├── actions.rb
│ │ │ ├── builder.rb
│ │ │ ├── col.rb
│ │ │ └── sorting.rb
│ ├── form_helper.rb
│ ├── format_helper.rb
│ ├── i18n_helper.rb
│ ├── table_helper.rb
│ └── utility_helper.rb
└── views
│ ├── crud
│ ├── _actions_edit.html.erb
│ ├── _actions_edit.html.haml
│ ├── _actions_index.html.erb
│ ├── _actions_index.html.haml
│ ├── _actions_show.html.erb
│ ├── _actions_show.html.haml
│ ├── _attrs.html.erb
│ ├── _attrs.html.haml
│ ├── _form.html.erb
│ ├── _form.html.haml
│ ├── _list.html.erb
│ ├── _list.html.haml
│ ├── edit.html.erb
│ ├── edit.html.haml
│ ├── new.html.erb
│ ├── new.html.haml
│ ├── show.html.erb
│ ├── show.html.haml
│ └── show.json.jbuilder
│ ├── layouts
│ ├── _flash.html.erb
│ ├── _flash.html.haml
│ ├── _nav.html.erb
│ ├── _nav.html.haml
│ ├── application.html.erb
│ └── application.html.haml
│ ├── list
│ ├── _actions_index.html.erb
│ ├── _actions_index.html.haml
│ ├── _list.html.erb
│ ├── _list.html.haml
│ ├── _search.html.erb
│ ├── _search.html.haml
│ ├── index.html.erb
│ ├── index.html.haml
│ └── index.json.jbuilder
│ └── shared
│ ├── _error_messages.html.erb
│ ├── _error_messages.html.haml
│ ├── _labeled.html.erb
│ └── _labeled.html.haml
├── config
└── locales
│ ├── crud.de.yml
│ ├── crud.en.yml
│ └── crud.it.yml
├── dry_crud.gemspec
├── lib
├── dry_crud.rb
├── dry_crud
│ └── engine.rb
└── generators
│ └── dry_crud
│ ├── USAGE
│ ├── dry_crud_generator.rb
│ ├── dry_crud_generator_base.rb
│ ├── file_generator.rb
│ └── templates
│ ├── INSTALL
│ ├── config
│ └── initializers
│ │ └── field_error_proc.rb
│ ├── spec
│ ├── controllers
│ │ └── crud_test_models_controller_spec.rb
│ ├── helpers
│ │ ├── dry_crud
│ │ │ ├── form
│ │ │ │ └── builder_spec.rb
│ │ │ └── table
│ │ │ │ └── builder_spec.rb
│ │ ├── form_helper_spec.rb
│ │ ├── format_helper_spec.rb
│ │ ├── i18n_helper_spec.rb
│ │ ├── table_helper_spec.rb
│ │ └── utility_helper_spec.rb
│ └── support
│ │ ├── crud_controller_examples.rb
│ │ └── crud_controller_test_helper.rb
│ └── test
│ ├── controllers
│ └── crud_test_models_controller_test.rb
│ ├── helpers
│ ├── custom_assertions_test.rb
│ ├── dry_crud
│ │ ├── form
│ │ │ └── builder_test.rb
│ │ └── table
│ │ │ └── builder_test.rb
│ ├── form_helper_test.rb
│ ├── format_helper_test.rb
│ ├── i18n_helper_test.rb
│ ├── table_helper_test.rb
│ └── utility_helper_test.rb
│ └── support
│ ├── crud_controller_test_helper.rb
│ ├── crud_test_helper.rb
│ ├── crud_test_model.rb
│ ├── crud_test_models_controller.rb
│ └── custom_assertions.rb
├── template.rb
└── test
└── templates
├── Gemfile.append
├── app
├── controllers
│ ├── admin
│ │ ├── cities_controller.rb
│ │ └── countries_controller.rb
│ ├── people_controller.rb
│ ├── turbo_controller.rb
│ └── vips_controller.rb
├── helpers
│ ├── cities_helper.rb
│ └── people_helper.rb
├── models
│ ├── city.rb
│ ├── country.rb
│ └── person.rb
└── views
│ ├── admin
│ ├── cities
│ │ ├── _actions_index.html.erb
│ │ ├── _actions_index.html.haml
│ │ ├── _attrs.html.erb
│ │ ├── _attrs.html.haml
│ │ ├── _form.html.erb
│ │ ├── _form.html.haml
│ │ ├── _hello.html.erb
│ │ ├── _hello.html.haml
│ │ ├── _list.html.erb
│ │ └── _list.html.haml
│ └── countries
│ │ ├── _form.html.erb
│ │ ├── _form.html.haml
│ │ ├── _list.html.erb
│ │ └── _list.html.haml
│ ├── layouts
│ ├── _nav.html.erb
│ └── _nav.html.haml
│ ├── people
│ ├── _attrs.html.erb
│ ├── _attrs.html.haml
│ ├── _list.html.erb
│ └── _list.html.haml
│ └── turbo
│ ├── _actions_index.html.erb
│ ├── _actions_index.html.haml
│ ├── _actions_show.html.erb
│ ├── _actions_show.html.haml
│ ├── _hello.html.erb
│ ├── _hello.html.haml
│ ├── edit.turbo_stream.erb
│ ├── edit.turbo_stream.haml
│ ├── show.turbo_stream.erb
│ ├── show.turbo_stream.haml
│ ├── turbo.turbo_stream.erb
│ ├── turbo.turbo_stream.haml
│ ├── update.turbo_stream.erb
│ └── update.turbo_stream.haml
├── config
├── database.yml
├── initializers
│ └── deprecations.rb
├── locales
│ ├── cities.en.yml
│ └── de.yml
└── routes.rb
├── db
├── migrate
│ └── 20100511174904_create_people_and_cities.rb
└── seeds.rb
├── spec
├── controllers
│ ├── admin
│ │ ├── cities_controller_spec.rb
│ │ └── countries_controller_spec.rb
│ └── people_controller_spec.rb
└── routing
│ ├── cities_routing_spec.rb
│ └── countries_routing_spec.rb
└── test
├── controllers
├── admin
│ ├── cities_controller_test.rb
│ └── countries_controller_test.rb
└── people_controller_test.rb
└── fixtures
├── cities.yml
├── countries.yml
└── people.yml
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Build
3 |
4 | on:
5 | push:
6 | pull_request:
7 | branches: [ $default-branch ]
8 |
9 | jobs:
10 | test:
11 | name: Test
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | ruby-version: ['3.3', '3.4']
16 | haml: [true, false]
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Set up Ruby
20 | uses: ruby/setup-ruby@v1
21 | with:
22 | ruby-version: ${{ matrix.ruby-version }}
23 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
24 | - name: Run tests
25 | run: bundle exec rake
26 | env:
27 | HAML: ${{ matrix.haml }}
28 |
29 | lint:
30 | name: Lint
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v4
34 | - name: Set up Ruby
35 | uses: ruby/setup-ruby@v1
36 | with:
37 | ruby-version: 3.3
38 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
39 | - name: Run rubocop
40 | run: bundle exec rubocop
41 |
42 | coverage:
43 | name: Coverage
44 | runs-on: ubuntu-latest
45 | steps:
46 | - uses: actions/checkout@v4
47 | - name: Set up Ruby
48 | uses: ruby/setup-ruby@v1
49 | with:
50 | ruby-version: 3.3
51 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
52 | - uses: paambaati/codeclimate-action@v5.0.0
53 | env:
54 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
55 | with:
56 | coverageCommand: bundle exec rake
57 | coverageLocations: |
58 | ${{github.workspace}}/coverage/spec/coverage.json:simplecov
59 | ${{github.workspace}}/coverage/test/coverage.json:simplecov
60 | debug: true
61 |
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .project
3 | .loadpath
4 | Gemfile.lock
5 | .tmp*
6 | coverage
7 | test/test_app
8 | tmp
9 | pkg
10 | rdoc
11 | *.gemfile.lock
12 | .byebug_history
13 | # RVM files
14 | .ruby-gemset
15 | .ruby-version
16 | .vscode
17 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem: { rubocop-rails-omakase: rubocop.yml }
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "rails", "~> 8.0.0"
4 |
5 | gem "puma"
6 |
7 | gem "rake"
8 |
9 | gem "rspec-rails"
10 |
11 | gem "haml"
12 | gem "jbuilder"
13 |
14 | gem "kaminari"
15 |
16 | gem "propshaft"
17 | gem "jsbundling-rails"
18 | gem "cssbundling-rails"
19 | gem "turbo-rails"
20 | gem "stimulus-rails"
21 |
22 | gem "bootsnap", require: false
23 |
24 | gem "tzinfo-data", platforms: [ :windows, :jruby ]
25 |
26 | group :development do
27 | gem "web-console"
28 | gem "rubocop"
29 | gem "rubocop-rails-omakase"
30 | gem "sdoc"
31 | gem "spring"
32 | end
33 |
34 | gem "simplecov", require: false
35 | gem "debug", platforms: [ :mri, :windows ], require: "debug/prelude"
36 |
37 | # platform specific gems
38 |
39 | platforms :ruby do
40 | gem "sqlite3"
41 | end
42 |
43 | platforms :jruby do
44 | gem "jdbc-sqlite3"
45 | gem "activerecord-jdbcsqlite3-adapter"
46 | gem "jruby-openssl"
47 | end
48 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2023 Pascal Zumkehr
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 8.0.0
2 |
--------------------------------------------------------------------------------
/app/assets/images/actions/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/add.png
--------------------------------------------------------------------------------
/app/assets/images/actions/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/delete.png
--------------------------------------------------------------------------------
/app/assets/images/actions/edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/edit.png
--------------------------------------------------------------------------------
/app/assets/images/actions/list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/list.png
--------------------------------------------------------------------------------
/app/assets/images/actions/show.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codez/dry_crud/b986c08ae8ac6e446b65ca24e2e48bc1e47c253d/app/assets/images/actions/show.png
--------------------------------------------------------------------------------
/app/assets/stylesheets/crud.scss:
--------------------------------------------------------------------------------
1 |
2 | h1 {
3 | margin-bottom: 20px;
4 | }
5 |
6 | .right {
7 | text-align: right;
8 | }
9 |
10 | .center {
11 | text-align: center;
12 | }
13 |
14 | #content {
15 | padding-top: 10px;
16 | }
17 |
18 | #flash {
19 | clear: both;
20 | padding-top: 5px;
21 | }
22 |
23 | table.table td.action {
24 | width: 20px;
25 | text-align: center;
26 | }
27 |
28 | .cancel {
29 | font-size: 80%;
30 | margin-left: 7px;
31 | }
32 |
33 | #error_explanation h2 {
34 | font-size: 100%;
35 | margin-top: 0px;
36 | }
37 |
38 | #error_explanation ul {
39 | margin-bottom: 5px;
40 | }
41 |
42 | footer {
43 | clear: both;
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sample.scss:
--------------------------------------------------------------------------------
1 | $container_width: 1000px;
2 | $theme_color: #2580a2;
3 |
4 | body,
5 | div,
6 | p,
7 | td,
8 | th {
9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
10 | font-size: 14px;
11 | line-height: 1.42857;
12 | }
13 |
14 | body {
15 | text-align: center;
16 | margin: 0;
17 | background-color: #ddf;
18 | }
19 |
20 | .container {
21 | padding: 20px 20px;
22 | margin: 0 auto;
23 | text-align: left;
24 | max-width: $container_width;
25 | min-width: $container_width - 200px;
26 | height: 100%;
27 | background-color: #f6f6ff;
28 | -moz-box-shadow: 0px 0px 5px $theme_color;
29 | -webkit-box-shadow: 0px 0px 5px $theme_color;
30 | box-shadow: 0px 0px 5px $theme_color;
31 | }
32 |
33 | .navbar {
34 | background: #333;
35 | margin: 0 auto;
36 | padding: 0 20px;
37 | max-width: $container_width;
38 | min-width: $container_width - 200px;
39 | height: 33px;
40 | -moz-box-shadow: 0px 0px 5px $theme_color;
41 | -webkit-box-shadow: 0px 0px 5px $theme_color;
42 | box-shadow: 0px 1px 5px $theme_color;
43 | }
44 |
45 | .navbar-brand {
46 | font-weight: bold;
47 | font-size: 130%;
48 | color: #ddd !important;
49 | padding: 5px;
50 | float: left;
51 | }
52 |
53 | a.navbar-brand:hover {
54 | text-decoration: none;
55 | color: #ddd;
56 | }
57 |
58 | .navbar-nav {
59 | list-style: none;
60 | margin: 0;
61 | float: left;
62 | }
63 |
64 | .navbar-nav li.nav-item {
65 | float: left;
66 | font-size: 110%;
67 | margin: 0;
68 | padding: 0;
69 | }
70 |
71 | .navbar-nav a.nav-link {
72 | color: #ddd !important;
73 | display: block;
74 | float: left;
75 | margin: 0;
76 | padding: 7px 12px 7px;
77 | text-decoration: none;
78 | height: 19px;
79 | }
80 |
81 | .navbar-nav a.nav-link:hover {
82 | background: $theme_color bottom center no-repeat;
83 | color: #fff !important;
84 | text-decoration: none;
85 | }
86 |
87 | .actions {
88 | height: 25px;
89 | }
90 |
91 | #content {
92 | clear: both;
93 | width: 100%;
94 | }
95 |
96 | h1 {
97 | font-size: 150%;
98 | margin: 0px 0 20px 0;
99 | }
100 |
101 | table.table {
102 | border-collapse: collapse;
103 | width: 100%;
104 | padding: 0;
105 | }
106 |
107 | /* div rendered if no entries available for list */
108 | div.table {
109 | }
110 |
111 | table.table th {
112 | background-color: $theme_color;
113 | color: white;
114 | font-weight: bold;
115 | padding: 4px 4px;
116 | }
117 |
118 | table.table th a {
119 | color: white;
120 | text-decoration: none;
121 | }
122 |
123 | table.table td {
124 | padding: 4px 4px;
125 | }
126 |
127 | .table-striped thead tr:nth-child(odd),
128 | .table thead tr:nth-child(even) {
129 | background-color: #d0d0d0;
130 | }
131 |
132 | .table-striped tr:nth-child(odd) {
133 | background-color: #f8f8f8;
134 | }
135 |
136 | .table-striped tr:nth-child(even) {
137 | background-color: #f0f0f0;
138 | }
139 |
140 | .table-hover tr:hover {
141 | background-color: #ffffe0;
142 | }
143 |
144 | td {
145 | vertical-align: top;
146 | }
147 |
148 | td p {
149 | margin: 0;
150 | }
151 |
152 | a {
153 | color: #2580a2;
154 | text-decoration: none;
155 | }
156 |
157 | a:hover {
158 | text-decoration: underline;
159 | }
160 |
161 | a:visited {
162 | color: #2580a2;
163 | }
164 |
165 | a img {
166 | border: none;
167 | }
168 |
169 | dl {
170 | vertical-align: top;
171 | margin: 0;
172 | clear: both;
173 | }
174 |
175 | dt {
176 | width: 120px;
177 | padding-right: 5px;
178 | margin-bottom: 5px;
179 | float: left;
180 | font-style: italic;
181 | }
182 |
183 | dd {
184 | margin-left: 130px;
185 | margin-bottom: 5px;
186 | }
187 |
188 | dd p {
189 | margin: 0;
190 | }
191 |
192 | label {
193 | font-style: italic;
194 | text-align: right;
195 | }
196 |
197 | .form-actions {
198 | margin-left: 130px;
199 | }
200 |
201 | a.action {
202 | padding: 0 5px;
203 | }
204 |
205 | a.icon {
206 | margin-right: 5px;
207 | margin-left: 5px;
208 | }
209 |
210 | a.icon img {
211 | vertical-align: text-top;
212 | }
213 |
214 | .icon {
215 | width: 16px;
216 | height: 16px;
217 | display: inline-block;
218 | background: no-repeat;
219 | vertical-align: top;
220 | }
221 |
222 | .icon-plus {
223 | background-image: image-url("actions/add.png");
224 | }
225 | .icon-trash {
226 | background-image: image-url("actions/delete.png");
227 | }
228 | .icon-pencil {
229 | background-image: image-url("actions/edit.png");
230 | }
231 | .icon-list {
232 | background-image: image-url("actions/list.png");
233 | }
234 | .icon-zoom-in {
235 | background-image: image-url("actions/show.png");
236 | }
237 |
238 | .form-group {
239 | clear: both;
240 | padding: 4px 0 4px;
241 | }
242 |
243 | .form-group > label {
244 | float: left;
245 | width: 120px;
246 | padding-right: 10px;
247 | padding-top: 3px;
248 | }
249 |
250 | input,
251 | textarea,
252 | select {
253 | font-family: Verdana, Geneva, Helvetica, Arial, sans-serif;
254 | font-size: 14px;
255 | }
256 |
257 | input[type="text"],
258 | input[type="password"],
259 | input[type="email"] {
260 | width: 300px;
261 | }
262 |
263 | input[type="number"] {
264 | width: 100px;
265 | }
266 |
267 | textarea,
268 | select[multiple] {
269 | width: 300px;
270 | height: 80px;
271 | }
272 |
273 | [role="search"] [type="search"] {
274 | width: 220px;
275 | }
276 |
277 | .has-error .control-label {
278 | color: #d88;
279 | }
280 |
281 | .has-error .form-control {
282 | border-color: #d88;
283 | }
284 |
285 | .input-group-append {
286 | font-size: 80%;
287 | vertical-align: top;
288 | margin-left: 4px;
289 | display: inline-block;
290 | }
291 |
292 | .help-block {
293 | margin-top: 0;
294 | }
295 |
296 | .alert {
297 | margin: 15px;
298 | padding: 5px 10px;
299 | clear: both;
300 | }
301 |
302 | .alert-success {
303 | border: solid 2px #6a6;
304 | background-color: #afa;
305 | }
306 |
307 | .alert-danger {
308 | border: solid 2px #d88;
309 | background-color: #fec;
310 | }
311 |
312 | .close {
313 | float: right;
314 | }
315 |
316 | .float-end {
317 | float: right;
318 | }
319 |
320 | .float-start {
321 | float: left;
322 | }
323 |
324 | footer {
325 | margin: auto;
326 | text-align: right;
327 | max-width: $container_width;
328 | min-width: $container_width - 200px;
329 | }
330 |
331 | .col-md-offset-2 {
332 | margin-left: 130px;
333 | }
334 |
335 | .col-md-8 {
336 | margin-left: 130px;
337 | }
338 |
339 | .col-md-5 {
340 | float: left;
341 | width: 350px;
342 | }
343 |
--------------------------------------------------------------------------------
/app/controllers/dry_crud/generic_model.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | # Connects the including controller to the model whose name corrsponds to
3 | # the controller's name.
4 | #
5 | # The two main methods are +model_class+ and +model_scope+.
6 | # Additional helper methods store and retrieve values in instance variables
7 | # named after their class.
8 | module GenericModel
9 | extend ActiveSupport::Concern
10 |
11 | included do
12 | helper_method :model_class, :models_label, :path_args
13 |
14 | delegate :model_class, :models_label, :model_identifier, to: "self.class"
15 | end
16 |
17 | # The scope where model entries will be listed and created.
18 | # This is mainly used for nested models to provide the
19 | # required context.
20 | def model_scope
21 | model_class.all
22 | end
23 |
24 | # The path arguments to link to the given model entry.
25 | # If the controller is nested, this provides the required context.
26 | def path_args(last)
27 | last
28 | end
29 |
30 | # Get the instance variable named after the +model_class+.
31 | # If the collection variable is required, pass true as the second argument.
32 | def model_ivar_get(plural: false)
33 | name = ivar_name(model_class)
34 | name = name.pluralize if plural
35 | name = :"@#{name}"
36 | instance_variable_get(name) if instance_variable_defined?(name)
37 | end
38 |
39 | # Sets an instance variable with the underscored class name if the given
40 | # value. If the value is a collection, sets the plural name.
41 | def model_ivar_set(value)
42 | name = if value.respond_to?(:klass) # ActiveRecord::Relation
43 | ivar_name(value.klass).pluralize
44 | elsif value.respond_to?(:each) # Array
45 | ivar_name(value.first.class).pluralize
46 | else
47 | ivar_name(value.class)
48 | end
49 | instance_variable_set(:"@#{name}", value)
50 | end
51 |
52 | def ivar_name(klass)
53 | klass.model_name.param_key
54 | end
55 |
56 | # Class methods from GenericModel.
57 | module ClassMethods
58 | # The ActiveRecord class of the model.
59 | def model_class
60 | @model_class ||= controller_name.classify.constantize
61 | end
62 |
63 | # The identifier of the model used for form parameters.
64 | # I.e., the symbol of the underscored model name.
65 | def model_identifier
66 | @model_identifier ||= model_class.model_name.param_key
67 | end
68 |
69 | # A human readable plural name of the model.
70 | def models_label(plural: true)
71 | opts = { count: (plural ? 3 : 1) }
72 | opts[:default] = model_class.model_name.human.titleize
73 | opts[:default] = opts[:default].pluralize if plural
74 |
75 | model_class.model_name.human(opts)
76 | end
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/app/controllers/dry_crud/nestable.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | # Provides functionality to nest controllers/resources.
3 | # If a controller is nested, the parent classes and namespaces
4 | # may be defined as an array in the +nesting+ class attribute.
5 | #
6 | # For example, a cities controller, nested in country and a admin
7 | # namespace, may define this attribute as follows:
8 | # self.nesting = :admin, Country
9 | module Nestable
10 | # Adds the :nesting class attribute and parent helper methods
11 | # to the including controller.
12 | def self.prepended(klass)
13 | klass.class_attribute :nesting
14 |
15 | klass.helper_method :parent, :parents
16 | end
17 |
18 | private
19 |
20 | # Returns the direct parent ActiveRecord of the current request, if any.
21 | def parent
22 | parents.reverse.find { |p| p.is_a?(ActiveRecord::Base) }
23 | end
24 |
25 | # Returns the parent entries of the current request, if any.
26 | # These are ActiveRecords or namespace symbols, corresponding
27 | # to the defined nesting attribute.
28 | def parents
29 | @parents ||= Array(nesting).map do |p|
30 | if p.is_a?(Class) && p < ActiveRecord::Base
31 | parent_entry(p)
32 | else
33 | p
34 | end
35 | end
36 | end
37 |
38 | # Loads the parent entry for the given ActiveRecord class.
39 | # By default, performs a find with the class_name_id param.
40 | def parent_entry(clazz)
41 | model_ivar_set(clazz.find(params["#{clazz.name.underscore}_id"]))
42 | end
43 |
44 | # An array of objects used in url_for and related functions.
45 | def path_args(last)
46 | parents + [ last ]
47 | end
48 |
49 | # Uses the parent entry (if any) to constrain the model scope.
50 | def model_scope
51 | if parent.present?
52 | parent_scope
53 | else
54 | super
55 | end
56 | end
57 |
58 | # The model scope for the current parent resource.
59 | def parent_scope
60 | parent.send(model_class.name.underscore.pluralize)
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/app/controllers/dry_crud/rememberable.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | # Remembers certain params of the index action in order to return
3 | # to the same list after an entry was viewed or edited.
4 | # If the index is called with a param :returning, the remembered params
5 | # will be re-used to present the user the same list as she left it.
6 | #
7 | # Define a list of param keys that should be remembered for the list action
8 | # with the class attribute +remember_params+.
9 | #
10 | # The params are stored separately for each different +remember_key+, which
11 | # defaults to the current request's path.
12 | module Rememberable
13 | extend ActiveSupport::Concern
14 |
15 | included do
16 | class_attribute :remember_params
17 | self.remember_params = %w[q sort sort_dir page]
18 |
19 | before_action :handle_remember_params, only: [ :index ]
20 | end
21 |
22 | private
23 |
24 | # Store and restore the corresponding params.
25 | def handle_remember_params
26 | remembered = remembered_params
27 |
28 | restore_params_on_return(remembered)
29 | store_current_params(remembered)
30 | clear_void_params(remembered)
31 | end
32 |
33 | def restore_params_on_return(remembered)
34 | if params[:returning]
35 | remember_params.each { |p| params[p] ||= remembered[p] }
36 | end
37 | end
38 |
39 | def store_current_params(remembered)
40 | remember_params.each do |p|
41 | remembered[p] = params[p].presence
42 | remembered.delete(p) if remembered[p].nil?
43 | end
44 | end
45 |
46 | def clear_void_params(remembered)
47 | session[:list_params].delete(remember_key) if remembered.blank?
48 | end
49 |
50 | # Get the params stored in the session.
51 | def remembered_params
52 | session[:list_params] ||= {}
53 | session[:list_params][remember_key] ||= {}
54 | session[:list_params][remember_key]
55 | end
56 |
57 | # Params are stored by request path to play nice when a controller
58 | # is used in different routes.
59 | def remember_key
60 | request.path
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/app/controllers/dry_crud/render_callbacks.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | # Provide +before_render+ callbacks.
3 | module RenderCallbacks
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | extend ActiveModel::Callbacks
8 | prepend Prepends
9 | end
10 |
11 | # Prepended methods for callbacks.
12 | module Prepends
13 | # Helper method to run +before_render+ callbacks and render the action.
14 | # If a callback renders or redirects, the action is not rendered.
15 | def render(...)
16 | options = _normalize_render(...)
17 | callback = "render_#{options[:template] || options[:action] || action_name}"
18 | run_callbacks(callback) if respond_to?(:"_#{callback}_callbacks", true)
19 |
20 | super unless performed?
21 | end
22 |
23 | private
24 |
25 | # Helper method the run the given block in between the before and after
26 | # callbacks of the given kinds.
27 | def with_callbacks(*kinds, &block)
28 | kinds.reverse.reduce(block) do |a, e|
29 | -> { run_callbacks(e, &a) }
30 | end.call
31 | end
32 | end
33 |
34 | # Class methods for callbacks.
35 | module ClassMethods
36 | # Defines before callbacks for the render actions.
37 | def define_render_callbacks(*actions)
38 | args = actions.map { |a| :"render_#{a}" }
39 | args << { only: :before, terminator: render_callback_terminator }
40 | define_model_callbacks(*args)
41 | end
42 |
43 | private
44 |
45 | def render_callback_terminator
46 | proc do |ctrl, result_lambda|
47 | terminate = true
48 | catch(:abort) do
49 | result_lambda.call if result_lambda.is_a?(Proc)
50 | terminate = !ctrl.response_body.nil?
51 | end
52 | terminate
53 | end
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/app/controllers/dry_crud/searchable.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | # The search functionality for the index table.
3 | # Define an array of searchable string columns in your subclassing
4 | # controllers using the class attribute +search_columns+.
5 | module Searchable
6 | extend ActiveSupport::Concern
7 |
8 | included do
9 | class_attribute :search_columns
10 | self.search_columns = []
11 |
12 | helper_method :search_support?
13 |
14 | prepend Prepends
15 | end
16 |
17 | # Prepended methods for searching.
18 | module Prepends
19 | private
20 |
21 | # Enhance the list entries with an optional search criteria
22 | def list_entries
23 | super.where(search_conditions)
24 | end
25 |
26 | # Concat the word clauses with AND.
27 | def search_conditions
28 | if search_support? && params[:q].present?
29 | search_word_conditions.reduce do |query, condition|
30 | query.and(condition)
31 | end
32 | end
33 | end
34 |
35 | # Split the search query in single words and create a list of word
36 | # clauses.
37 | def search_word_conditions
38 | params[:q].split(/\s+/).map { |w| search_word_condition(w) }
39 | end
40 |
41 | # Concat the column queries of the given word with OR.
42 | def search_word_condition(word)
43 | search_column_condition(word).reduce do |query, condition|
44 | query.or(condition)
45 | end
46 | end
47 |
48 | # Create a list of Arel #matches queries for each column and the given
49 | # word.
50 | def search_column_condition(word)
51 | self.class.search_tables_and_fields.map do |table_name, field|
52 | table = Arel::Table.new(table_name)
53 | table[field].matches(Arel::Nodes::Quoted.new("%#{word}%"))
54 | end
55 | end
56 |
57 | # Returns true if this controller has searchable columns.
58 | def search_support?
59 | search_columns.present?
60 | end
61 | end
62 |
63 | # Class methods for Searchable.
64 | module ClassMethods
65 | # All search columns divided in table and field names.
66 | def search_tables_and_fields
67 | @search_tables_and_fields ||= search_columns.map do |f|
68 | if f.to_s.include?(".")
69 | f.split(".", 2)
70 | else
71 | [ model_class.table_name, f ]
72 | end
73 | end
74 | end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/app/controllers/dry_crud/sortable.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | # Sort functionality for the index table.
3 | # Define a default sort expression that is always appended to the
4 | # current sort params with the class attribute +default_sort+.
5 | module Sortable
6 | extend ActiveSupport::Concern
7 |
8 | included do
9 | class_attribute :sort_mappings_with_indifferent_access
10 | self.sort_mappings = {}
11 |
12 | class_attribute :default_sort
13 |
14 | helper_method :sortable?
15 |
16 | prepend Prepends
17 | end
18 |
19 | # Class methods for sorting.
20 | module ClassMethods
21 | # Define a map of (virtual) attributes to SQL order expressions.
22 | # May be used for sorting table columns that do not appear directly
23 | # in the database table. E.g., map city_id: 'cities.name' to
24 | # sort the displayed city names.
25 | def sort_mappings=(hash)
26 | self.sort_mappings_with_indifferent_access =
27 | hash.with_indifferent_access
28 | end
29 | end
30 |
31 | # Prepended methods for sorting.
32 | module Prepends
33 | private
34 |
35 | # Enhance the list entries with an optional sort order.
36 | def list_entries
37 | sortable = sortable?(params[:sort])
38 | if sortable || default_sort
39 | clause = [ sortable ? sort_expression : nil, default_sort ]
40 | super.reorder(Arel.sql(clause.compact.join(", ")))
41 | else
42 | super
43 | end
44 | end
45 |
46 | # Return the sort expression to be used in the list query.
47 | def sort_expression
48 | col = sort_mappings_with_indifferent_access[params[:sort]] ||
49 | "#{model_class.table_name}.#{params[:sort]}"
50 | "#{col} #{sort_dir}"
51 | end
52 |
53 | # The sort direction, either 'asc' or 'desc'.
54 | def sort_dir
55 | params[:sort_dir] == "desc" ? "DESC" : "ASC"
56 | end
57 |
58 | # Returns true if the passed attribute is sortable.
59 | def sortable?(attr)
60 | attr.present? && (
61 | model_class.column_names.include?(attr.to_s) ||
62 | sort_mappings_with_indifferent_access.include?(attr))
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/app/controllers/list_controller.rb:
--------------------------------------------------------------------------------
1 | # Abstract controller providing a basic list action.
2 | # The loaded model entries are available in the view as an instance variable
3 | # named after the +model_class+ or by the helper method +entries+.
4 | #
5 | # The +index+ action lists all entries of a certain model and provides
6 | # functionality to search and sort this list.
7 | # Furthermore, it remembers the last search and sort parameters after the
8 | # user returns from a displayed or edited entry.
9 | class ListController < ApplicationController
10 | include DryCrud::GenericModel
11 | prepend DryCrud::Nestable
12 | include DryCrud::RenderCallbacks
13 | include DryCrud::Rememberable
14 |
15 | define_render_callbacks :index
16 |
17 | helper_method :entries
18 |
19 | ############## ACTIONS ############################################
20 |
21 | # GET /entries
22 | # GET /entries.json
23 | #
24 | # List all entries of this model.
25 | def index
26 | entries
27 | end
28 |
29 | private
30 |
31 | # Helper method to access the entries to be displayed in the current index
32 | # page in an uniform way.
33 | def entries
34 | model_ivar_get(plural: true) || model_ivar_set(list_entries)
35 | end
36 |
37 | # The base relation used to filter the entries.
38 | # Calls the #list scope if it is defined on the model class.
39 | #
40 | # This method may be adapted as long it returns an
41 | # ActiveRecord::Relation.
42 | # Some of the modules included extend this method.
43 | def list_entries
44 | model_class.respond_to?(:list) ? model_scope.list : model_scope
45 | end
46 |
47 | # Include these modules after the #list_entries method is defined.
48 | include DryCrud::Searchable
49 | include DryCrud::Sortable
50 | end
51 |
--------------------------------------------------------------------------------
/app/helpers/actions_helper.rb:
--------------------------------------------------------------------------------
1 | # Helpers to create action links. This default implementation supports
2 | # regular links with an icon and a label. To change the general style
3 | # of action links, change the method #action_link, e.g. to generate a button.
4 | # The common crud actions show, edit, destroy, index and add are provided here.
5 | module ActionsHelper
6 | # A generic helper method to create action links.
7 | # These link could be styled to look like buttons, for example.
8 | def action_link(label, icon = nil, url = {}, html_options = {})
9 | add_css_class html_options, "action btn btn-light"
10 | link_to(icon ? action_icon(icon, label) : label,
11 | url, html_options)
12 | end
13 |
14 | # Outputs an icon for an action with an optional label.
15 | def action_icon(icon, label = nil)
16 | html = tag.i("", class: "bi-#{icon}")
17 | html << " " << label if label
18 | html
19 | end
20 |
21 | # Standard show action to the given path.
22 | # Uses the current +entry+ if no path is given.
23 | def show_action_link(path = nil)
24 | path ||= path_args(entry)
25 | action_link(ti("link.show"), "zoom-in", path)
26 | end
27 |
28 | # Standard edit action to given path.
29 | # Uses the current +entry+ if no path is given.
30 | def edit_action_link(path = nil)
31 | path ||= path_args(entry)
32 | path = edit_polymorphic_path(path) unless path.is_a?(String)
33 | action_link(ti("link.edit"), "pencil", path)
34 | end
35 |
36 | # Standard destroy action to the given path.
37 | # Uses the current +entry+ if no path is given.
38 | def destroy_action_link(path = nil)
39 | path ||= path_args(entry)
40 | action_link(ti("link.delete"), "trash", path,
41 | data: { 'turbo-confirm': ti(:confirm_delete),
42 | 'turbo-method': :delete })
43 | end
44 |
45 | # Standard list action to the given path.
46 | # Uses the current +model_class+ if no path is given.
47 | def index_action_link(path = nil, url_options = { returning: true })
48 | path ||= path_args(model_class)
49 | path = polymorphic_path(path, url_options) unless path.is_a?(String)
50 | action_link(ti("link.list"), "list", path)
51 | end
52 |
53 | # Standard add action to given path.
54 | # Uses the current +model_class+ if no path is given.
55 | def add_action_link(path = nil, url_options = {})
56 | path ||= path_args(model_class)
57 | path = new_polymorphic_path(path, url_options) unless path.is_a?(String)
58 | action_link(ti("link.add"), "plus", path)
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/app/helpers/dry_crud/form/control.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | module Form
3 | # Internal class to handle the rendering of a single form control,
4 | # consisting of a label, input field, addon, help text or
5 | # required mark.
6 | class Control
7 | attr_reader :builder, :attr, :args, :options, :addon, :help
8 |
9 | delegate :tag, :object, :add_css_class,
10 | to: :builder
11 |
12 | # Html displayed to mark an input as required.
13 | REQUIRED_MARK = "*".freeze
14 |
15 | # Number of default input field span columns depending
16 | # on the #field_method.
17 | INPUT_SPANS = Hash.new(8)
18 | INPUT_SPANS[:number_field] =
19 | INPUT_SPANS[:integer_field] =
20 | INPUT_SPANS[:float_field] =
21 | INPUT_SPANS[:decimal_field] = 2
22 | INPUT_SPANS[:date_field] =
23 | INPUT_SPANS[:time_field] = 3
24 |
25 | # Create a new control instance.
26 | # Takes the form builder, the attribute to build the control for
27 | # as well as any additional arguments for the field method.
28 | # This includes an options hash as the last argument, that
29 | # may contain the following special options:
30 | #
31 | # * :addon - Addon content displayed just after the input field.
32 | # * :help - A help text displayed below the input field.
33 | # * :span - Number of columns the input field should span.
34 | # * :caption - Different caption for the label.
35 | # * :field_method - Different method to create the input field.
36 | # * :required - Sets the field as required
37 | # (The value for this option usually is 'required').
38 | #
39 | # All the other options will go to the field_method.
40 | def initialize(builder, attr, *args, **options)
41 | @builder = builder
42 | @attr = attr
43 | @options = options
44 | @args = args
45 |
46 | @addon = options.delete(:addon)
47 | @help = options.delete(:help)
48 | @span = options.delete(:span)
49 | @caption = options.delete(:caption)
50 | @field_method = options.delete(:field_method)
51 | @required = options[:required]
52 | end
53 |
54 | # Renders only the content of the control.
55 | # I.e. no label and span divs.
56 | def render_content
57 | content
58 | end
59 |
60 | # Renders the complete control with label and everything.
61 | # Render the content given or the default one.
62 | def render_labeled(content = nil)
63 | @content = content if content
64 | labeled
65 | end
66 |
67 | private
68 |
69 | # Create the HTML markup for any labeled content.
70 | def labeled
71 | tag.div(class: "row mb-3") do
72 | builder.label(attr, caption, class: "col-md-2 col-form-label") +
73 | tag.div(content, class: "col-md-#{span}")
74 | end
75 | end
76 |
77 | # Return the currently set content or create it
78 | # based on the various options given.
79 | #
80 | # Optionally renders addon, required mark and/or a help block
81 | # additionally to the input field.
82 | def content
83 | @content ||= begin
84 | content = input
85 | if addon
86 | content = builder.with_addon(content, addon)
87 | elsif required
88 | content = builder.with_addon(content, REQUIRED_MARK)
89 | end
90 | content << builder.help_block(help) if help.present?
91 | content
92 | end
93 | end
94 |
95 | # Return the currently set input field or create it
96 | # depending on the attribute.
97 | def input
98 | @input ||= begin
99 | options[:required] = "required" if required
100 | add_css_class(options, "is-invalid") if errors?
101 | builder.send(field_method, attr, *args, **options)
102 | end
103 | end
104 |
105 | # The field method used to create the input.
106 | # If none is set, detect it from the attribute type.
107 | def field_method
108 | @field_method ||= detect_field_method
109 | end
110 |
111 | # True if the attr is required, false otherwise.
112 | def required
113 | @required = @required.nil? ? builder.required?(attr) : @required
114 | end
115 |
116 | # Number of grid columns the input field should span.
117 | def span
118 | @span ||= INPUT_SPANS[field_method]
119 | end
120 |
121 | # The caption of the label.
122 | # If none is set, uses the I18n value of the attribute.
123 | def caption
124 | @caption ||= builder.captionize(attr, object.class)
125 | end
126 |
127 | # Returns true if any errors are found on the passed attribute or its
128 | # association.
129 | def errors?
130 | attr_plain, attr_id = builder.assoc_and_id_attr(attr)
131 | object.errors.key?(attr_plain.to_sym) ||
132 | object.errors.key?(attr_id.to_sym)
133 | end
134 |
135 | # Defines the field method to use based on the attribute
136 | # type, association or name.
137 | def detect_field_method
138 | if type == :text
139 | :text_area
140 | elsif association_kind?(:belongs_to)
141 | :belongs_to_field
142 | elsif association_kind?(:has_and_belongs_to_many, :has_many)
143 | :has_many_field
144 | elsif attr.to_s.include?("password")
145 | :password_field
146 | elsif attr.to_s.include?("email")
147 | :email_field
148 | elsif builder.respond_to?(:"#{type}_field")
149 | :"#{type}_field"
150 | else
151 | :text_field
152 | end
153 | end
154 |
155 | # The column type of the attribute.
156 | def type
157 | @type ||= builder.column_type(object, attr)
158 | end
159 |
160 | # Returns true if attr is a non-polymorphic association.
161 | # If one or more macros are given, the association must be of this kind.
162 | def association_kind?(*macros)
163 | if type == :integer || type.nil?
164 | assoc = builder.association(object, attr, *macros)
165 |
166 | assoc.present? && assoc.options[:polymorphic].nil?
167 | else
168 | false
169 | end
170 | end
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/app/helpers/dry_crud/table/actions.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | module Table
3 | # Adds action columns to the table builder.
4 | # Predefined actions are available for show, edit and destroy.
5 | # Additionally, a special col type to define cells linked to the show page
6 | # of the row entry is provided.
7 | module Actions
8 | extend ActiveSupport::Concern
9 |
10 | included do
11 | delegate :link_to, :path_args, :edit_polymorphic_path, :ti,
12 | to: :template
13 | end
14 |
15 | # Renders the passed attr with a link to the show action for
16 | # the current entry.
17 | # A block may be given to define the link path for the row entry.
18 | def attr_with_show_link(attr, &block)
19 | sortable_attr(attr) do |entry|
20 | link_to(format_attr(entry, attr), action_path(entry, &block))
21 | end
22 | end
23 |
24 | # Action column to show the row entry.
25 | # A block may be given to define the link path for the row entry.
26 | # If the block returns nil, no link is rendered.
27 | def show_action_col(**html_options, &block)
28 | action_col do |entry|
29 | path = action_path(entry, &block)
30 | if path
31 | table_action_link("zoom-in",
32 | path,
33 | **html_options.clone)
34 | end
35 | end
36 | end
37 |
38 | # Action column to edit the row entry.
39 | # A block may be given to define the link path for the row entry.
40 | # If the block returns nil, no link is rendered.
41 | def edit_action_col(**html_options, &block)
42 | action_col do |entry|
43 | path = action_path(entry, &block)
44 | if path
45 | path = edit_polymorphic_path(path) unless path.is_a?(String)
46 | table_action_link("pencil", path, **html_options.clone)
47 | end
48 | end
49 | end
50 |
51 | # Action column to destroy the row entry.
52 | # A block may be given to define the link path for the row entry.
53 | # If the block returns nil, no link is rendered.
54 | def destroy_action_col(**html_options, &block)
55 | action_col do |entry|
56 | path = action_path(entry, &block)
57 | if path
58 | table_action_link("trash",
59 | path,
60 | **html_options,
61 | data: { 'turbo-confirm': ti(:confirm_delete),
62 | 'turbo-method': :delete })
63 | end
64 | end
65 | end
66 |
67 | # Action column inside a table. No header.
68 | # The cell content should be defined in the passed block.
69 | def action_col(&block)
70 | col("", class: "action", &block)
71 | end
72 |
73 | # Generic action link inside a table.
74 | def table_action_link(icon, url, **html_options)
75 | add_css_class(html_options, "bi-#{icon}")
76 | link_to("", url, html_options)
77 | end
78 |
79 | private
80 |
81 | # If a block is given, call it to get the path for the current row entry.
82 | # Otherwise, return the standard path args.
83 | def action_path(entry)
84 | block_given? ? yield(entry) : path_args(entry)
85 | end
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/app/helpers/dry_crud/table/builder.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | module Table
3 | # A simple helper to easily define tables listing several rows of the same
4 | # data type.
5 | #
6 | # Example Usage:
7 | # DryCrud::Table::Builder.table(entries, template) do |t|
8 | # t.col('My Header', class: 'css') {|e| link_to 'Show', e }
9 | # t.attrs :name, :city
10 | # end
11 | class Builder
12 | include Sorting
13 | include Actions
14 |
15 | attr_reader :entries, :cols, :options, :template
16 |
17 | delegate :tag, :format_attr, :column_type, :association, :dom_id,
18 | :captionize, :add_css_class, :content_tag_nested,
19 | to: :template
20 |
21 | def initialize(entries, template, **options)
22 | @entries = entries
23 | @template = template
24 | @options = options
25 | @cols = []
26 | end
27 |
28 | # Convenience method to directly generate a table. Renders a row for each
29 | # entry in entries. Takes a block that gets the table object as parameter
30 | # for configuration. Returns the generated html for the table.
31 | def self.table(entries, template, **options)
32 | t = new(entries, template, **options)
33 | yield t
34 | t.to_html
35 | end
36 |
37 | # Define a column for the table with the given header, the html_options
38 | # used for each td and a block rendering the contents of a cell for the
39 | # current entry. The columns appear in the order they are defined.
40 | def col(header = "", **html_options, &block)
41 | @cols << Col.new(header, html_options, @template, block)
42 | end
43 |
44 | # Convenience method to add one or more attribute columns.
45 | # The attribute name will become the header, the cells will contain
46 | # the formatted attribute value for the current entry.
47 | def attrs(*attrs)
48 | attrs.each do |a|
49 | attr(a)
50 | end
51 | end
52 |
53 | # Define a column for the given attribute and an optional header.
54 | # If no header is given, the attribute name is used. The cell will
55 | # contain the formatted attribute value for the current entry.
56 | def attr(attr, header = nil, **html_options, &block)
57 | header ||= attr_header(attr)
58 | block ||= ->(e) { format_attr(e, attr) }
59 | add_css_class(html_options, align_class(attr))
60 | col(header, **html_options, &block)
61 | end
62 |
63 | # Renders the table as HTML.
64 | def to_html
65 | tag.table(**options) do
66 | tag.thead(html_header) +
67 | content_tag_nested(:tbody, entries) { |e| html_row(e) }
68 | end
69 | end
70 |
71 | # Returns css classes used for alignment of the cell data.
72 | # Based on the column type of the attribute.
73 | def align_class(attr)
74 | entry = entries.present? ? entry_class.new : nil
75 | case column_type(entry, attr)
76 | when :integer, :float, :decimal
77 | "right" unless association(entry, attr, :belongs_to)
78 | when :boolean
79 | "center"
80 | end
81 | end
82 |
83 | # Creates a header string for the given attr.
84 | def attr_header(attr)
85 | captionize(attr, entry_class)
86 | end
87 |
88 | private
89 |
90 | # Renders the header row of the table.
91 | def html_header
92 | content_tag_nested(:tr, cols, &:html_header)
93 | end
94 |
95 | # Renders a table row for the given entry.
96 | def html_row(entry)
97 | attrs = {}
98 | attrs[:id] = dom_id(entry) if entry.respond_to?(:to_key)
99 | content_tag_nested(:tr, cols, **attrs) { |c| c.html_cell(entry) }
100 | end
101 |
102 | # Determines the class of the table entries.
103 | # All entries should be of the same type.
104 | def entry_class
105 | if entries.respond_to?(:klass)
106 | entries.klass
107 | else
108 | entries.first.class
109 | end
110 | end
111 | end
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/app/helpers/dry_crud/table/col.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | module Table
3 | # Helper class to store column information.
4 | class Col # :nodoc:
5 | delegate :tag, :capture, to: :template
6 |
7 | attr_reader :header, :html_options, :template, :block
8 |
9 | def initialize(header, html_options, template, block)
10 | @header = header
11 | @html_options = html_options
12 | @template = template
13 | @block = block
14 | end
15 |
16 | # Runs the Col block for the given entry.
17 | def content(entry)
18 | entry.nil? ? "" : capture(entry, &block)
19 | end
20 |
21 | # Renders the header cell of the Col.
22 | def html_header
23 | tag.th(header, **html_options)
24 | end
25 |
26 | # Renders a table cell for the given entry.
27 | def html_cell(entry)
28 | tag.td(content(entry), **html_options)
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/helpers/dry_crud/table/sorting.rb:
--------------------------------------------------------------------------------
1 | module DryCrud
2 | module Table
3 | # Provides headers with sort links. Expects a method :sortable?(attr)
4 | # in the template/controller to tell if an attribute is sortable or not.
5 | # Extracted into an own module for convenience.
6 | module Sorting
7 | # Create a header with sort links and a mark for the current sort
8 | # direction.
9 | def sort_header(attr, label = nil)
10 | label ||= attr_header(attr)
11 | template.link_to(label, sort_params(attr)) + current_mark(attr)
12 | end
13 |
14 | # Same as :attrs, except that it renders a sort link in the header
15 | # if an attr is sortable.
16 | def sortable_attrs(*attrs)
17 | attrs.each { |a| sortable_attr(a) }
18 | end
19 |
20 | # Renders a sort link header, otherwise similar to :attr.
21 | def sortable_attr(attr, header = nil, &block)
22 | if template.sortable?(attr)
23 | attr(attr, sort_header(attr, header), &block)
24 | else
25 | attr(attr, header, &block)
26 | end
27 | end
28 |
29 | private
30 |
31 | # Request params for the sort link.
32 | def sort_params(attr)
33 | result = params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params
34 | result.merge(sort: attr, sort_dir: sort_dir(attr), only_path: true)
35 | end
36 |
37 | # The sort mark, if any, for the given attribute.
38 | def current_mark(attr)
39 | if current_sort?(attr)
40 | # rubocop:disable Rails/OutputSafety
41 | (sort_dir(attr) == "asc" ? " ↑" : " ↓").html_safe
42 | # rubocop:enable Rails/OutputSafety
43 | else
44 | ""
45 | end
46 | end
47 |
48 | # Returns true if the given attribute is the current sort column.
49 | def current_sort?(attr)
50 | params[:sort] == attr.to_s
51 | end
52 |
53 | # The sort direction to use in the sort link for the given attribute.
54 | def sort_dir(attr)
55 | current_sort?(attr) && params[:sort_dir] == "asc" ? "desc" : "asc"
56 | end
57 |
58 | # Delegate to template.
59 | def params
60 | template.params
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/app/helpers/form_helper.rb:
--------------------------------------------------------------------------------
1 | # Defines forms to edit models. The helper methods come in different
2 | # granularities:
3 | # * #plain_form - A form using Crud::FormBuilder.
4 | # * #standard_form - A #plain_form for a given object and attributes with error
5 | # messages and save and cancel buttons.
6 | # * #crud_form - A #standard_form for the current +entry+, with the given
7 | # attributes or default.
8 | module FormHelper
9 | # Renders a form using Crud::FormBuilder.
10 | def plain_form(object, **options, &block)
11 | options[:html] ||= {}
12 | add_css_class(options[:html], "form-horizontal")
13 | options[:html][:role] ||= "form"
14 | options[:builder] ||= DryCrud::Form::Builder
15 | options[:cancel_url] ||= polymorphic_path(object, returning: true)
16 |
17 | form_for(object, **options, &block)
18 | end
19 |
20 | # Renders a standard form for the given entry and attributes.
21 | # The form is rendered with a basic save and cancel button.
22 | # If a block is given, custom input fields may be rendered and attrs is
23 | # ignored. Before the input fields, the error messages are rendered,
24 | # if present. An options hash may be given as the last argument.
25 | def standard_form(object, *attrs, **options, &block)
26 | plain_form(object, **options) do |form|
27 | content = [ form.error_messages ]
28 |
29 | content << if block_given?
30 | capture(form, &block)
31 | else
32 | form.labeled_input_fields(*attrs)
33 | end
34 |
35 | content << form.standard_actions
36 | safe_join(content)
37 | end
38 | end
39 |
40 | # Renders a crud form for the current entry with default_crud_attrs or the
41 | # given attribute array. An options hash may be given as the last argument.
42 | # If a block is given, a custom form may be rendered and attrs is ignored.
43 | def crud_form(*attrs, **options, &block)
44 | attrs = default_crud_attrs - %i[created_at updated_at] if attrs.blank?
45 | standard_form(path_args(entry), *attrs, **options, &block)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/app/helpers/format_helper.rb:
--------------------------------------------------------------------------------
1 | # Provides uniform formatting of basic data types, based on Ruby class (#f)
2 | # or database column type (#format_attr). If other helpers define methods
3 | # with names like 'format_{class}_{attr}', these methods are used for
4 | # formatting.
5 | #
6 | # Futher helpers standartize the layout of multiple attributes (#render_attrs),
7 | # values with labels (#labeled) and simple lists.
8 | module FormatHelper
9 | # Formats a basic value based on its Ruby class.
10 | def f(value)
11 | case value
12 | when Float, BigDecimal
13 | number_with_precision(value, precision: t("number.format.precision"),
14 | delimiter: t("number.format.delimiter"))
15 | when Integer
16 | number_with_delimiter(value, delimiter: t("number.format.delimiter"))
17 | when Date then l(value)
18 | when Time then "#{l(value.to_date)} #{l(value, format: :time)}"
19 | when true then t("global.yes")
20 | when false then t("global.no")
21 | when nil then UtilityHelper::EMPTY_STRING
22 | else value.to_s
23 | end
24 | end
25 |
26 | # Formats an arbitrary attribute of the given ActiveRecord object.
27 | # If no specific format_{class}_{attr} or format_{attr} method is found,
28 | # formats the value as follows:
29 | # If the value is an associated model, renders the label of this object.
30 | # Otherwise, calls format_type.
31 | def format_attr(obj, attr)
32 | format_with_helper(obj, attr) ||
33 | format_association(obj, attr) ||
34 | format_type(obj, attr)
35 | end
36 |
37 | # Renders a simple unordered list, which will
38 | # simply render all passed items or yield them
39 | # to your block.
40 | def simple_list(items, **ul_options)
41 | content_tag_nested(:ul, items, **ul_options) do |item|
42 | tag.li(block_given? ? yield(item) : f(item))
43 | end
44 | end
45 |
46 | # Renders a list of attributes with label and value for a given object.
47 | # Optionally surrounded with a div.
48 | def render_attrs(obj, *attrs)
49 | content_tag_nested(:dl, attrs, class: "dl-horizontal") do |a|
50 | labeled_attr(obj, a)
51 | end
52 | end
53 |
54 | # Renders the formatted content of the given attribute with a label.
55 | def labeled_attr(obj, attr)
56 | labeled(captionize(attr, obj.class), format_attr(obj, attr))
57 | end
58 |
59 | # Renders an arbitrary content with the given label. Used for uniform
60 | # presentation.
61 | def labeled(label, content = nil, &block)
62 | content = capture(&block) if block_given?
63 | render("shared/labeled", label: label, content: content)
64 | end
65 |
66 | # Transform the given text into a form as used by labels or table headers.
67 | def captionize(text, clazz = nil)
68 | text = text.to_s
69 | if clazz.respond_to?(:human_attribute_name)
70 | text_without_id = text.end_with?("_ids") ? text[0..-5].pluralize : text
71 | clazz.human_attribute_name(text_without_id)
72 | else
73 | text.humanize.titleize
74 | end
75 | end
76 |
77 | private
78 |
79 | # Checks whether a format_{class}_{attr} or format_{attr} helper method is
80 | # defined and calls it if is.
81 | def format_with_helper(obj, attr)
82 | class_name = obj.class.name.underscore.tr("/", "_")
83 | format_type_attr_method = :"format_#{class_name}_#{attr}"
84 | format_attr_method = :"format_#{attr}"
85 |
86 | if respond_to?(format_type_attr_method)
87 | send(format_type_attr_method, obj)
88 | elsif respond_to?(format_attr_method)
89 | send(format_attr_method, obj)
90 | else
91 | false
92 | end
93 | end
94 |
95 | # Checks whether the given attr is an association of obj and formats it
96 | # accordingly if it is.
97 | def format_association(obj, attr)
98 | belongs_to = association(obj, attr, :belongs_to, :has_one)
99 | has_many = association(obj, attr, :has_many, :has_and_belongs_to_many)
100 |
101 | if belongs_to
102 | format_belongs_to(obj, belongs_to)
103 | elsif has_many
104 | format_has_many(obj, has_many)
105 | else
106 | false
107 | end
108 | end
109 |
110 | # Formats an arbitrary attribute of the given object depending on its data
111 | # type. For Active Records, take the defined data type into account for
112 | # special types that have no own object class.
113 | def format_type(obj, attr)
114 | val = obj.send(attr)
115 | return UtilityHelper::EMPTY_STRING if val.blank? && val != false
116 |
117 | case column_type(obj, attr)
118 | when :time then l(val, format: :time)
119 | when :date then f(val.to_date)
120 | when :datetime, :timestamp then f(val.time)
121 | when :text then simple_format(h(val))
122 | when :decimal
123 | number_with_precision(val.to_s.to_f,
124 | precision: column_property(obj, attr, :scale),
125 | delimiter: t("number.format.delimiter"))
126 | else f(val)
127 | end
128 | end
129 |
130 | # Formats an ActiveRecord +belongs_to+ or +has_one+ association.
131 | def format_belongs_to(obj, assoc)
132 | val = obj.send(assoc.name)
133 | if val
134 | assoc_link(assoc, val)
135 | else
136 | ta(:no_entry, assoc)
137 | end
138 | end
139 |
140 | # Formats an ActiveRecord +has_and_belongs_to_many+ or
141 | # +has_many+ association.
142 | def format_has_many(obj, assoc)
143 | values = obj.send(assoc.name)
144 | if values.size == 1
145 | assoc_link(assoc, values.first)
146 | elsif values.present?
147 | simple_list(values) { |val| assoc_link(assoc, val) }
148 | else
149 | ta(:no_entry, assoc)
150 | end
151 | end
152 |
153 | # Renders a link to the given association entry.
154 | def assoc_link(assoc, val)
155 | link_to_if(assoc_link?(assoc, val), val.to_s, val)
156 | end
157 |
158 | # Returns true if no link should be created when formatting the given
159 | # association.
160 | def assoc_link?(_assoc, val)
161 | respond_to?(:"#{val.class.model_name.singular_route_key}_path")
162 | end
163 | end
164 |
--------------------------------------------------------------------------------
/app/helpers/i18n_helper.rb:
--------------------------------------------------------------------------------
1 | # Translation helpers extending the Rails +translate+ helper to support
2 | # translation inheritance over the controller class hierarchy.
3 | module I18nHelper
4 | # Translates the passed key by looking it up over the controller hierarchy.
5 | # The key is searched in the following order:
6 | # - {controller}.{current_partial}.{key}
7 | # - {controller}.{current_action}.{key}
8 | # - {controller}.global.{key}
9 | # - {parent_controller}.{current_partial}.{key}
10 | # - {parent_controller}.{current_action}.{key}
11 | # - {parent_controller}.global.{key}
12 | # - ...
13 | # - global.{key}
14 | def translate_inheritable(key, **variables)
15 | partial = defined?(@virtual_path) ? @virtual_path.gsub(/.*\/_?/, "") : nil
16 | defaults = inheritable_translation_defaults(key, partial)
17 | variables[:default] ||= defaults
18 | t(defaults.shift, **variables)
19 | end
20 |
21 | alias ti translate_inheritable
22 |
23 | # Translates the passed key for an active record association. This helper is
24 | # used for rendering association dependent keys in forms like :no_entry,
25 | # :none_available or :please_select.
26 | # The key is looked up in the following order:
27 | # - activerecord.associations.models.{model_name}.{association_name}.{key}
28 | # - activerecord.associations.{association_model_name}.{key}
29 | # - global.associations.{key}
30 | def translate_association(key, assoc = nil, **variables)
31 | if assoc && assoc.options[:polymorphic].nil?
32 | variables[:default] ||= [ association_klass_key(assoc, key).to_sym,
33 | :"global.associations.#{key}" ]
34 | t(association_owner_key(assoc, key), **variables)
35 | else
36 | t("global.associations.#{key}", **variables)
37 | end
38 | end
39 |
40 | alias ta translate_association
41 |
42 | private
43 |
44 | # General translation key based on the klass of the association.
45 | def association_klass_key(assoc, key)
46 | k = "activerecord.associations."
47 | k << assoc.klass.model_name.singular
48 | k << "."
49 | k << key.to_s
50 | end
51 |
52 | # Specific translation key based on the owner model and the name
53 | # of the association.
54 | def association_owner_key(assoc, key)
55 | k = "activerecord.associations.models."
56 | k << assoc.active_record.model_name.singular
57 | k << "."
58 | k << assoc.name.to_s
59 | k << "."
60 | k << key.to_s
61 | end
62 |
63 | def inheritable_translation_defaults(key, partial)
64 | defaults = []
65 | current = controller.class
66 | while current < ActionController::Base
67 | folder = current.controller_path
68 | if folder.present?
69 | append_controller_translation_keys(defaults, folder, partial, key)
70 | end
71 | current = current.superclass
72 | end
73 | defaults << :"global.#{key}"
74 | end
75 |
76 | def append_controller_translation_keys(defaults, folder, partial, key)
77 | defaults << :"#{folder}.#{partial}.#{key}" if partial
78 | defaults << :"#{folder}.#{action_name}.#{key}"
79 | defaults << :"#{folder}.global.#{key}"
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/app/helpers/table_helper.rb:
--------------------------------------------------------------------------------
1 | # Defines tables to display a list of entries. The helper methods come in
2 | # different granularities:
3 | # * #plain_table - A basic table for the given entries and attributes using
4 | # the Crud::TableBuilder.
5 | # * #list_table - A sortable #plain_table for the current +entries+, with the
6 | # given attributes or default.
7 | # * #crud_table - A sortable #plain_table for the current +entries+, with the
8 | # given attributes or default and the standard crud action links.
9 | module TableHelper
10 | # Renders a table for the given entries. One column is rendered for each
11 | # attribute passed. If a block is given, the columns defined therein are
12 | # appended to the attribute columns.
13 | # If entries is empty, an appropriate message is rendered.
14 | # An options hash may be given as the last argument.
15 | def plain_table(entries, *attrs, **options)
16 | add_css_class(options, "table table-striped table-hover")
17 | builder = options.delete(:builder) || DryCrud::Table::Builder
18 | builder.table(entries, self, **options) do |t|
19 | t.attrs(*attrs)
20 | yield t if block_given?
21 | end
22 | end
23 |
24 | # Renders a #plain_table for the given entries.
25 | # If entries is empty, an appropriate message is rendered.
26 | def plain_table_or_message(entries, *attrs, **options, &block)
27 | entries.to_a # force evaluation of relations
28 | if entries.present?
29 | plain_table(entries, *attrs, **options, &block)
30 | else
31 | tag.div(ti(:no_list_entries), class: "table")
32 | end
33 | end
34 |
35 | # Create a table of the +entries+ with the default or
36 | # the passed attributes in its columns. An options hash may be given
37 | # as the last argument.
38 | def list_table(*attrs, **options, &block)
39 | attrs = attrs_or_default(attrs, &block)
40 | plain_table_or_message(entries, **options) do |t|
41 | t.sortable_attrs(*attrs)
42 | yield t if block_given?
43 | end
44 | end
45 |
46 | # Create a table of the current +entries+ with the default or the passed
47 | # attributes in its columns. Edit and destroy actions are added to each row.
48 | # If attrs are present, the first column will link to the show
49 | # action. Edit and destroy actions are appended to the end of each row.
50 | # If a block is given, the column defined there will be inserted
51 | # between the given attributes and the actions.
52 | # An options hash for the table builder may be given as the last argument.
53 | def crud_table(*attrs, **options, &block)
54 | attrs = attrs_or_default(attrs, &block)
55 | first = attrs.shift
56 | plain_table_or_message(entries, **options) do |t|
57 | t.attr_with_show_link(first) if first
58 | t.sortable_attrs(*attrs)
59 | yield t if block_given?
60 | standard_table_actions(t)
61 | end
62 | end
63 |
64 | # Adds standard action link columns (edit, destroy) to the given table.
65 | def standard_table_actions(table)
66 | table.edit_action_col
67 | table.destroy_action_col
68 | end
69 |
70 | private
71 |
72 | def attrs_or_default(attrs)
73 | if !block_given? && attrs.blank?
74 | default_crud_attrs
75 | else
76 | attrs
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/app/helpers/utility_helper.rb:
--------------------------------------------------------------------------------
1 | require "English"
2 |
3 | # View helpers for basic functions used in various other helpers.
4 | module UtilityHelper
5 | # non-breaking space asserts better css.
6 | EMPTY_STRING = " ".html_safe.freeze
7 |
8 | # Render a content tag with the collected contents rendered
9 | # by &block for each item in collection.
10 | def content_tag_nested(tag, collection, **options, &block)
11 | content_tag(tag, safe_join(collection, &block), **options)
12 | end
13 |
14 | # Overridden method that takes a block that is executed for each item in
15 | # array before appending the results.
16 | def safe_join(array, sep = $OUTPUT_FIELD_SEPARATOR, &block)
17 | super(block_given? ? array.map(&block).compact : array, sep)
18 | end
19 |
20 | # Returns the css class for the given flash level.
21 | def flash_class(level)
22 | case level
23 | when :notice then "success"
24 | when :alert then "error"
25 | else level.to_s
26 | end
27 | end
28 |
29 | # Adds a class to the given options, even if there are already classes.
30 | def add_css_class(options, classes)
31 | if options[:class]
32 | options[:class] += " #{classes}" if classes
33 | else
34 | options[:class] = classes
35 | end
36 | end
37 |
38 | # The default attributes to use in attrs, list and form partials.
39 | # These are all defined attributes except certain special ones like
40 | # 'id' or 'position'.
41 | def default_crud_attrs
42 | attrs = model_class.column_names.map(&:to_sym)
43 | attrs - %i[id position password]
44 | end
45 |
46 | # Returns the ActiveRecord column type or nil.
47 | def column_type(obj, attr)
48 | column_property(obj, attr, :type)
49 | end
50 |
51 | # Returns an ActiveRecord column property for the passed attr or nil
52 | def column_property(obj, attr, property)
53 | if obj.respond_to?(:column_for_attribute) && obj.has_attribute?(attr)
54 | obj.column_for_attribute(attr).send(property)
55 | end
56 | end
57 |
58 | # Returns the association proxy for the given attribute. The attr parameter
59 | # may be the _id column or the association name. If a macro (e.g.
60 | # :belongs_to) is given, the association must be of this type, otherwise,
61 | # any association is returned. Returns nil if no association (or not of the
62 | # given macro) was found.
63 | def association(obj, attr, *macros)
64 | if obj.class.respond_to?(:reflect_on_association)
65 | name = assoc_and_id_attr(attr).first.to_sym
66 | assoc = obj.class.reflect_on_association(name)
67 | assoc if assoc && (macros.blank? || macros.include?(assoc.macro))
68 | end
69 | end
70 |
71 | # Returns the name of the attr and it's corresponding field
72 | def assoc_and_id_attr(attr)
73 | attr = attr.to_s
74 | if attr.end_with?("_id")
75 | [ attr[0..-4], attr ]
76 | elsif attr.end_with?("_ids")
77 | [ attr[0..-5].pluralize, attr ]
78 | else
79 | [ attr, "#{attr}_id" ]
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/app/views/crud/_actions_edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= index_action_link %>
2 | <%= show_action_link %>
3 | <%= destroy_action_link %>
--------------------------------------------------------------------------------
/app/views/crud/_actions_edit.html.haml:
--------------------------------------------------------------------------------
1 | = index_action_link
2 | = show_action_link
3 | = destroy_action_link
--------------------------------------------------------------------------------
/app/views/crud/_actions_index.html.erb:
--------------------------------------------------------------------------------
1 | <%= add_action_link %>
--------------------------------------------------------------------------------
/app/views/crud/_actions_index.html.haml:
--------------------------------------------------------------------------------
1 | = add_action_link
2 |
--------------------------------------------------------------------------------
/app/views/crud/_actions_show.html.erb:
--------------------------------------------------------------------------------
1 | <%= index_action_link %>
2 | <%= edit_action_link %>
3 | <%= destroy_action_link %>
--------------------------------------------------------------------------------
/app/views/crud/_actions_show.html.haml:
--------------------------------------------------------------------------------
1 | = index_action_link
2 | = edit_action_link
3 | = destroy_action_link
--------------------------------------------------------------------------------
/app/views/crud/_attrs.html.erb:
--------------------------------------------------------------------------------
1 | <%= render_attrs entry, *default_crud_attrs %>
--------------------------------------------------------------------------------
/app/views/crud/_attrs.html.haml:
--------------------------------------------------------------------------------
1 | = render_attrs entry, *default_crud_attrs
--------------------------------------------------------------------------------
/app/views/crud/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= crud_form %>
--------------------------------------------------------------------------------
/app/views/crud/_form.html.haml:
--------------------------------------------------------------------------------
1 | = crud_form
--------------------------------------------------------------------------------
/app/views/crud/_list.html.erb:
--------------------------------------------------------------------------------
1 | <%= crud_table %>
--------------------------------------------------------------------------------
/app/views/crud/_list.html.haml:
--------------------------------------------------------------------------------
1 | = crud_table
--------------------------------------------------------------------------------
/app/views/crud/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% @title ||= ti(:title, model: full_entry_label).html_safe -%>
2 |
3 | <% content_for(:actions, render('actions_edit')) %>
4 |
5 | <%= render 'form' %>
6 |
--------------------------------------------------------------------------------
/app/views/crud/edit.html.haml:
--------------------------------------------------------------------------------
1 | - @title ||= ti(:title, model: full_entry_label).html_safe
2 |
3 | - content_for(:actions, render('actions_edit'))
4 |
5 | = render 'form'
6 |
--------------------------------------------------------------------------------
/app/views/crud/new.html.erb:
--------------------------------------------------------------------------------
1 | <% @title ||= ti(:title, model: models_label(plural: false)) -%>
2 |
3 | <% content_for(:actions, index_action_link) %>
4 |
5 | <%= render 'form' %>
6 |
--------------------------------------------------------------------------------
/app/views/crud/new.html.haml:
--------------------------------------------------------------------------------
1 | - @title ||= ti(:title, model: models_label(plural: false))
2 |
3 | - content_for(:actions, index_action_link)
4 |
5 | = render 'form'
6 |
--------------------------------------------------------------------------------
/app/views/crud/show.html.erb:
--------------------------------------------------------------------------------
1 | <% @title ||= ti(:title, model: full_entry_label).html_safe -%>
2 |
3 | <% content_for(:actions, render('actions_show')) %>
4 |
5 | <%= render 'attrs' %>
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/views/crud/show.html.haml:
--------------------------------------------------------------------------------
1 | - @title ||= ti(:title, model: full_entry_label).html_safe
2 |
3 | - content_for(:actions, render('actions_show'))
4 |
5 | = render 'attrs'
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/views/crud/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! entry, :id, *default_crud_attrs
2 |
--------------------------------------------------------------------------------
/app/views/layouts/_flash.html.erb:
--------------------------------------------------------------------------------
1 | <% if flash[level].present? %>
2 |
26 |
27 |
<%= @title %>
28 |
29 |
30 |
31 | <%= yield :tools %>
32 |
33 |
34 |
35 | <%= yield :actions %>
36 |
37 |
38 |
39 |
40 |
41 | <%= render partial: 'layouts/flash', collection: [:notice, :alert], as: :level %>
42 |
43 |
44 |
45 | <%= yield %>
46 |
47 |
48 |
49 |
50 |
5 | <%= search_field_tag :q, params[:q], class: 'form-control' %>
6 | <%= submit_tag ti(:"button.search"), class: 'btn btn-outline-secondary' %>
7 |
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/app/views/list/_search.html.haml:
--------------------------------------------------------------------------------
1 | = form_tag(nil, { method: :get, class: 'form-inline', role: 'search' }) do
2 | = hidden_field_tag :returning, true
3 | = hidden_field_tag :page, 1
4 | .input-group
5 | = search_field_tag :q, params[:q], class: 'form-control'
6 | = submit_tag ti(:"button.search"), class: 'btn btn-outline-secondary'
7 |
--------------------------------------------------------------------------------
/app/views/list/index.html.erb:
--------------------------------------------------------------------------------
1 | <% @title ||= ti(:title, models: models_label) -%>
2 |
3 | <% content_for(:tools, render('search')) if search_support? %>
4 |
5 | <% content_for(:actions, render('actions_index')) %>
6 |
7 | <%= render 'list' %>
8 |
--------------------------------------------------------------------------------
/app/views/list/index.html.haml:
--------------------------------------------------------------------------------
1 | - @title ||= ti(:title, models: models_label)
2 |
3 | - content_for(:tools, render('search')) if search_support?
4 |
5 | - content_for(:actions, render('actions_index'))
6 |
7 | = render 'list'
8 |
--------------------------------------------------------------------------------
/app/views/list/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.array!(entries) do |entry|
2 | json.extract! entry, :id, *default_crud_attrs
3 | json.url polymorphic_url(path_args(entry), format: :json)
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/shared/_error_messages.html.erb:
--------------------------------------------------------------------------------
1 | <% if errors.any? %>
2 |