b4um_generators 0.2.17 → 0.3.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 +4 -4
- data/README.de.md +81 -1
- data/README.md +76 -1
- data/lib/b4um_generators/version.rb +1 -1
- data/lib/generators/b4um/help/help_generator.rb +66 -0
- data/lib/generators/b4um/install/templates/b4um/sortable.css +158 -0
- data/lib/generators/b4um/install/templates/b4um.css +1 -0
- data/lib/generators/b4um/pagination/pagination_generator.rb +32 -5
- data/lib/generators/b4um/search/search_generator.rb +22 -8
- data/lib/generators/b4um/sortable/sortable_generator.rb +529 -0
- data/lib/generators/b4um/sortable/templates/_sortable.html.erb.tt +40 -0
- data/lib/generators/b4um/sortable/templates/add_position_migration.rb.tt +26 -0
- data/lib/generators/b4um/sortable/templates/sort.html.erb.tt +3 -0
- data/lib/generators/b4um/sortable/templates/sortable_controller.js +166 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 167af7d5ad10e01abf428bf058e8d0ebc1b85e7791e43882dd1c53966d1ef89f
|
|
4
|
+
data.tar.gz: 715da7ed28265ec0f4451684dd35d375c749fbb8eb7f06b915a13e8bbadea26b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2478f3db2a6f42ca3ca83158f86d9313676b5263c07b324f69d786b587ed603ab5af6a2319d123e7524e4cab508530431a161c18ed3e5e1deea50cd581f90a4f
|
|
7
|
+
data.tar.gz: 7c874c56a5057bbb5110b683ae3cdfbfd84c4471f5f40cc96e2f53fb1ca32843df64f8f5dde7ba3614274cad19e3fa33e5e563c217b9d46394f4e5dbbbdb9c16
|
data/README.de.md
CHANGED
|
@@ -732,6 +732,86 @@ Die Suche bietet:
|
|
|
732
732
|
- Kein zusätzliches Such-Gem erforderlich
|
|
733
733
|
- Sichere wiederholte Ausführung des Generators
|
|
734
734
|
|
|
735
|
+
## Sortable
|
|
736
|
+
|
|
737
|
+
Füge einer bestehenden Ressource eine dauerhaft gespeicherte Sortierreihenfolge hinzu:
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
bin/rails generate b4um:sortable Product
|
|
741
|
+
bin/rails db:migrate
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Der Generator fügt eine `position`-Spalte hinzu und weist bestehenden Datensätzen beim Ausführen der generierten
|
|
745
|
+
Migration automatisch Positionen zu.
|
|
746
|
+
|
|
747
|
+
Neue Datensätze werden automatisch am Ende der Liste eingefügt.
|
|
748
|
+
|
|
749
|
+
Standardmäßig wird die bestehende Index-Seite zur sortierbaren Seite:
|
|
750
|
+
|
|
751
|
+
```text
|
|
752
|
+
/products
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Das bestehende Index-Layout wird auf das b4um-Sortable-Layout umgestellt. Die gespeicherte Position wird außerdem als
|
|
756
|
+
Standardsortierung der Ressource verwendet.
|
|
757
|
+
|
|
758
|
+
Sortable bietet:
|
|
759
|
+
|
|
760
|
+
- Dauerhaft in der Datenbank gespeicherte ganzzahlige Positionen
|
|
761
|
+
- Automatische Positionierung bestehender Datensätze während der Migration
|
|
762
|
+
- Automatische Positionierung neuer Datensätze am Ende
|
|
763
|
+
- Drag-and-Drop-Sortierung mit Stimulus
|
|
764
|
+
- Direkte Änderung der Position über ein Zahlenfeld
|
|
765
|
+
- Automatisches Verschieben der betroffenen Datensätze
|
|
766
|
+
- Lückenlose Positionen nach dem Verschieben oder Löschen von Datensätzen
|
|
767
|
+
- Serverseitige Sortierung innerhalb einer Datenbanktransaktion
|
|
768
|
+
- CSRF-geschützte Update-Anfragen
|
|
769
|
+
- Integration mit b4um Search und Pagination
|
|
770
|
+
- Kein zusätzliches Sortier-Gem erforderlich
|
|
771
|
+
- Sichere wiederholte Ausführung des Generators
|
|
772
|
+
|
|
773
|
+
### Separate Sortierseite
|
|
774
|
+
|
|
775
|
+
Mit `--path` kann eine separate Sortierseite erstellt werden:
|
|
776
|
+
|
|
777
|
+
```bash
|
|
778
|
+
bin/rails generate b4um:sortable Product --path=sortierung
|
|
779
|
+
bin/rails db:migrate
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Dadurch wird die sortierbare Seite genau unter folgender URL erstellt:
|
|
783
|
+
|
|
784
|
+
```text
|
|
785
|
+
/sortierung
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
während der normale Ressourcen-Index weiterhin unter
|
|
789
|
+
|
|
790
|
+
```text
|
|
791
|
+
/products
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
verfügbar bleibt.
|
|
795
|
+
|
|
796
|
+
Das bestehende Index-Layout, beispielsweise Bento oder Table, bleibt unverändert. Es verwendet weiterhin die
|
|
797
|
+
gespeicherte `position`-Sortierung.
|
|
798
|
+
|
|
799
|
+
Die separate Sortierseite verwendet den bestehenden Controller der Ressource. Es wird kein zusätzlicher Controller und
|
|
800
|
+
kein Namespace erzeugt.
|
|
801
|
+
|
|
802
|
+
Der mit `--path` angegebene Wert bestimmt direkt die URL der separaten Sortierseite. Zum Beispiel:
|
|
803
|
+
|
|
804
|
+
```bash
|
|
805
|
+
bin/rails generate b4um:sortable Product --path=produktuebersicht
|
|
806
|
+
bin/rails generate b4um:sortable Product --path=verwaltung
|
|
807
|
+
bin/rails generate b4um:sortable Product --path=admin
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
Dadurch entstehen entsprechend `/produktuebersicht`, `/verwaltung` und `/admin`.
|
|
811
|
+
|
|
812
|
+
Eine separate Sortierseite ermöglicht außerdem, nur die `sort`-Action durch die Authentifizierung zu schützen, während
|
|
813
|
+
der normale Ressourcen-Index öffentlich bleiben kann.
|
|
814
|
+
|
|
735
815
|
## Kommentare
|
|
736
816
|
|
|
737
817
|
Füge einem bestehenden Modell polymorphe Kommentare hinzu:
|
|
@@ -1135,7 +1215,7 @@ wird.
|
|
|
1135
1215
|
|
|
1136
1216
|
## Version
|
|
1137
1217
|
|
|
1138
|
-
Aktuelle Version: `0.
|
|
1218
|
+
Aktuelle Version: `0.3.0`
|
|
1139
1219
|
|
|
1140
1220
|
## Autor
|
|
1141
1221
|
|
data/README.md
CHANGED
|
@@ -725,6 +725,81 @@ Search provides:
|
|
|
725
725
|
- No additional search gem
|
|
726
726
|
- Safe repeated generator runs
|
|
727
727
|
|
|
728
|
+
## Sortable
|
|
729
|
+
|
|
730
|
+
Add persistent sortable positioning to an existing resource:
|
|
731
|
+
|
|
732
|
+
```bash
|
|
733
|
+
bin/rails generate b4um:sortable Product
|
|
734
|
+
bin/rails db:migrate
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
The generator adds a `position` column and assigns positions to existing records when the generated migration is run.
|
|
738
|
+
|
|
739
|
+
New records are automatically added at the end of the list.
|
|
740
|
+
|
|
741
|
+
By default, the existing resource index becomes the sortable page:
|
|
742
|
+
|
|
743
|
+
```text
|
|
744
|
+
/products
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
The existing index layout is switched to the b4um sortable layout. The stored position is also used as the default
|
|
748
|
+
ordering for the resource.
|
|
749
|
+
|
|
750
|
+
Sortable provides:
|
|
751
|
+
|
|
752
|
+
- Persistent integer positions stored in the database
|
|
753
|
+
- Automatic positioning of existing records during migration
|
|
754
|
+
- Automatic positioning of new records at the end
|
|
755
|
+
- Drag-and-drop sorting with Stimulus
|
|
756
|
+
- Direct position changes using a number field
|
|
757
|
+
- Automatic shifting of affected records
|
|
758
|
+
- Contiguous positions after moving or deleting records
|
|
759
|
+
- Server-side sorting inside a database transaction
|
|
760
|
+
- CSRF-protected update requests
|
|
761
|
+
- Integration with b4um Search and Pagination
|
|
762
|
+
- No additional sorting gem
|
|
763
|
+
- Safe repeated generator runs
|
|
764
|
+
|
|
765
|
+
### Separate Sortable Page
|
|
766
|
+
|
|
767
|
+
A separate sortable page can be created with `--path`:
|
|
768
|
+
|
|
769
|
+
```bash
|
|
770
|
+
bin/rails generate b4um:sortable Product --path=sortierung
|
|
771
|
+
bin/rails db:migrate
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
This creates the sortable page at exactly:
|
|
775
|
+
|
|
776
|
+
```text
|
|
777
|
+
/sortierung
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
while the regular resource index remains available at:
|
|
781
|
+
|
|
782
|
+
```text
|
|
783
|
+
/products
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
The existing index layout, such as Bento or Table, remains unchanged. It still uses the stored `position` ordering.
|
|
787
|
+
|
|
788
|
+
The separate sortable page uses the existing resource controller. No additional controller or namespace is generated.
|
|
789
|
+
|
|
790
|
+
The value passed to `--path` directly defines the URL of the separate sortable page. For example:
|
|
791
|
+
|
|
792
|
+
```bash
|
|
793
|
+
bin/rails generate b4um:sortable Product --path=produktuebersicht
|
|
794
|
+
bin/rails generate b4um:sortable Product --path=verwaltung
|
|
795
|
+
bin/rails generate b4um:sortable Product --path=admin
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
These create `/produktuebersicht`, `/verwaltung`, and `/admin` respectively.
|
|
799
|
+
|
|
800
|
+
Using a separate sortable page also makes it possible to protect only the `sort` action with authentication while
|
|
801
|
+
keeping the regular resource index public.
|
|
802
|
+
|
|
728
803
|
## Comments
|
|
729
804
|
|
|
730
805
|
Add polymorphic comments to an existing model:
|
|
@@ -1124,7 +1199,7 @@ The gem can be tested locally by referencing the repository with `path:` from a
|
|
|
1124
1199
|
|
|
1125
1200
|
## Version
|
|
1126
1201
|
|
|
1127
|
-
Current version: `0.
|
|
1202
|
+
Current version: `0.3.0`
|
|
1128
1203
|
|
|
1129
1204
|
## Author
|
|
1130
1205
|
|
|
@@ -943,6 +943,72 @@ module B4um
|
|
|
943
943
|
- No additional search gem required
|
|
944
944
|
- Safe repeated generator runs
|
|
945
945
|
|
|
946
|
+
SORTABLE
|
|
947
|
+
|
|
948
|
+
bin/rails generate b4um:sortable MODEL
|
|
949
|
+
|
|
950
|
+
Adds persistent sortable positioning to an existing resource.
|
|
951
|
+
|
|
952
|
+
Example:
|
|
953
|
+
|
|
954
|
+
bin/rails generate b4um:sortable Product
|
|
955
|
+
bin/rails db:migrate
|
|
956
|
+
|
|
957
|
+
Existing records receive positions automatically when the
|
|
958
|
+
generated migration is run.
|
|
959
|
+
|
|
960
|
+
New records are automatically added at the end of the list.
|
|
961
|
+
|
|
962
|
+
By default, the existing index page becomes the sortable page:
|
|
963
|
+
|
|
964
|
+
/products
|
|
965
|
+
|
|
966
|
+
The existing index layout is switched to the b4um sortable
|
|
967
|
+
layout. The stored position is also used as the default
|
|
968
|
+
ordering for the resource.
|
|
969
|
+
|
|
970
|
+
A separate sortable page can be created with --path:
|
|
971
|
+
|
|
972
|
+
bin/rails generate b4um:sortable Product --path=sortierung
|
|
973
|
+
bin/rails db:migrate
|
|
974
|
+
|
|
975
|
+
This creates the sortable page at exactly:
|
|
976
|
+
|
|
977
|
+
/sortierung
|
|
978
|
+
|
|
979
|
+
The existing resource index remains unchanged:
|
|
980
|
+
|
|
981
|
+
/products
|
|
982
|
+
|
|
983
|
+
The separate page uses the existing resource controller.
|
|
984
|
+
No additional controller or namespace is generated.
|
|
985
|
+
|
|
986
|
+
Other examples:
|
|
987
|
+
|
|
988
|
+
--path=produktuebersicht
|
|
989
|
+
--path=verwaltung
|
|
990
|
+
--path=admin
|
|
991
|
+
|
|
992
|
+
The value passed to --path defines the URL of the separate
|
|
993
|
+
sortable page.
|
|
994
|
+
|
|
995
|
+
Sortable features:
|
|
996
|
+
|
|
997
|
+
- Persistent integer positions stored in the database
|
|
998
|
+
- Existing records receive positions during migration
|
|
999
|
+
- New records are automatically placed at the end
|
|
1000
|
+
- Drag-and-drop sorting with Stimulus
|
|
1001
|
+
- Positions can also be changed directly with a number field
|
|
1002
|
+
- Moving a record automatically shifts the affected records
|
|
1003
|
+
- Positions remain contiguous after moving or deleting records
|
|
1004
|
+
- Server-side sorting inside a database transaction
|
|
1005
|
+
- CSRF-protected update requests
|
|
1006
|
+
- No additional sorting gem required
|
|
1007
|
+
- Works with b4um Search and Pagination
|
|
1008
|
+
- Stored ordering is reflected by the regular resource index
|
|
1009
|
+
- Optional separate sortable page with --path
|
|
1010
|
+
- Safe repeated generator runs
|
|
1011
|
+
|
|
946
1012
|
|
|
947
1013
|
COMMENTS
|
|
948
1014
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* -------------------------------------------------------
|
|
2
|
+
Sortable
|
|
3
|
+
------------------------------------------------------- */
|
|
4
|
+
|
|
5
|
+
.b4um-sortable {
|
|
6
|
+
display: grid;
|
|
7
|
+
gap: 0.75rem;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.b4um-sortable__item {
|
|
11
|
+
display: grid;
|
|
12
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 1.25rem;
|
|
15
|
+
|
|
16
|
+
padding: 1rem 1.25rem;
|
|
17
|
+
|
|
18
|
+
background: var(--b4um-surface);
|
|
19
|
+
border: 1px solid var(--b4um-border-light);
|
|
20
|
+
border-radius: var(--b4um-radius);
|
|
21
|
+
|
|
22
|
+
transition:
|
|
23
|
+
border-color 0.2s ease,
|
|
24
|
+
box-shadow 0.2s ease,
|
|
25
|
+
opacity 0.2s ease;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.b4um-sortable__item:hover {
|
|
29
|
+
border-color: var(--b4um-border-hover);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.b4um-sortable__item--dragging {
|
|
33
|
+
opacity: 0.5;
|
|
34
|
+
box-shadow: 0 8px 24px var(--b4um-primary-shadow-soft);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* -------------------------------------------------------
|
|
38
|
+
Controls
|
|
39
|
+
------------------------------------------------------- */
|
|
40
|
+
|
|
41
|
+
.b4um-sortable__controls {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 0.5rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.b4um-sortable__position {
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
|
|
50
|
+
width: 4rem;
|
|
51
|
+
min-width: 4rem;
|
|
52
|
+
height: 2.75rem;
|
|
53
|
+
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 0.5rem;
|
|
56
|
+
|
|
57
|
+
background: var(--b4um-surface);
|
|
58
|
+
color: var(--b4um-text);
|
|
59
|
+
|
|
60
|
+
border: 1px solid var(--b4um-border);
|
|
61
|
+
border-radius: 10px;
|
|
62
|
+
|
|
63
|
+
font: inherit;
|
|
64
|
+
text-align: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.b4um-sortable__position:hover {
|
|
68
|
+
border-color: var(--b4um-border-hover);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.b4um-sortable__position:focus {
|
|
72
|
+
border-color: var(--b4um-primary);
|
|
73
|
+
outline: 3px solid var(--b4um-primary-focus);
|
|
74
|
+
outline-offset: 1px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* -------------------------------------------------------
|
|
78
|
+
Drag handle
|
|
79
|
+
------------------------------------------------------- */
|
|
80
|
+
|
|
81
|
+
.b4um-sortable__handle {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
|
|
86
|
+
width: 2.75rem;
|
|
87
|
+
height: 2.75rem;
|
|
88
|
+
|
|
89
|
+
margin: 0;
|
|
90
|
+
padding: 0;
|
|
91
|
+
|
|
92
|
+
background: transparent;
|
|
93
|
+
color: var(--b4um-text-secondary);
|
|
94
|
+
|
|
95
|
+
border: 0;
|
|
96
|
+
border-radius: 10px;
|
|
97
|
+
|
|
98
|
+
font: inherit;
|
|
99
|
+
font-size: 1.2rem;
|
|
100
|
+
line-height: 1;
|
|
101
|
+
|
|
102
|
+
cursor: grab;
|
|
103
|
+
|
|
104
|
+
transition:
|
|
105
|
+
background 0.2s ease,
|
|
106
|
+
color 0.2s ease;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.b4um-sortable__handle:hover {
|
|
110
|
+
background: var(--b4um-primary-soft);
|
|
111
|
+
color: var(--b4um-primary);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.b4um-sortable__handle:active {
|
|
115
|
+
cursor: grabbing;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* -------------------------------------------------------
|
|
119
|
+
Content
|
|
120
|
+
------------------------------------------------------- */
|
|
121
|
+
|
|
122
|
+
.b4um-sortable__content {
|
|
123
|
+
min-width: 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.b4um-sortable__content .resource {
|
|
127
|
+
margin: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* -------------------------------------------------------
|
|
131
|
+
Actions
|
|
132
|
+
------------------------------------------------------- */
|
|
133
|
+
|
|
134
|
+
.b4um-sortable__actions {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: flex-end;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.b4um-sortable__actions .button {
|
|
141
|
+
margin: 0;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* -------------------------------------------------------
|
|
146
|
+
Responsive
|
|
147
|
+
------------------------------------------------------- */
|
|
148
|
+
|
|
149
|
+
@media (max-width: 48rem) {
|
|
150
|
+
.b4um-sortable__item {
|
|
151
|
+
grid-template-columns: 1fr;
|
|
152
|
+
gap: 0.75rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.b4um-sortable__actions {
|
|
156
|
+
justify-content: flex-start;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -108,20 +108,33 @@ module B4um
|
|
|
108
108
|
/
|
|
109
109
|
@#{Regexp.escape(plural_table_name)}\s*=\s*
|
|
110
110
|
b4um_search\(\s*
|
|
111
|
-
|
|
111
|
+
(?<collection>
|
|
112
|
+
#{Regexp.escape(class_name)}
|
|
113
|
+
\.(?:all|order\(\s*:position\s*,\s*:id\s*\))
|
|
114
|
+
)\s*,\s*
|
|
112
115
|
params\[:q\]\s*
|
|
113
116
|
\)
|
|
114
117
|
/x
|
|
115
118
|
end
|
|
116
119
|
|
|
117
120
|
def paginate_existing_search
|
|
121
|
+
controller_content = File.read(controller_path)
|
|
122
|
+
match = controller_content.match(search_pattern)
|
|
123
|
+
|
|
124
|
+
unless match
|
|
125
|
+
raise Thor::Error,
|
|
126
|
+
"Could not find the search collection in app/controllers/#{plural_table_name}_controller.rb"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
collection = match[:collection]
|
|
130
|
+
|
|
118
131
|
gsub_file(
|
|
119
132
|
controller_path,
|
|
120
133
|
search_pattern,
|
|
121
134
|
[
|
|
122
135
|
"@#{plural_table_name}, @pagination = b4um_paginate(",
|
|
123
136
|
" b4um_search(",
|
|
124
|
-
" #{
|
|
137
|
+
" #{collection},",
|
|
125
138
|
" params[:q]",
|
|
126
139
|
" ),",
|
|
127
140
|
" per_page: #{options[:per_page]}",
|
|
@@ -131,19 +144,33 @@ module B4um
|
|
|
131
144
|
end
|
|
132
145
|
|
|
133
146
|
def paginate_plain_collection(controller_content)
|
|
134
|
-
|
|
147
|
+
plain_collection = "#{class_name}.all"
|
|
148
|
+
sortable_collection = "#{class_name}.order(:position, :id)"
|
|
135
149
|
|
|
136
|
-
|
|
150
|
+
collection =
|
|
151
|
+
if controller_content.include?(
|
|
152
|
+
"@#{plural_table_name} = #{sortable_collection}"
|
|
153
|
+
)
|
|
154
|
+
sortable_collection
|
|
155
|
+
elsif controller_content.include?(
|
|
156
|
+
"@#{plural_table_name} = #{plain_collection}"
|
|
157
|
+
)
|
|
158
|
+
plain_collection
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
unless collection
|
|
137
162
|
raise Thor::Error,
|
|
138
163
|
"Could not find the index collection in app/controllers/#{plural_table_name}_controller.rb"
|
|
139
164
|
end
|
|
140
165
|
|
|
166
|
+
old_index = "@#{plural_table_name} = #{collection}"
|
|
167
|
+
|
|
141
168
|
gsub_file(
|
|
142
169
|
controller_path,
|
|
143
170
|
old_index,
|
|
144
171
|
[
|
|
145
172
|
"@#{plural_table_name}, @pagination = b4um_paginate(",
|
|
146
|
-
" #{
|
|
173
|
+
" #{collection},",
|
|
147
174
|
" per_page: #{options[:per_page]}",
|
|
148
175
|
" )"
|
|
149
176
|
].join("\n")
|
|
@@ -92,19 +92,33 @@ module B4um
|
|
|
92
92
|
return
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
plain_collection = "#{class_name}.all"
|
|
96
|
+
sortable_collection = "#{class_name}.order(:position, :id)"
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
collection =
|
|
99
|
+
if controller_content.include?(
|
|
100
|
+
"@#{file_name.pluralize} = #{sortable_collection}"
|
|
101
|
+
)
|
|
102
|
+
sortable_collection
|
|
103
|
+
elsif controller_content.include?(
|
|
104
|
+
"@#{file_name.pluralize} = #{plain_collection}"
|
|
105
|
+
)
|
|
106
|
+
plain_collection
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
unless collection
|
|
98
110
|
raise Thor::Error,
|
|
99
111
|
"Could not find the index collection in #{controller_path}"
|
|
100
112
|
end
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
old_index = "@#{file_name.pluralize} = #{collection}"
|
|
115
|
+
|
|
116
|
+
search_code = [
|
|
117
|
+
"@#{file_name.pluralize} = b4um_search(",
|
|
118
|
+
" #{collection},",
|
|
119
|
+
" params[:q]",
|
|
120
|
+
" )"
|
|
121
|
+
].join("\n")
|
|
108
122
|
|
|
109
123
|
gsub_file(
|
|
110
124
|
controller_path,
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/named_base"
|
|
5
|
+
|
|
6
|
+
module B4um
|
|
7
|
+
module Generators
|
|
8
|
+
class SortableGenerator < Rails::Generators::NamedBase
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
class_option :path,
|
|
14
|
+
type: :string,
|
|
15
|
+
default: nil,
|
|
16
|
+
desc: "Creates a separate sortable page under the given path."
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def desc(_description = nil)
|
|
20
|
+
"Adds sortable positioning to an existing B4UM resource."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate_model
|
|
25
|
+
return if File.exist?(model_path)
|
|
26
|
+
|
|
27
|
+
raise Thor::Error,
|
|
28
|
+
"Model not found: app/models/#{file_name}.rb"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_controller
|
|
32
|
+
return if File.exist?(controller_path)
|
|
33
|
+
|
|
34
|
+
raise Thor::Error,
|
|
35
|
+
"Controller not found: app/controllers/#{plural_table_name}_controller.rb"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_index_view
|
|
39
|
+
return if File.exist?(index_view_path)
|
|
40
|
+
|
|
41
|
+
raise Thor::Error,
|
|
42
|
+
"Index view not found: app/views/#{plural_table_name}/index.html.erb"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_position_migration
|
|
46
|
+
return if position_column_exists?
|
|
47
|
+
|
|
48
|
+
migration_template(
|
|
49
|
+
"add_position_migration.rb.tt",
|
|
50
|
+
"db/migrate/add_position_to_#{plural_table_name}.rb"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def update_model
|
|
55
|
+
model_content = File.read(model_path)
|
|
56
|
+
|
|
57
|
+
return if model_content.include?("before_create :b4um_set_sortable_position")
|
|
58
|
+
|
|
59
|
+
inject_into_class(
|
|
60
|
+
model_path,
|
|
61
|
+
class_name,
|
|
62
|
+
<<~RUBY.indent(2)
|
|
63
|
+
before_create :b4um_set_sortable_position
|
|
64
|
+
after_destroy :b4um_compact_sortable_positions
|
|
65
|
+
|
|
66
|
+
def b4um_set_sortable_position
|
|
67
|
+
return if position.present?
|
|
68
|
+
|
|
69
|
+
self.position = self.class.maximum(:position).to_i + 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def b4um_compact_sortable_positions
|
|
73
|
+
self.class
|
|
74
|
+
.where("position > ?", position)
|
|
75
|
+
.update_all("position = position - 1")
|
|
76
|
+
end
|
|
77
|
+
RUBY
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
say_status(
|
|
81
|
+
:sortable,
|
|
82
|
+
"#{class_name} now assigns sortable positions",
|
|
83
|
+
:green
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def update_controller
|
|
88
|
+
controller_content = File.read(controller_path)
|
|
89
|
+
|
|
90
|
+
update_index_order(controller_content)
|
|
91
|
+
add_sort_action
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def add_sort_route
|
|
95
|
+
routes_path = File.join(
|
|
96
|
+
destination_root,
|
|
97
|
+
"config/routes.rb"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
unless File.exist?(routes_path)
|
|
101
|
+
raise Thor::Error,
|
|
102
|
+
"Routes not found: config/routes.rb"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
routes = File.read(routes_path)
|
|
106
|
+
|
|
107
|
+
if options[:path].present?
|
|
108
|
+
add_separate_sort_routes(routes)
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return if sort_route_exists?(routes)
|
|
113
|
+
return if separate_sort_route_exists?(routes)
|
|
114
|
+
|
|
115
|
+
add_sort_route_to_resources(routes)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def create_sortable_controller
|
|
119
|
+
copy_file(
|
|
120
|
+
File.expand_path(
|
|
121
|
+
"templates/sortable_controller.js",
|
|
122
|
+
__dir__
|
|
123
|
+
),
|
|
124
|
+
"app/javascript/controllers/sortable_controller.js"
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def create_sortable_partial
|
|
129
|
+
template(
|
|
130
|
+
"_sortable.html.erb.tt",
|
|
131
|
+
File.join(
|
|
132
|
+
"app/views",
|
|
133
|
+
plural_table_name,
|
|
134
|
+
"_sortable.html.erb"
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def create_sortable_view
|
|
140
|
+
return unless options[:path].present?
|
|
141
|
+
|
|
142
|
+
template(
|
|
143
|
+
"sort.html.erb.tt",
|
|
144
|
+
File.join(
|
|
145
|
+
"app/views",
|
|
146
|
+
plural_table_name,
|
|
147
|
+
"sort.html.erb"
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def update_index_layout
|
|
153
|
+
return unless File.exist?(index_path)
|
|
154
|
+
|
|
155
|
+
index = File.read(index_path)
|
|
156
|
+
|
|
157
|
+
if options[:path].present?
|
|
158
|
+
restore_index_layout(index)
|
|
159
|
+
else
|
|
160
|
+
use_sortable_index_layout(index)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.next_migration_number(_dirname)
|
|
165
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def use_sortable_index_layout(index)
|
|
171
|
+
updated_index = index.sub(
|
|
172
|
+
/render\s+["'](?:bento|table|list|alternating|sortable)["']/,
|
|
173
|
+
'render "sortable"'
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return if updated_index == index
|
|
177
|
+
|
|
178
|
+
File.write(index_path, updated_index)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def restore_index_layout(index)
|
|
182
|
+
return unless index.match?(
|
|
183
|
+
/render\s+["']sortable["']/
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
layout = original_index_layout
|
|
187
|
+
|
|
188
|
+
return unless layout
|
|
189
|
+
|
|
190
|
+
updated_index = index.sub(
|
|
191
|
+
/render\s+["']sortable["']/,
|
|
192
|
+
%(render "#{layout}")
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
File.write(index_path, updated_index)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def original_index_layout
|
|
199
|
+
%w[bento table list alternating].find do |layout|
|
|
200
|
+
File.exist?(
|
|
201
|
+
File.join(
|
|
202
|
+
destination_root,
|
|
203
|
+
"app/views",
|
|
204
|
+
plural_table_name,
|
|
205
|
+
"_#{layout}.html.erb"
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def index_path
|
|
212
|
+
File.join(
|
|
213
|
+
destination_root,
|
|
214
|
+
"app/views",
|
|
215
|
+
plural_table_name,
|
|
216
|
+
"index.html.erb"
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def model_path
|
|
221
|
+
File.join(
|
|
222
|
+
destination_root,
|
|
223
|
+
"app/models",
|
|
224
|
+
"#{file_name}.rb"
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def controller_path
|
|
229
|
+
File.join(
|
|
230
|
+
destination_root,
|
|
231
|
+
"app/controllers",
|
|
232
|
+
"#{plural_table_name}_controller.rb"
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def index_view_path
|
|
237
|
+
File.join(
|
|
238
|
+
destination_root,
|
|
239
|
+
"app/views",
|
|
240
|
+
plural_table_name,
|
|
241
|
+
"index.html.erb"
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def update_index_order(controller_content)
|
|
246
|
+
return if controller_content.include?(
|
|
247
|
+
"#{class_name}.order(:position, :id)"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
old_scope = "#{class_name}.all"
|
|
251
|
+
|
|
252
|
+
unless controller_content.include?(old_scope)
|
|
253
|
+
raise Thor::Error,
|
|
254
|
+
"Could not find #{class_name}.all in app/controllers/#{plural_table_name}_controller.rb"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
gsub_file(
|
|
258
|
+
controller_path,
|
|
259
|
+
old_scope,
|
|
260
|
+
"#{class_name}.order(:position, :id)"
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def add_sort_action
|
|
265
|
+
controller_content = File.read(controller_path)
|
|
266
|
+
|
|
267
|
+
if controller_content.match?(/^\s*def\s+sort\b/)
|
|
268
|
+
add_sort_page_support(controller_content)
|
|
269
|
+
return
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
get_handling =
|
|
273
|
+
if options[:path].present?
|
|
274
|
+
<<~RUBY
|
|
275
|
+
if request.get?
|
|
276
|
+
@#{plural_table_name} = #{class_name}.order(:position, :id)
|
|
277
|
+
return
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
RUBY
|
|
281
|
+
else
|
|
282
|
+
""
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
action = <<~RUBY.indent(2)
|
|
286
|
+
def sort
|
|
287
|
+
#{get_handling.indent(2)} record = #{class_name}.find_by(id: params[:id])
|
|
288
|
+
target_position = params[:position].to_i
|
|
289
|
+
maximum_position = #{class_name}.count
|
|
290
|
+
|
|
291
|
+
unless record &&
|
|
292
|
+
target_position.between?(1, maximum_position)
|
|
293
|
+
head :unprocessable_content
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
if target_position == record.position
|
|
298
|
+
head :no_content
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
old_position = record.position
|
|
303
|
+
|
|
304
|
+
#{class_name}.transaction do
|
|
305
|
+
if target_position < old_position
|
|
306
|
+
#{class_name}
|
|
307
|
+
.where(position: target_position...old_position)
|
|
308
|
+
.where.not(id: record.id)
|
|
309
|
+
.update_all("position = position + 1")
|
|
310
|
+
elsif target_position > old_position
|
|
311
|
+
#{class_name}
|
|
312
|
+
.where(position: (old_position + 1)..target_position)
|
|
313
|
+
.where.not(id: record.id)
|
|
314
|
+
.update_all("position = position - 1")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
record.update_columns(
|
|
318
|
+
position: target_position,
|
|
319
|
+
updated_at: Time.current
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
head :no_content
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
RUBY
|
|
327
|
+
|
|
328
|
+
private_marker = "\n private\n"
|
|
329
|
+
|
|
330
|
+
if controller_content.include?(private_marker)
|
|
331
|
+
insert_into_file(
|
|
332
|
+
controller_path,
|
|
333
|
+
action,
|
|
334
|
+
before: private_marker
|
|
335
|
+
)
|
|
336
|
+
else
|
|
337
|
+
controller_end = /\nend\s*\z/
|
|
338
|
+
|
|
339
|
+
unless controller_content.match?(controller_end)
|
|
340
|
+
raise Thor::Error,
|
|
341
|
+
"Could not find the end of app/controllers/#{plural_table_name}_controller.rb"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
insert_into_file(
|
|
345
|
+
controller_path,
|
|
346
|
+
"\n#{action}",
|
|
347
|
+
before: controller_end
|
|
348
|
+
)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def add_sort_page_support(controller_content)
|
|
353
|
+
return unless options[:path].present?
|
|
354
|
+
return if controller_content.include?("if request.get?")
|
|
355
|
+
|
|
356
|
+
sort_action =
|
|
357
|
+
/^(\s*)def sort\s*$/
|
|
358
|
+
|
|
359
|
+
unless controller_content.match?(sort_action)
|
|
360
|
+
raise Thor::Error,
|
|
361
|
+
"Could not find sort action in app/controllers/#{plural_table_name}_controller.rb"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
gsub_file(
|
|
365
|
+
controller_path,
|
|
366
|
+
sort_action
|
|
367
|
+
) do |match|
|
|
368
|
+
indentation = match[/\A\s*/]
|
|
369
|
+
|
|
370
|
+
<<~RUBY.chomp
|
|
371
|
+
#{match}
|
|
372
|
+
#{indentation} if request.get?
|
|
373
|
+
#{indentation} @#{plural_table_name} = #{class_name}.order(:position, :id)
|
|
374
|
+
#{indentation} return
|
|
375
|
+
#{indentation} end
|
|
376
|
+
RUBY
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def add_separate_sort_routes(routes)
|
|
381
|
+
path = options[:path].to_s
|
|
382
|
+
|
|
383
|
+
remove_default_sort_route(routes)
|
|
384
|
+
|
|
385
|
+
routes = File.read(
|
|
386
|
+
File.join(
|
|
387
|
+
destination_root,
|
|
388
|
+
"config/routes.rb"
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
get_route =
|
|
393
|
+
%(get "#{path}", to: "#{plural_table_name}#sort")
|
|
394
|
+
|
|
395
|
+
patch_route =
|
|
396
|
+
%(patch "#{path}", to: "#{plural_table_name}#sort", as: :sort_#{plural_table_name})
|
|
397
|
+
|
|
398
|
+
routes_to_add = []
|
|
399
|
+
|
|
400
|
+
routes_to_add << get_route unless routes.include?(get_route)
|
|
401
|
+
|
|
402
|
+
routes_to_add << patch_route unless routes.include?(patch_route)
|
|
403
|
+
|
|
404
|
+
return if routes_to_add.empty?
|
|
405
|
+
|
|
406
|
+
routes_end = /^end\s*\z/
|
|
407
|
+
|
|
408
|
+
unless routes.match?(routes_end)
|
|
409
|
+
raise Thor::Error,
|
|
410
|
+
"Could not find the end of config/routes.rb"
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
route_code = <<~RUBY
|
|
414
|
+
|
|
415
|
+
#{routes_to_add.join("\n ")}
|
|
416
|
+
RUBY
|
|
417
|
+
|
|
418
|
+
insert_into_file(
|
|
419
|
+
"config/routes.rb",
|
|
420
|
+
route_code,
|
|
421
|
+
before: routes_end
|
|
422
|
+
)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def remove_default_sort_route(routes)
|
|
426
|
+
return unless sort_route_exists?(routes)
|
|
427
|
+
|
|
428
|
+
gsub_file(
|
|
429
|
+
"config/routes.rb",
|
|
430
|
+
/^(\s*)patch :sort, on: :collection\s*\n/,
|
|
431
|
+
""
|
|
432
|
+
)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def sort_route_exists?(routes)
|
|
436
|
+
resource_block = sortable_resource_block(routes)
|
|
437
|
+
|
|
438
|
+
resource_block&.match?(
|
|
439
|
+
/^\s*patch\s+:sort,\s+on:\s+:collection\s*$/
|
|
440
|
+
) || false
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def separate_sort_route_exists?(routes)
|
|
444
|
+
routes.match?(
|
|
445
|
+
/^\s*patch\s+["'][^"']+["'],\s+to:\s+["']#{Regexp.escape(plural_table_name)}#sort["']/
|
|
446
|
+
)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def sortable_resource_block(routes)
|
|
450
|
+
pattern = /
|
|
451
|
+
^(?<indent>\s*)
|
|
452
|
+
resources\s+:#{Regexp.escape(plural_table_name)}\s+do\s*$
|
|
453
|
+
(?<body>.*?)
|
|
454
|
+
^\k<indent>end\s*$
|
|
455
|
+
/mx
|
|
456
|
+
|
|
457
|
+
routes[pattern]
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def add_sort_route_to_resources(routes)
|
|
461
|
+
block_pattern =
|
|
462
|
+
/^(\s*)resources :#{Regexp.escape(plural_table_name)} do\s*$/
|
|
463
|
+
|
|
464
|
+
plain_pattern =
|
|
465
|
+
/^(\s*)resources :#{Regexp.escape(plural_table_name)}\s*$/
|
|
466
|
+
|
|
467
|
+
if routes.match?(block_pattern)
|
|
468
|
+
gsub_file(
|
|
469
|
+
"config/routes.rb",
|
|
470
|
+
block_pattern
|
|
471
|
+
) do |match|
|
|
472
|
+
indentation = match[/\A\s*/]
|
|
473
|
+
|
|
474
|
+
[
|
|
475
|
+
match,
|
|
476
|
+
"#{indentation} patch :sort, on: :collection"
|
|
477
|
+
].join("\n")
|
|
478
|
+
end
|
|
479
|
+
elsif routes.match?(plain_pattern)
|
|
480
|
+
gsub_file(
|
|
481
|
+
"config/routes.rb",
|
|
482
|
+
plain_pattern
|
|
483
|
+
) do |match|
|
|
484
|
+
indentation = match[/\A\s*/]
|
|
485
|
+
|
|
486
|
+
[
|
|
487
|
+
"#{indentation}resources :#{plural_table_name} do",
|
|
488
|
+
"#{indentation} patch :sort, on: :collection",
|
|
489
|
+
"#{indentation}end"
|
|
490
|
+
].join("\n")
|
|
491
|
+
end
|
|
492
|
+
else
|
|
493
|
+
raise Thor::Error,
|
|
494
|
+
"Could not find resources :#{plural_table_name} in config/routes.rb"
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def position_column_exists?
|
|
499
|
+
migration_contains_position? ||
|
|
500
|
+
schema_contains_position?
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def migration_contains_position?
|
|
504
|
+
Dir.glob(
|
|
505
|
+
File.join(
|
|
506
|
+
destination_root,
|
|
507
|
+
"db/migrate/*_add_position_to_#{plural_table_name}.rb"
|
|
508
|
+
)
|
|
509
|
+
).any?
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def schema_contains_position?
|
|
513
|
+
schema_path = File.join(destination_root, "db/schema.rb")
|
|
514
|
+
|
|
515
|
+
return false unless File.exist?(schema_path)
|
|
516
|
+
|
|
517
|
+
schema = File.read(schema_path)
|
|
518
|
+
|
|
519
|
+
table_pattern = /
|
|
520
|
+
create_table\s+"#{Regexp.escape(plural_table_name)}".*?^ end
|
|
521
|
+
/mx
|
|
522
|
+
|
|
523
|
+
table = schema[table_pattern]
|
|
524
|
+
|
|
525
|
+
table&.match?(/t\.integer\s+"position"/) || false
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<div class="b4um-sortable"
|
|
2
|
+
data-controller="sortable"
|
|
3
|
+
data-sortable-update-url-value="<%%= sort_<%= plural_table_name %>_path %>">
|
|
4
|
+
|
|
5
|
+
<%% <%= plural_table_name %>.each do |<%= singular_table_name %>| %>
|
|
6
|
+
<div class="b4um-sortable__item"
|
|
7
|
+
data-sortable-id="<%%= <%= singular_table_name %>.id %>"
|
|
8
|
+
data-action="dragover->sortable#dragOver drop->sortable#drop">
|
|
9
|
+
|
|
10
|
+
<div class="b4um-sortable__controls">
|
|
11
|
+
<input type="number"
|
|
12
|
+
class="b4um-sortable__position"
|
|
13
|
+
value="<%%= <%= singular_table_name %>.position %>"
|
|
14
|
+
min="1"
|
|
15
|
+
aria-label="Position"
|
|
16
|
+
data-action="change->sortable#positionChanged keydown->sortable#positionKeydown">
|
|
17
|
+
|
|
18
|
+
<button type="button"
|
|
19
|
+
class="b4um-sortable__handle"
|
|
20
|
+
draggable="true"
|
|
21
|
+
aria-label="Move <%= singular_table_name.humanize.downcase %>"
|
|
22
|
+
data-action="dragstart->sortable#dragStart dragend->sortable#dragEnd">
|
|
23
|
+
☰
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="b4um-sortable__content">
|
|
28
|
+
<%%= render <%= singular_table_name %>,
|
|
29
|
+
compact: true,
|
|
30
|
+
preview_length: 100 %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="b4um-sortable__actions">
|
|
34
|
+
<%%= link_to "Show",
|
|
35
|
+
<%= singular_table_name %>,
|
|
36
|
+
class: "button button--secondary" %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<%% end %>
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddPositionTo<%= class_name.pluralize %> < ActiveRecord::Migration[8.0]
|
|
4
|
+
def up
|
|
5
|
+
add_column :<%= plural_table_name %>, :position, :integer
|
|
6
|
+
|
|
7
|
+
sortable_record = Class.new(ActiveRecord::Base) do
|
|
8
|
+
self.table_name = "<%= plural_table_name %>"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
sortable_record.reset_column_information
|
|
12
|
+
|
|
13
|
+
sortable_record.order(:id).find_each.with_index(1) do |record, position|
|
|
14
|
+
record.update_columns(position: position)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
change_column_null :<%= plural_table_name %>, :position, false
|
|
18
|
+
|
|
19
|
+
add_index :<%= plural_table_name %>, :position
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def down
|
|
23
|
+
remove_index :<%= plural_table_name %>, :position
|
|
24
|
+
remove_column :<%= plural_table_name %>, :position
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
updateUrl: String
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this.draggedItem = null
|
|
10
|
+
this.draggedFromPosition = null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
dragStart(event) {
|
|
14
|
+
const item = event.currentTarget.closest('[data-sortable-id]')
|
|
15
|
+
|
|
16
|
+
if (!item) return
|
|
17
|
+
|
|
18
|
+
this.draggedItem = item
|
|
19
|
+
this.draggedFromPosition = this.positionFor(item)
|
|
20
|
+
|
|
21
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
22
|
+
event.dataTransfer.setData('text/plain', item.dataset.sortableId)
|
|
23
|
+
|
|
24
|
+
item.classList.add('b4um-sortable__item--dragging')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
dragEnd() {
|
|
28
|
+
if (this.draggedItem) {
|
|
29
|
+
this.draggedItem.classList.remove('b4um-sortable__item--dragging')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.draggedItem = null
|
|
33
|
+
this.draggedFromPosition = null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
dragOver(event) {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
|
|
39
|
+
if (!this.draggedItem) return
|
|
40
|
+
|
|
41
|
+
const target = event.currentTarget
|
|
42
|
+
|
|
43
|
+
if (target === this.draggedItem) return
|
|
44
|
+
|
|
45
|
+
const bounds = target.getBoundingClientRect()
|
|
46
|
+
const insertAfter = event.clientY > bounds.top + bounds.height / 2
|
|
47
|
+
|
|
48
|
+
if (insertAfter) {
|
|
49
|
+
target.after(this.draggedItem)
|
|
50
|
+
} else {
|
|
51
|
+
target.before(this.draggedItem)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
drop(event) {
|
|
56
|
+
event.preventDefault()
|
|
57
|
+
|
|
58
|
+
if (!this.draggedItem) return
|
|
59
|
+
|
|
60
|
+
const item = this.draggedItem
|
|
61
|
+
const position = this.positionAfterDrop(item)
|
|
62
|
+
|
|
63
|
+
if (!position) {
|
|
64
|
+
window.location.reload()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.persist(item.dataset.sortableId, position)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
positionKeydown(event) {
|
|
72
|
+
if (event.key !== 'Enter') return
|
|
73
|
+
|
|
74
|
+
event.preventDefault()
|
|
75
|
+
event.currentTarget.blur()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
positionChanged(event) {
|
|
79
|
+
const input = event.currentTarget
|
|
80
|
+
const item = input.closest('[data-sortable-id]')
|
|
81
|
+
|
|
82
|
+
if (!item) return
|
|
83
|
+
|
|
84
|
+
const position = Number.parseInt(input.value, 10)
|
|
85
|
+
|
|
86
|
+
if (Number.isNaN(position) || position < 1) {
|
|
87
|
+
window.location.reload()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.persist(item.dataset.sortableId, position)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
positionFor(item) {
|
|
95
|
+
const input = item.querySelector('.b4um-sortable__position')
|
|
96
|
+
|
|
97
|
+
if (!input) return null
|
|
98
|
+
|
|
99
|
+
const position = Number.parseInt(input.value, 10)
|
|
100
|
+
|
|
101
|
+
return Number.isNaN(position) ? null : position
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
positionAfterDrop(item) {
|
|
105
|
+
const previousItem = item.previousElementSibling
|
|
106
|
+
const nextItem = item.nextElementSibling
|
|
107
|
+
|
|
108
|
+
if (previousItem?.matches('[data-sortable-id]')) {
|
|
109
|
+
const previousPosition = this.positionFor(previousItem)
|
|
110
|
+
|
|
111
|
+
if (previousPosition) {
|
|
112
|
+
if (this.draggedFromPosition && this.draggedFromPosition < previousPosition) {
|
|
113
|
+
return previousPosition
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return previousPosition + 1
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (nextItem?.matches('[data-sortable-id]')) {
|
|
121
|
+
const nextPosition = this.positionFor(nextItem)
|
|
122
|
+
|
|
123
|
+
if (nextPosition) {
|
|
124
|
+
if (this.draggedFromPosition && this.draggedFromPosition > nextPosition) {
|
|
125
|
+
return nextPosition
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Math.max(1, nextPosition - 1)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return this.draggedFromPosition
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async persist(id, position) {
|
|
136
|
+
if (!this.hasUpdateUrlValue || !this.updateUrlValue) return
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const response = await fetch(this.updateUrlValue, {
|
|
140
|
+
method: 'PATCH',
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
Accept: 'application/json',
|
|
144
|
+
'X-CSRF-Token': this.csrfToken()
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
id,
|
|
148
|
+
position
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`Sortable update failed: ${response.status}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
window.location.reload()
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(error)
|
|
159
|
+
window.location.reload()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
csrfToken() {
|
|
164
|
+
return document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
165
|
+
}
|
|
166
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: b4um_generators
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Baum
|
|
@@ -115,6 +115,7 @@ files:
|
|
|
115
115
|
- lib/generators/b4um/install/templates/b4um/pagination.css
|
|
116
116
|
- lib/generators/b4um/install/templates/b4um/resources.css
|
|
117
117
|
- lib/generators/b4um/install/templates/b4um/search.css
|
|
118
|
+
- lib/generators/b4um/install/templates/b4um/sortable.css
|
|
118
119
|
- lib/generators/b4um/install/templates/b4um/tables.css
|
|
119
120
|
- lib/generators/b4um/install/templates/b4um/theme-switcher.css
|
|
120
121
|
- lib/generators/b4um/install/templates/b4um/theme.css
|
|
@@ -145,6 +146,11 @@ files:
|
|
|
145
146
|
- lib/generators/b4um/search/search_generator.rb
|
|
146
147
|
- lib/generators/b4um/search/templates/search.rb
|
|
147
148
|
- lib/generators/b4um/search/templates/search_form.html.erb.tt
|
|
149
|
+
- lib/generators/b4um/sortable/sortable_generator.rb
|
|
150
|
+
- lib/generators/b4um/sortable/templates/_sortable.html.erb.tt
|
|
151
|
+
- lib/generators/b4um/sortable/templates/add_position_migration.rb.tt
|
|
152
|
+
- lib/generators/b4um/sortable/templates/sort.html.erb.tt
|
|
153
|
+
- lib/generators/b4um/sortable/templates/sortable_controller.js
|
|
148
154
|
- lib/generators/b4um/table/table_generator.rb
|
|
149
155
|
- lib/generators/b4um/table/templates/_table.html.erb.tt
|
|
150
156
|
- lib/generators/b4um/trix/templates/b4um_rich_text_helper.rb
|