33module BetterTogether
44 class ChecklistItemsController < FriendlyResourceController # rubocop:todo Style/Documentation, Metrics/ClassLength
55 before_action :set_checklist
6- before_action :checklist_item , only : %i[ show edit update destroy ]
6+ before_action :checklist_item , only : %i[ show edit update destroy position ]
77
88 helper_method :new_checklist_item
99
@@ -13,7 +13,9 @@ def create # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
1313 authorize @checklist_item
1414 respond_to do |format |
1515 if @checklist_item . save
16- format . html { redirect_to request . referer || checklist_path ( @checklist ) , notice : t ( 'flash.generic.created' ) }
16+ format . html do
17+ redirect_to request . referer || checklist_path ( @checklist ) , notice : t ( 'flash.generic.created' )
18+ end
1719 format . turbo_stream
1820 else
1921 format . html do
@@ -35,7 +37,9 @@ def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
3537 authorize @checklist_item
3638 respond_to do |format |
3739 if @checklist_item . update ( resource_params )
38- format . html { redirect_to request . referer || checklist_path ( @checklist ) , notice : t ( 'flash.generic.updated' ) }
40+ format . html do
41+ redirect_to request . referer || checklist_path ( @checklist ) , notice : t ( 'flash.generic.updated' )
42+ end
3943 format . turbo_stream
4044 else
4145 format . html do
@@ -65,7 +69,8 @@ def destroy
6569 end
6670
6771 def position # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
68- authorize @checklist_item
72+ # Reordering affects the checklist as a whole; require permission to update the parent
73+ authorize @checklist , :update?
6974
7075 direction = params [ :direction ]
7176 sibling = if direction == 'up'
@@ -87,10 +92,27 @@ def position # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
8792 respond_to do |format |
8893 format . html { redirect_to request . referer || checklist_path ( @checklist ) , notice : t ( 'flash.generic.updated' ) }
8994 format . turbo_stream do
90- render turbo_stream : turbo_stream . replace ( dom_id ( @checklist , :checklist_items ) ) {
91- render partial : 'better_together/checklist_items/checklist_item' ,
92- collection : @checklist . checklist_items . with_translations , as : :checklist_item
93- }
95+ # Move the LI node: remove the moved element and insert before/after the sibling
96+ begin
97+ a = @checklist_item
98+ b = sibling
99+ streams = [ ]
100+ streams << turbo_stream . remove ( helpers . dom_id ( a ) )
101+
102+ # If direction is up, insert before sibling; if down, insert after sibling
103+ if direction == 'up'
104+ streams << turbo_stream . before ( helpers . dom_id ( b ) , partial : 'better_together/checklist_items/checklist_item' , locals : { checklist_item : a , checklist : @checklist , moved : true } )
105+ else
106+ streams << turbo_stream . after ( helpers . dom_id ( b ) , partial : 'better_together/checklist_items/checklist_item' , locals : { checklist_item : a , checklist : @checklist , moved : true } )
107+ end
108+
109+ render turbo_stream : streams
110+ rescue StandardError
111+ # Fallback: update only the inner list contents
112+ render turbo_stream : turbo_stream . update ( "#{ helpers . dom_id ( @checklist , :checklist_items ) } " ,
113+ partial : 'better_together/checklist_items/list_contents' ,
114+ locals : { checklist : @checklist } )
115+ end
94116 end
95117 end
96118 end
@@ -104,6 +126,9 @@ def reorder # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
104126
105127 klass = resource_class
106128
129+ # Capture previous order before we update positions so we can compute a minimal DOM update
130+ previous_order = @checklist . checklist_items . order ( :position ) . pluck ( :id )
131+
107132 klass . transaction do
108133 ids . each_with_index do |id , idx |
109134 item = klass . find_by ( id : id , checklist : @checklist )
@@ -116,10 +141,60 @@ def reorder # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
116141 respond_to do |format |
117142 format . json { head :no_content }
118143 format . turbo_stream do
119- render turbo_stream : turbo_stream . replace ( dom_id ( @checklist , :checklist_items ) ) {
120- render partial : 'better_together/checklist_items/checklist_item' ,
121- collection : @checklist . checklist_items . with_translations , as : :checklist_item
122- }
144+ # Try a minimal DOM update: if exactly one item moved, remove it and insert before/after the neighbor.
145+ begin
146+ ordered = params [ :ordered_ids ] . map ( &:to_i )
147+ # previous_order holds the order before we updated positions
148+ current_before = previous_order
149+
150+ # If nothing changed, no content
151+ if ordered == current_before
152+ head :no_content and return
153+ end
154+
155+ # Detect single moved id (difference between arrays)
156+ moved = ( ordered - current_before )
157+ removed = ( current_before - ordered )
158+
159+ if moved . size == 1 && removed . size == 1
160+ moved_id = moved . first
161+ moved_item = @checklist . checklist_items . find_by ( id : moved_id )
162+ # Safety: if item not found, fallback
163+ unless moved_item
164+ raise 'moved-missing'
165+ end
166+
167+ # Where did it land?
168+ new_index = ordered . index ( moved_id )
169+
170+ streams = [ ]
171+ # Remove original node first
172+ streams << turbo_stream . remove ( helpers . dom_id ( moved_item ) )
173+
174+ # Append after the next element (neighbor at new_index + 1)
175+ neighbor_id = ordered [ new_index + 1 ] if new_index
176+ if neighbor_id
177+ neighbor = @checklist . checklist_items . find_by ( id : neighbor_id )
178+ if neighbor
179+ streams << turbo_stream . after ( helpers . dom_id ( neighbor ) , partial : 'better_together/checklist_items/checklist_item' , locals : { checklist_item : moved_item , checklist : @checklist , moved : true } )
180+ render turbo_stream : streams and return
181+ end
182+ end
183+
184+ # If neighbor not found (moved to end), append to the UL
185+ streams << turbo_stream . append ( "#{ helpers . dom_id ( @checklist , :checklist_items ) } ul" , partial : 'better_together/checklist_items/checklist_item' , locals : { checklist_item : moved_item , checklist : @checklist , moved : true } )
186+ render turbo_stream : streams and return
187+ end
188+
189+ # Fallback: update inner contents for complex reorders
190+ render turbo_stream : turbo_stream . update ( "#{ helpers . dom_id ( @checklist , :checklist_items ) } " ,
191+ partial : 'better_together/checklist_items/list_contents' ,
192+ locals : { checklist : @checklist } )
193+ rescue StandardError
194+ render turbo_stream : turbo_stream . update ( "#{ helpers . dom_id ( @checklist , :checklist_items ) } " ,
195+ partial : 'better_together/checklist_items/list_contents' ,
196+ locals : { checklist : @checklist } )
197+ end
123198 end
124199 end
125200 end
@@ -128,14 +203,37 @@ def reorder # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
128203
129204 def set_checklist
130205 key = params [ :checklist_id ] || params [ :id ]
131- @checklist = if key . nil?
132- nil
133- else
134- # The checklists table doesn't have a direct `slug` column in this schema
135- # (friendly id slugs are stored in the `friendly_id_slugs` table), so avoid
136- # querying `slug` directly. Lookup by id or identifier instead.
137- BetterTogether ::Checklist . where ( id : key ) . or ( BetterTogether ::Checklist . where ( identifier : key ) ) . first
138- end
206+
207+ @checklist = nil
208+ if key . present?
209+ # Try direct id/identifier lookup first (fast)
210+ @checklist = BetterTogether ::Checklist . where ( id : key ) . or ( BetterTogether ::Checklist . where ( identifier : key ) ) . first
211+
212+ # Fallbacks to mirror FriendlyResourceController behaviour: try translated slug lookups
213+ if @checklist . nil?
214+ begin
215+ # Try Mobility translation lookup across locales
216+ translation = Mobility ::Backends ::ActiveRecord ::KeyValue ::StringTranslation . where (
217+ translatable_type : 'BetterTogether::Checklist' ,
218+ key : 'slug' ,
219+ value : key
220+ ) . includes ( :translatable ) . last
221+
222+ @checklist ||= translation &.translatable
223+ rescue StandardError
224+ # ignore DB/translation lookup errors and continue to friendly_id fallback
225+ end
226+ end
227+
228+ if @checklist . nil?
229+ begin
230+ @checklist = BetterTogether ::Checklist . friendly . find ( key )
231+ rescue StandardError
232+ @checklist ||= BetterTogether ::Checklist . find_by ( id : key )
233+ end
234+ end
235+ end
236+
139237 raise ActiveRecord ::RecordNotFound unless @checklist
140238 end
141239
0 commit comments