crud_components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +23 -0
- data/LICENSE +21 -0
- data/README.md +511 -0
- data/RELEASING.md +44 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/crud_components.css +35 -0
- data/app/views/crud_components/_action_button.html.erb +11 -0
- data/app/views/crud_components/_actions.html.erb +12 -0
- data/app/views/crud_components/_column_header.html.erb +24 -0
- data/app/views/crud_components/_column_picker.html.erb +66 -0
- data/app/views/crud_components/_filter.html.erb +34 -0
- data/app/views/crud_components/_form.html.erb +30 -0
- data/app/views/crud_components/_pager.html.erb +41 -0
- data/app/views/crud_components/_record.html.erb +15 -0
- data/app/views/crud_components/_row.html.erb +26 -0
- data/app/views/crud_components/_selection_action.html.erb +14 -0
- data/app/views/crud_components/_sort_link.html.erb +17 -0
- data/app/views/crud_components/_toolbar.html.erb +50 -0
- data/app/views/crud_components/fields/_asciidoc.html.erb +8 -0
- data/app/views/crud_components/fields/_association.html.erb +13 -0
- data/app/views/crud_components/fields/_association_list.html.erb +24 -0
- data/app/views/crud_components/fields/_attachment.html.erb +16 -0
- data/app/views/crud_components/fields/_attachment_thumb.html.erb +17 -0
- data/app/views/crud_components/fields/_boolean.html.erb +13 -0
- data/app/views/crud_components/fields/_date.html.erb +6 -0
- data/app/views/crud_components/fields/_datetime.html.erb +6 -0
- data/app/views/crud_components/fields/_email.html.erb +7 -0
- data/app/views/crud_components/fields/_enum.html.erb +14 -0
- data/app/views/crud_components/fields/_json.html.erb +10 -0
- data/app/views/crud_components/fields/_markdown.html.erb +9 -0
- data/app/views/crud_components/fields/_number.html.erb +8 -0
- data/app/views/crud_components/fields/_string.html.erb +8 -0
- data/app/views/crud_components/fields/_text.html.erb +9 -0
- data/app/views/crud_components/fields/_url.html.erb +11 -0
- data/app/views/crud_components/filters/_boolean.html.erb +12 -0
- data/app/views/crud_components/filters/_date_range.html.erb +11 -0
- data/app/views/crud_components/filters/_number_range.html.erb +13 -0
- data/app/views/crud_components/filters/_select.html.erb +8 -0
- data/app/views/crud_components/filters/_text.html.erb +5 -0
- data/app/views/crud_components/form_fields/_belongs_to.html.erb +3 -0
- data/app/views/crud_components/form_fields/_boolean.html.erb +12 -0
- data/app/views/crud_components/form_fields/_date.html.erb +2 -0
- data/app/views/crud_components/form_fields/_datetime.html.erb +2 -0
- data/app/views/crud_components/form_fields/_enum.html.erb +8 -0
- data/app/views/crud_components/form_fields/_file.html.erb +47 -0
- data/app/views/crud_components/form_fields/_habtm.html.erb +5 -0
- data/app/views/crud_components/form_fields/_number.html.erb +2 -0
- data/app/views/crud_components/form_fields/_string.html.erb +3 -0
- data/app/views/crud_components/form_fields/_text.html.erb +2 -0
- data/app/views/crud_components/layouts/_table.html.erb +143 -0
- data/config/locales/crud_components.de.yml +39 -0
- data/config/locales/crud_components.en.yml +40 -0
- data/crud_components.gemspec +48 -0
- data/docs/extending.md +308 -0
- data/docs/fields.md +442 -0
- data/docs/forms.md +253 -0
- data/docs/performance.md +90 -0
- data/docs/security.md +139 -0
- data/docs/views.md +405 -0
- data/lib/crud_components/action.rb +85 -0
- data/lib/crud_components/builder.rb +246 -0
- data/lib/crud_components/config.rb +128 -0
- data/lib/crud_components/dynamic_column.rb +68 -0
- data/lib/crud_components/engine.rb +25 -0
- data/lib/crud_components/errors.rb +9 -0
- data/lib/crud_components/fields/attachment_field.rb +22 -0
- data/lib/crud_components/fields/base.rb +260 -0
- data/lib/crud_components/fields/belongs_to_field.rb +91 -0
- data/lib/crud_components/fields/boolean_field.rb +31 -0
- data/lib/crud_components/fields/computed_field.rb +34 -0
- data/lib/crud_components/fields/date_field.rb +51 -0
- data/lib/crud_components/fields/dynamic_field.rb +44 -0
- data/lib/crud_components/fields/enum_field.rb +40 -0
- data/lib/crud_components/fields/has_many_field.rb +50 -0
- data/lib/crud_components/fields/json_field.rb +10 -0
- data/lib/crud_components/fields/numeric_field.rb +31 -0
- data/lib/crud_components/fields/path_field.rb +327 -0
- data/lib/crud_components/fields/string_field.rb +41 -0
- data/lib/crud_components/fields/text_field.rb +9 -0
- data/lib/crud_components/fieldset.rb +38 -0
- data/lib/crud_components/helpers.rb +259 -0
- data/lib/crud_components/like_spec.rb +113 -0
- data/lib/crud_components/markup.rb +36 -0
- data/lib/crud_components/model.rb +33 -0
- data/lib/crud_components/permission_context.rb +62 -0
- data/lib/crud_components/presenters/actions.rb +51 -0
- data/lib/crud_components/presenters/base.rb +95 -0
- data/lib/crud_components/presenters/cell_context.rb +28 -0
- data/lib/crud_components/presenters/cells.rb +160 -0
- data/lib/crud_components/presenters/collection.rb +498 -0
- data/lib/crud_components/presenters/column_selection.rb +91 -0
- data/lib/crud_components/presenters/filter.rb +38 -0
- data/lib/crud_components/presenters/form.rb +57 -0
- data/lib/crud_components/presenters/record.rb +57 -0
- data/lib/crud_components/query.rb +110 -0
- data/lib/crud_components/route_resolver.rb +123 -0
- data/lib/crud_components/structure.rb +343 -0
- data/lib/crud_components/version.rb +3 -0
- data/lib/crud_components/where_like.rb +13 -0
- data/lib/crud_components.rb +160 -0
- data/lib/generators/crud_components/install/install_generator.rb +43 -0
- data/lib/generators/crud_components/install/templates/crud_columns_controller.js +76 -0
- data/lib/generators/crud_components/install/templates/crud_filter_controller.js +32 -0
- data/lib/generators/crud_components/install/templates/crud_multiselect_controller.js +70 -0
- data/lib/generators/crud_components/install/templates/crud_select_controller.js +35 -0
- data/lib/generators/crud_components/install/templates/initializer.rb +56 -0
- data/lib/generators/crud_components/views/views_generator.rb +14 -0
- metadata +209 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%# Locals: field, query, form_id, compact, autosubmit, param_name, css
|
|
2
|
+
Inline (compact) stacks the two inputs tightly; the sidebar lays them out
|
|
3
|
+
as a horizontal group. %>
|
|
4
|
+
<div class="<%= compact ? 'd-flex flex-column gap-1' : css.input_group %>">
|
|
5
|
+
<%= tag.input type: 'number', step: 'any', name: "#{param_name}_geq",
|
|
6
|
+
value: query.value("#{field.name}_geq"),
|
|
7
|
+
class: compact ? css.input_sm : css.input, form: form_id,
|
|
8
|
+
placeholder: '≥', aria: { label: "#{field.human_name} ≥" } %>
|
|
9
|
+
<%= tag.input type: 'number', step: 'any', name: "#{param_name}_leq",
|
|
10
|
+
value: query.value("#{field.name}_leq"),
|
|
11
|
+
class: compact ? css.input_sm : css.input, form: form_id,
|
|
12
|
+
placeholder: '≤', aria: { label: "#{field.human_name} ≤" } %>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# Locals: field, query, form_id, compact, autosubmit, param_name, css %>
|
|
2
|
+
<% choices = [[t('crud_components.filter.any', default: '–'), '']] + field.filter_choices(query) %>
|
|
3
|
+
<% choices << [t('crud_components.filter.not_set', default: 'Not set'), CrudComponents::NULL_FILTER_VALUE] if field.filter_includes_null? %>
|
|
4
|
+
<%= select_tag param_name,
|
|
5
|
+
options_for_select(choices, query.value(field.name.to_s)),
|
|
6
|
+
class: compact ? css.select_input_sm : css.select_input, form: form_id, id: nil,
|
|
7
|
+
data: (autosubmit ? { action: 'change->crud-filter#submit' } : {}),
|
|
8
|
+
aria: { label: field.human_name } %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# Locals: field, query, form_id, compact, autosubmit, param_name, css %>
|
|
2
|
+
<%= tag.input type: 'search', name: param_name, value: query.value(field.name.to_s),
|
|
3
|
+
class: compact ? css.input_sm : css.input, form: form_id,
|
|
4
|
+
placeholder: (field.human_name if compact),
|
|
5
|
+
aria: { label: field.human_name } %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%# Form input for a boolean column. Locals: f, field, form.
|
|
2
|
+
A non-null column is a plain checkbox. A nullable column becomes a 3-state
|
|
3
|
+
select (Yes / No / not set), so the absence of a value is expressible — the
|
|
4
|
+
blank option submits "" and persists NULL. %>
|
|
5
|
+
<% if field.nullable? %>
|
|
6
|
+
<%= f.input field.name, as: :select,
|
|
7
|
+
collection: [[t('crud_components.filter.yes', default: 'Yes'), true],
|
|
8
|
+
[t('crud_components.filter.no', default: 'No'), false]],
|
|
9
|
+
include_blank: t('crud_components.filter.not_set', default: 'Not set') %>
|
|
10
|
+
<% else %>
|
|
11
|
+
<%= f.input field.name, as: :boolean %>
|
|
12
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# Form input for an enum column. Locals: f, field, form.
|
|
2
|
+
A nullable column gets a "not set" blank option so NULL stays expressible. %>
|
|
3
|
+
<% if field.nullable? %>
|
|
4
|
+
<%= f.input field.name, collection: field.form_choices,
|
|
5
|
+
include_blank: t('crud_components.filter.not_set', default: 'Not set') %>
|
|
6
|
+
<% else %>
|
|
7
|
+
<%= f.input field.name, collection: field.form_choices %>
|
|
8
|
+
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<%# Form input for an Active Storage attachment. Locals: f, field, form.
|
|
2
|
+
Keep / add / remove all flow through the standard permit list (`:cover` /
|
|
3
|
+
`{ images: [] }`) with no controller code, relying on one fact: an empty
|
|
4
|
+
file input submits nothing, so leaving it empty keeps the current file(s).
|
|
5
|
+
|
|
6
|
+
has_one : choose a file to replace; tick "Remove" to delete (submits "").
|
|
7
|
+
has_many: each existing file has a "Keep" checkbox carrying its signed_id
|
|
8
|
+
(untick to remove); the file input adds. A hidden blank keeps the
|
|
9
|
+
array present so unticking everything actually clears it. %>
|
|
10
|
+
<% name = field.name %>
|
|
11
|
+
<% many = field.respond_to?(:many?) && field.many? %>
|
|
12
|
+
<% value = field.value(form.record) %>
|
|
13
|
+
<% attachments = value.respond_to?(:attached?) && value.attached? ? Array(many ? value.to_a : value) : [] %>
|
|
14
|
+
<div class="mb-3 crud-attachment-field">
|
|
15
|
+
<%= f.label name, class: form.css.form_label %>
|
|
16
|
+
|
|
17
|
+
<% if many %>
|
|
18
|
+
<%= hidden_field_tag "#{f.object_name}[#{name}][]", '', id: nil %>
|
|
19
|
+
<% if attachments.any? %>
|
|
20
|
+
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
21
|
+
<% attachments.each do |attachment| %>
|
|
22
|
+
<label class="d-flex flex-column align-items-center gap-1 border rounded p-1">
|
|
23
|
+
<%= render 'crud_components/fields/attachment_thumb', attachment: attachment, surface: :collection %>
|
|
24
|
+
<span class="form-check small mb-0">
|
|
25
|
+
<%= check_box_tag "#{f.object_name}[#{name}][]", attachment.signed_id, true, id: nil, class: 'form-check-input' %>
|
|
26
|
+
<%= t('crud_components.attachment.keep', default: 'Keep') %>
|
|
27
|
+
</span>
|
|
28
|
+
</label>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
<%# f.file_field (not file_field_tag) so simple_form marks the form multipart %>
|
|
33
|
+
<%= f.file_field name, multiple: true, class: form.css.input %>
|
|
34
|
+
<% else %>
|
|
35
|
+
<% attachment = attachments.first %>
|
|
36
|
+
<% if attachment %>
|
|
37
|
+
<div class="mb-2"><%= render 'crud_components/fields/attachment_thumb', attachment: attachment, surface: :record %></div>
|
|
38
|
+
<span class="form-check small mb-1">
|
|
39
|
+
<%# value "" purges on submit; rendered before the file input so an upload still wins %>
|
|
40
|
+
<%= check_box_tag "#{f.object_name}[#{name}]", '', false, id: nil, class: 'form-check-input' %>
|
|
41
|
+
<%= t('crud_components.attachment.remove', default: 'Remove') %>
|
|
42
|
+
</span>
|
|
43
|
+
<% end %>
|
|
44
|
+
<%# f.file_field (not file_field_tag) so simple_form marks the form multipart %>
|
|
45
|
+
<%= f.file_field name, class: form.css.input %>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# Form input for a habtm / has_many(:through) association. Locals: f, field,
|
|
2
|
+
form. A no-JS multiselect baseline carrying the optional `crud-multiselect`
|
|
3
|
+
Stimulus hook (chips + add picker); without JS the multiselect stands. %>
|
|
4
|
+
<%= f.association field.reflection.name, as: :select, collection: field.form_choices,
|
|
5
|
+
input_html: { multiple: true, data: { controller: 'crud-multiselect' } } %>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<%# The default collection layout. Receives one local: `collection`
|
|
2
|
+
(CrudComponents::Presenters::Collection). %>
|
|
3
|
+
<% css = collection.css %>
|
|
4
|
+
<div class="crud-collection" data-controller="crud-filter<%= ' crud-select' if collection.selectable? %>">
|
|
5
|
+
<% if collection.filterable? %>
|
|
6
|
+
<%# The inline filter row lives inside <thead>; a form cannot wrap table
|
|
7
|
+
cells, so the inputs bind to this external form via the HTML `form`
|
|
8
|
+
attribute. Plain GET — works without JavaScript. %>
|
|
9
|
+
<form method="get" action="<%= request.path %>" id="<%= collection.filter_form_id %>"
|
|
10
|
+
data-turbo-action="advance" data-crud-filter-target="form"
|
|
11
|
+
data-action="submit->crud-filter#clean">
|
|
12
|
+
<% collection.preserved_params.each do |name, value| %>
|
|
13
|
+
<%= hidden_field_tag name, value, id: nil %>
|
|
14
|
+
<% end %>
|
|
15
|
+
</form>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if collection.selectable? %>
|
|
18
|
+
<%# Selection form: the row checkboxes bind to it via the HTML `form`
|
|
19
|
+
attribute (same trick as the filter row); the toolbar's bulk-action
|
|
20
|
+
buttons submit it with per-button formaction/formmethod. A *global* CSRF
|
|
21
|
+
token (not form_tag's per-form one) so it stays valid for whichever
|
|
22
|
+
action a button posts to. Plain POST — works without JavaScript. %>
|
|
23
|
+
<form id="<%= collection.select_form_id %>" method="post" action="<%= request.path %>">
|
|
24
|
+
<%= hidden_field_tag 'authenticity_token', form_authenticity_token, id: nil %>
|
|
25
|
+
</form>
|
|
26
|
+
<% end %>
|
|
27
|
+
|
|
28
|
+
<div class="table-responsive">
|
|
29
|
+
<table class="<%= css.table %>">
|
|
30
|
+
<thead class="<%= css.thead %>">
|
|
31
|
+
<% if collection.show_toolbar? %>
|
|
32
|
+
<%# Search + collection actions, seated in the table head (a full-width
|
|
33
|
+
cell) so they read as part of the table rather than floating above. %>
|
|
34
|
+
<tr>
|
|
35
|
+
<th colspan="<%= collection.columns_count %>" class="crud-toolbar-cell">
|
|
36
|
+
<%= render 'crud_components/toolbar', collection: collection %>
|
|
37
|
+
</th>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
<tr>
|
|
41
|
+
<% if collection.selectable? %>
|
|
42
|
+
<th scope="col" class="crud-select-cell">
|
|
43
|
+
<input type="checkbox" class="form-check-input" aria-label="Select all"
|
|
44
|
+
data-crud-select-target="all" data-action="change->crud-select#toggleAll">
|
|
45
|
+
</th>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% collection.fields.each do |field| %>
|
|
48
|
+
<th scope="col"><%= render 'crud_components/column_header', collection: collection, field: field %></th>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% if collection.trailing_column? %>
|
|
51
|
+
<th scope="col" class="<%= css.actions_cell %>">
|
|
52
|
+
<% if collection.column_picker? %>
|
|
53
|
+
<%= render 'crud_components/column_picker', collection: collection, url: request.path %>
|
|
54
|
+
<% end %>
|
|
55
|
+
</th>
|
|
56
|
+
<% end %>
|
|
57
|
+
</tr>
|
|
58
|
+
<% if collection.filterable? %>
|
|
59
|
+
<tr class="<%= css.filter_row %>">
|
|
60
|
+
<% if collection.selectable? %><td class="crud-select-cell"></td><% end %>
|
|
61
|
+
<% collection.fields.each do |field| %>
|
|
62
|
+
<td>
|
|
63
|
+
<% if collection.filterable_field?(field) %>
|
|
64
|
+
<%= collection.render_filter_control(field, collection.query,
|
|
65
|
+
form_id: collection.filter_form_id,
|
|
66
|
+
compact: true, autosubmit: true) %>
|
|
67
|
+
<% end %>
|
|
68
|
+
</td>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% if collection.trailing_column? %>
|
|
71
|
+
<td class="<%= css.actions_cell %>">
|
|
72
|
+
<div class="<%= css.button_group %>">
|
|
73
|
+
<button type="submit" form="<%= collection.filter_form_id %>" class="<%= css.button %>">
|
|
74
|
+
<%= t('crud_components.filter.submit', default: 'Filter') %>
|
|
75
|
+
</button>
|
|
76
|
+
<% if collection.filtered? %>
|
|
77
|
+
<%= link_to t('crud_components.filter.reset', default: 'Reset'),
|
|
78
|
+
collection.reset_url, class: css.button,
|
|
79
|
+
title: t('crud_components.filter.reset', default: 'Reset'),
|
|
80
|
+
data: { turbo_action: 'advance' } %>
|
|
81
|
+
<% end %>
|
|
82
|
+
</div>
|
|
83
|
+
</td>
|
|
84
|
+
<% end %>
|
|
85
|
+
</tr>
|
|
86
|
+
<% end %>
|
|
87
|
+
</thead>
|
|
88
|
+
<tbody>
|
|
89
|
+
<% if collection.grouped? %>
|
|
90
|
+
<% collection.groups.each do |group| %>
|
|
91
|
+
<tr class="crud-group-row">
|
|
92
|
+
<td colspan="<%= collection.columns_count %>">
|
|
93
|
+
<span class="d-inline-flex align-items-center gap-2">
|
|
94
|
+
<% if collection.selectable? && collection.group_open?(group) %>
|
|
95
|
+
<input type="checkbox" class="form-check-input" aria-label="Select group"
|
|
96
|
+
data-crud-select-target="group" data-group="<%= group.key %>"
|
|
97
|
+
data-action="change->crud-select#toggleGroup">
|
|
98
|
+
<% end %>
|
|
99
|
+
<%# turbo_action: replace (not advance) so toggling ?open= counts as a
|
|
100
|
+
Turbo page refresh — same pathname + replace → morph (only the
|
|
101
|
+
affected rows change, scroll preserved per the layout's
|
|
102
|
+
turbo-refresh-* metas). Collapsed rows stay unrendered. No JS
|
|
103
|
+
needed; without Turbo this is a plain GET that reloads as before. %>
|
|
104
|
+
<%= link_to collection.group_toggle_url(group), data: { turbo_action: 'replace' },
|
|
105
|
+
class: 'text-reset text-decoration-none d-inline-flex align-items-center gap-2' do %>
|
|
106
|
+
<i class="<%= css.icon_prefix %><%= collection.group_open?(group) ? 'chevron-down' : 'chevron-right' %>"></i>
|
|
107
|
+
<span class="fw-medium"><%= group.label %></span>
|
|
108
|
+
<span class="<%= css.badge_muted %>"><%= group.count %></span>
|
|
109
|
+
<% end %>
|
|
110
|
+
</span>
|
|
111
|
+
</td>
|
|
112
|
+
</tr>
|
|
113
|
+
<% if collection.group_open?(group) %>
|
|
114
|
+
<% group.records.each do |record| %>
|
|
115
|
+
<%= render 'crud_components/row', collection: collection, record: record, css: css, group: group %>
|
|
116
|
+
<% end %>
|
|
117
|
+
<% end %>
|
|
118
|
+
<% end %>
|
|
119
|
+
<% else %>
|
|
120
|
+
<% collection.records.each do |record| %>
|
|
121
|
+
<%= render 'crud_components/row', collection: collection, record: record, css: css %>
|
|
122
|
+
<% end %>
|
|
123
|
+
<% end %>
|
|
124
|
+
<% if collection.records.empty? %>
|
|
125
|
+
<tr>
|
|
126
|
+
<td colspan="<%= collection.columns_count %>" class="<%= css.muted %>">
|
|
127
|
+
<%= t('crud_components.empty', default: 'No records') %>
|
|
128
|
+
</td>
|
|
129
|
+
</tr>
|
|
130
|
+
<% end %>
|
|
131
|
+
</tbody>
|
|
132
|
+
<% if collection.show_pager? %>
|
|
133
|
+
<tfoot>
|
|
134
|
+
<tr>
|
|
135
|
+
<td colspan="<%= collection.columns_count %>" class="crud-pager-cell">
|
|
136
|
+
<%= render 'crud_components/pager', collection: collection %>
|
|
137
|
+
</td>
|
|
138
|
+
</tr>
|
|
139
|
+
</tfoot>
|
|
140
|
+
<% end %>
|
|
141
|
+
</table>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Deutsche Standardtexte für die eingebaute Oberfläche des Gems. Jede Abfrage
|
|
2
|
+
# übergibt zusätzlich einen Inline-Default, daher sind diese Schlüssel optional
|
|
3
|
+
# und jederzeit in der eigenen config/locales/*.yml überschreibbar.
|
|
4
|
+
de:
|
|
5
|
+
crud_components:
|
|
6
|
+
confirm: "Sind Sie sicher?"
|
|
7
|
+
empty: "Keine Einträge"
|
|
8
|
+
more: "+%{count} weitere"
|
|
9
|
+
sort: "Sortieren"
|
|
10
|
+
filter_by: "Nach %{name} filtern"
|
|
11
|
+
columns:
|
|
12
|
+
edit: "Spalten"
|
|
13
|
+
apply: "Anwenden"
|
|
14
|
+
reset: "Zurücksetzen"
|
|
15
|
+
legend: "Spalten anzeigen"
|
|
16
|
+
attachment:
|
|
17
|
+
keep: "Behalten"
|
|
18
|
+
remove: "Entfernen"
|
|
19
|
+
actions:
|
|
20
|
+
new: "Neu"
|
|
21
|
+
show: "Anzeigen"
|
|
22
|
+
edit: "Bearbeiten"
|
|
23
|
+
destroy: "Löschen"
|
|
24
|
+
filter:
|
|
25
|
+
any: "–"
|
|
26
|
+
not_set: "Nicht gesetzt"
|
|
27
|
+
"yes": "Ja"
|
|
28
|
+
"no": "Nein"
|
|
29
|
+
search: "Suche"
|
|
30
|
+
apply: "Anwenden"
|
|
31
|
+
submit: "Filtern"
|
|
32
|
+
reset: "Zurücksetzen"
|
|
33
|
+
pager:
|
|
34
|
+
label: "Seitennummerierung"
|
|
35
|
+
summary: "Seite %{page} von %{total} · %{count} gesamt"
|
|
36
|
+
previous: "‹"
|
|
37
|
+
next: "›"
|
|
38
|
+
previous_label: "Zurück"
|
|
39
|
+
next_label: "Weiter"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Default strings for the gem's built-in UI. Every lookup also passes an inline
|
|
2
|
+
# default, so these are overridable in your app and never required — they just
|
|
3
|
+
# spare you from translating the defaults yourself. Override any key by defining
|
|
4
|
+
# it in your own config/locales/*.yml.
|
|
5
|
+
en:
|
|
6
|
+
crud_components:
|
|
7
|
+
confirm: "Are you sure?"
|
|
8
|
+
empty: "No records"
|
|
9
|
+
more: "+%{count} more"
|
|
10
|
+
sort: "Sort"
|
|
11
|
+
filter_by: "Filter by %{name}"
|
|
12
|
+
columns:
|
|
13
|
+
edit: "Columns"
|
|
14
|
+
apply: "Apply"
|
|
15
|
+
reset: "Reset"
|
|
16
|
+
legend: "Show columns"
|
|
17
|
+
attachment:
|
|
18
|
+
keep: "Keep"
|
|
19
|
+
remove: "Remove"
|
|
20
|
+
actions:
|
|
21
|
+
new: "New"
|
|
22
|
+
show: "Show"
|
|
23
|
+
edit: "Edit"
|
|
24
|
+
destroy: "Delete"
|
|
25
|
+
filter:
|
|
26
|
+
any: "–"
|
|
27
|
+
not_set: "Not set"
|
|
28
|
+
"yes": "Yes"
|
|
29
|
+
"no": "No"
|
|
30
|
+
search: "Search"
|
|
31
|
+
apply: "Apply"
|
|
32
|
+
submit: "Filter"
|
|
33
|
+
reset: "Reset"
|
|
34
|
+
pager:
|
|
35
|
+
label: "Pagination"
|
|
36
|
+
summary: "Page %{page} of %{total} · %{count} total"
|
|
37
|
+
previous: "‹"
|
|
38
|
+
next: "›"
|
|
39
|
+
previous_label: "Previous"
|
|
40
|
+
next_label: "Next"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative 'lib/crud_components/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = 'crud_components'
|
|
5
|
+
spec.version = CrudComponents::VERSION
|
|
6
|
+
spec.authors = ['Anatoly Zelenin']
|
|
7
|
+
spec.email = ['anatoly@zelenin.de']
|
|
8
|
+
|
|
9
|
+
spec.summary = 'Declarative CRUD UI for ActiveRecord models, rendered inside your app'
|
|
10
|
+
spec.description = 'Tables, record views and filter forms derived from what Rails ' \
|
|
11
|
+
'already knows about your models. Zero configuration works; ' \
|
|
12
|
+
'declarations only improve. Forms render through simple_form; ' \
|
|
13
|
+
'nothing beyond Rails is otherwise required.'
|
|
14
|
+
spec.homepage = 'https://github.com/itadventurer/crud_components'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
# 3.2 floor: the lib itself only needs 3.1-era syntax, but Ruby 3.1 is EOL and
|
|
17
|
+
# i18n (>= 1.15, pulled transitively by Rails) uses Fiber storage that needs
|
|
18
|
+
# 3.2+. The CI matrix proves Ruby 3.2–3.4.
|
|
19
|
+
spec.required_ruby_version = '>= 3.2'
|
|
20
|
+
|
|
21
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
22
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
23
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
24
|
+
|
|
25
|
+
# Prefer git, but fall back to a glob so the gem (and the demo image) build
|
|
26
|
+
# without git or a .git directory. Excludes tests, the dummy, CI and demo-deploy
|
|
27
|
+
# artifacts — none ship in the gem. (`git` is absent in the slim demo image, so
|
|
28
|
+
# the backtick is rescued — Errno::ENOENT otherwise aborts the build.)
|
|
29
|
+
tracked = begin
|
|
30
|
+
`git ls-files -z`.split("\x0")
|
|
31
|
+
rescue StandardError
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
tracked = Dir.glob('**/*', File::FNM_DOTMATCH).select { |f| File.file?(f) } if tracked.empty?
|
|
35
|
+
spec.files = tracked.reject do |f|
|
|
36
|
+
f.start_with?('test/', 'script/', '.github/', 'docs/screenshots/', 'deploy/') ||
|
|
37
|
+
%w[AGENTS.md Dockerfile .dockerignore].include?(f)
|
|
38
|
+
end
|
|
39
|
+
spec.require_paths = ['lib']
|
|
40
|
+
|
|
41
|
+
spec.add_dependency 'actionview', '>= 7.1'
|
|
42
|
+
spec.add_dependency 'activerecord', '>= 7.1'
|
|
43
|
+
spec.add_dependency 'activesupport', '>= 7.1'
|
|
44
|
+
# Form rendering. simple_form's wrappers are the standard way to make form
|
|
45
|
+
# markup match a design system (Bootstrap/Tailwind/Bulma/…); deferring to it
|
|
46
|
+
# is less code and a better fit than reinventing wrappers. Light + ubiquitous.
|
|
47
|
+
spec.add_dependency 'simple_form', '>= 5.0'
|
|
48
|
+
end
|