@@ -220,70 +220,334 @@ public function replace_merge_tag( $matches = array(), $text = '', $form = array
220
220
/**
221
221
* Calculate the current sequence number for the context.
222
222
*
223
- * @param \GV\Template_Context $context The context.
223
+ * This method handles three scenarios:
224
+ * 1. Single Entry - Finds the entry's position in the full result set.
225
+ * 2. Paginated Multiple Entries - Calculates sequence based on current page.
226
+ * 3. Sequential calls - Increments/decrements from cached starting point.
227
+ *
228
+ * @since 2.3.3
229
+ *
230
+ * @param \GV\Template_Context $context The template context containing View, field, and entry information.
224
231
*
225
232
* @return int The sequence number for the field/entry within the view results.
226
233
*/
227
234
public function get_sequence ( $ context ) {
235
+ // Static cache for starting line numbers per view/field combination.
228
236
static $ startlines = array ();
229
237
230
- $ context_key = md5 (
238
+ // Ensure field configuration is loaded from View settings.
239
+ $ this ->ensure_field_configuration ( $ context );
240
+
241
+ // Generate unique key for this view/field combination to track sequence state.
242
+ $ context_key = $ this ->get_context_key ( $ context );
243
+
244
+ // Handle single entry view - need to find its position in the full result set.
245
+ if ( $ this ->is_single_entry_view ( $ context ) ) {
246
+ return $ this ->get_single_entry_sequence ( $ context );
247
+ }
248
+
249
+ // Initialize starting line for this context if not already cached.
250
+ if ( ! isset ( $ startlines [ $ context_key ] ) ) {
251
+ $ starting_number = $ this ->calculate_starting_number ( $ context );
252
+ $ startlines [ $ context_key ] = $ starting_number ;
253
+ }
254
+
255
+ // Return current sequence and increment/decrement for next call.
256
+ return $ this ->get_next_sequence_number ( $ startlines , $ context_key , $ context );
257
+ }
258
+
259
+ /**
260
+ * Ensure field configuration values are loaded from View settings.
261
+ *
262
+ * This method reads the 'start' and 'reverse' settings from the field's
263
+ * configuration as saved in the View. This fixes the bug where these
264
+ * settings were ignored when the field was displayed.
265
+ *
266
+ * @since TODO
267
+ *
268
+ * @param \GV\Template_Context $context The template context.
269
+ *
270
+ * @return void
271
+ */
272
+ private function ensure_field_configuration ( $ context ) {
273
+ // Get field configuration once
274
+ $ field_settings = $ context ->field ->as_configuration ();
275
+
276
+ // Load 'start' value from field configuration if not already set
277
+ // The property may exist but be null/empty, so check for meaningful value
278
+ if ( empty ( $ context ->field ->start ) && $ context ->field ->start !== 0 ) {
279
+ $ start_value = isset ( $ field_settings ['start ' ] ) ? $ field_settings ['start ' ] : 1 ;
280
+ $ context ->field ->start = is_numeric ( $ start_value ) ? (int ) $ start_value : 1 ;
281
+ }
282
+
283
+ // Load 'reverse' value from field configuration if it exists
284
+ // Only override the field's reverse property if the configuration explicitly sets it
285
+ // This allows tests to manually set reverse while still respecting View configuration
286
+ if ( isset ( $ field_settings ['reverse ' ] ) ) {
287
+ $ context ->field ->reverse = ! empty ( $ field_settings ['reverse ' ] );
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Generate a unique key for caching sequence state per view/field combination.
293
+ *
294
+ * @since TODO
295
+ *
296
+ * @param \GV\Template_Context $context The template context.
297
+ *
298
+ * @return string MD5 hash of the view anchor ID and field UID.
299
+ */
300
+ private function get_context_key ( $ context ) {
301
+ return md5 (
231
302
json_encode (
232
303
array (
233
304
$ context ->view ->get_anchor_id (),
234
305
\GV \Utils::get ( $ context , 'field/UID ' ),
235
306
)
236
307
)
237
308
);
309
+ }
238
310
239
- /**
240
- * Figure out the starting number.
241
- */
242
- if ( $ context ->request && $ entry = $ context ->request ->is_entry () ) {
311
+ /**
312
+ * Check if we're in a single entry view context.
313
+ *
314
+ * @since TODO
315
+ *
316
+ * @param \GV\Template_Context $context The template context.
317
+ *
318
+ * @return bool True if viewing a single entry, false otherwise.
319
+ */
320
+ private function is_single_entry_view ( $ context ) {
321
+ return $ context ->request && $ context ->request ->is_entry ();
322
+ }
243
323
244
- $ sql_query = array ();
324
+ /**
325
+ * Calculate sequence number for a single entry view.
326
+ *
327
+ * This method finds the position of the current entry within the full
328
+ * result set by executing the view's query and locating the entry.
329
+ *
330
+ * @since TODO
331
+ *
332
+ * @param \GV\Template_Context $context The template context.
333
+ *
334
+ * @return int The sequence number for the entry, or 0 if not found.
335
+ */
336
+ private function get_single_entry_sequence ( $ context ) {
337
+ $ entry = $ context ->request ->is_entry ();
245
338
246
- add_filter (
247
- 'gform_gf_query_sql ' ,
248
- $ callback = function ( $ sql ) use ( &$ sql_query ) {
249
- $ sql_query = $ sql ;
250
- return $ sql ;
251
- }
252
- );
339
+ // Capture the SQL query used by the view
340
+ $ sql_query = $ this ->capture_view_sql_query ( $ context );
253
341
254
- $ total = $ context ->view ->get_entries ()->total ();
255
- remove_filter ( 'gform_gf_query_sql ' , $ callback );
342
+ if ( empty ( $ sql_query ) ) {
343
+ return 0 ;
344
+ }
256
345
257
- unset( $ sql_query ['paginate ' ] );
346
+ // Get all entries (without pagination) to find current entry's position
347
+ $ results = $ this ->get_all_entry_results ( $ sql_query );
258
348
259
- global $ wpdb ;
349
+ if ( is_null ( $ results ) ) {
350
+ return 0 ;
351
+ }
260
352
261
- $ results = $ wpdb ->get_results ( implode ( ' ' , $ sql_query ), ARRAY_A );
353
+ // Find the entry's position in the results
354
+ return $ this ->find_entry_position ( $ results , $ entry , $ context );
355
+ }
262
356
263
- if ( is_null ( $ results ) ) {
264
- return 0 ;
357
+ /**
358
+ * Capture the SQL query used to fetch entries for the view.
359
+ *
360
+ * @since TODO
361
+ *
362
+ * @param \GV\Template_Context $context The template context.
363
+ *
364
+ * @return array The SQL query parts.
365
+ */
366
+ private function capture_view_sql_query ( $ context ) {
367
+ $ sql_query = array ();
368
+
369
+ // Hook into Gravity Forms query generation to capture the SQL
370
+ add_filter (
371
+ 'gform_gf_query_sql ' ,
372
+ $ callback = function ( $ sql ) use ( &$ sql_query ) {
373
+ $ sql_query = $ sql ;
374
+ return $ sql ;
265
375
}
376
+ );
266
377
267
- foreach ( $ results as $ n => $ result ) {
268
- if ( in_array ( $ entry ->ID , $ result ) ) {
269
- return $ context ->field ->reverse ? ( $ total - $ n ) : ( $ n + 1 );
270
- }
271
- }
378
+ // Trigger the query by getting entries (this populates $sql_query)
379
+ // Pass the request to ensure proper context
380
+ $ request = isset ( $ context ->request ) ? $ context ->request : gravityview ()->request ;
381
+ $ context ->view ->get_entries ( $ request )->total ();
272
382
273
- return 0 ;
274
- } elseif ( ! isset ( $ startlines [ $ context_key ] ) ) {
275
- $ pagenum = max ( 0 , \GV \Utils::_GET ( 'pagenum ' , 1 ) - 1 );
276
- $ pagesize = $ context ->view ->settings ->get ( 'page_size ' , 25 );
383
+ // Clean up by removing our filter
384
+ remove_filter ( 'gform_gf_query_sql ' , $ callback );
277
385
386
+ // Remove pagination to get all results
387
+ unset( $ sql_query ['paginate ' ] );
388
+
389
+ // For single entry views with a custom start value, sort by ID ASC to get
390
+ // consistent sequence numbers based on creation order
391
+ // Only override if start value is different from default (1)
392
+ if ( $ this ->is_single_entry_view ( $ context ) && $ context ->field ->start !== 1 ) {
393
+ $ sql_query ['order ' ] = 'ORDER BY `t1`.`id` ASC ' ;
394
+ }
395
+
396
+ return $ sql_query ;
397
+ }
398
+
399
+ /**
400
+ * Execute SQL query to get all entry results.
401
+ *
402
+ * @since TODO
403
+ *
404
+ * @param array $sql_query The SQL query parts.
405
+ *
406
+ * @return array|null Array of results or null on failure.
407
+ */
408
+ private function get_all_entry_results ( $ sql_query ) {
409
+ global $ wpdb ;
410
+ return $ wpdb ->get_results ( implode ( ' ' , $ sql_query ), ARRAY_A );
411
+ }
412
+
413
+ /**
414
+ * Find the position of an entry in the results and calculate its sequence number.
415
+ *
416
+ * @since TODO
417
+ *
418
+ * @param array $results The query results.
419
+ * @param \GV\Entry $entry The entry to find.
420
+ * @param \GV\Template_Context $context The template context.
421
+ *
422
+ * @return int The sequence number for the entry, or 0 if not found.
423
+ */
424
+ private function find_entry_position ( $ results , $ entry , $ context ) {
425
+ $ total_entries = count ( $ results );
426
+
427
+ foreach ( $ results as $ position => $ result ) {
428
+ // Check if this result row contains the entry ID
429
+ // The result may have the ID in 'id' or 'entry_id' column
430
+ $ result_id = isset ( $ result ['id ' ] ) ? $ result ['id ' ] : ( isset ( $ result ['entry_id ' ] ) ? $ result ['entry_id ' ] : null );
431
+
432
+ // Use loose comparison to handle string/int type differences
433
+ if ( $ result_id != $ entry ->ID ) {
434
+ continue ;
435
+ }
436
+
437
+ // Calculate sequence based on position in the view's sort order
278
438
if ( $ context ->field ->reverse ) {
279
- $ startlines [ $ context_key ] = $ context -> view -> get_entries ()-> total () - ( $ pagenum * $ pagesize );
280
- $ startlines [ $ context_key ] += $ context ->field ->start - 1 ;
439
+ // For reverse: highest number - position
440
+ return $ context ->field ->start + $ total_entries - $ position - 1 ;
281
441
} else {
282
- $ startlines [ $ context_key ] = ( $ pagenum * $ pagesize ) + $ context ->field ->start ;
442
+ // For normal: position + start value (position is 0-based)
443
+ return $ position + $ context ->field ->start ;
444
+ }
445
+ }
446
+
447
+ return 0 ; // Entry not found
448
+ }
449
+
450
+ /**
451
+ * Calculate the starting number for paginated views.
452
+ *
453
+ * Takes into account the current page, page size, and whether
454
+ * the sequence is reversed.
455
+ *
456
+ * @since TODO
457
+ *
458
+ * @param \GV\Template_Context $context The template context.
459
+ *
460
+ * @return int The starting sequence number for the current page.
461
+ */
462
+ private function calculate_starting_number ( $ context ) {
463
+ // Get current page number (convert from 1-based to 0-based)
464
+ $ pagenum = max ( 0 , \GV \Utils::_GET ( 'pagenum ' , 1 ) - 1 );
465
+
466
+ // Get number of entries per page
467
+ $ pagesize = $ context ->view ->settings ->get ( 'page_size ' , 25 );
468
+
469
+ if ( $ context ->field ->reverse ) {
470
+ // For reversed sequences: highest number = start + total - 1
471
+ // Then subtract entries on previous pages
472
+
473
+ // Get total entries count
474
+ $ total_entries = 0 ;
475
+
476
+ // Always use GFAPI for accurate count in reverse mode
477
+ if ( $ context ->view ->form ) {
478
+ $ search_criteria = array ( 'status ' => 'active ' );
479
+ $ form_id = $ context ->view ->form ->ID ;
480
+
481
+ if ( $ form_id ) {
482
+ // Count all entries for this form - this is the most reliable method
483
+ $ total_entries = \GFAPI ::count_entries ( $ form_id , $ search_criteria );
484
+ }
485
+ }
486
+
487
+ // Only use view's collection as a fallback if GFAPI didn't work
488
+ if ( $ total_entries <= 0 ) {
489
+ $ request = gravityview ()->request ;
490
+ $ entries_collection = $ context ->view ->get_entries ( $ request );
491
+ $ total_entries = $ entries_collection ->total ();
492
+ }
493
+
494
+ // Final fallback - if still no total, assume at least 1
495
+ if ( $ total_entries <= 0 ) {
496
+ $ total_entries = 1 ;
283
497
}
498
+
499
+ $ entries_before = $ pagenum * $ pagesize ;
500
+
501
+ // Highest number in sequence = start + (total - 1)
502
+ // Current page starts at: highest - entries_before
503
+ // Formula: start + (total - 1) - entries_before = start + total - entries_before - 1
504
+ return $ context ->field ->start + $ total_entries - $ entries_before - 1 ;
505
+ } else {
506
+ // For normal sequences: calculate based on page position
507
+ $ entries_before = $ pagenum * $ pagesize ;
508
+
509
+ // Calculate: entries_before + start_value
510
+ return $ entries_before + $ context ->field ->start ;
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Get the next sequence number and update the counter for subsequent calls.
516
+ *
517
+ * @since TODO
518
+ *
519
+ * @param array $startlines Reference to the static startlines cache.
520
+ * @param string $context_key The unique context key.
521
+ * @param \GV\Template_Context $context The template context.
522
+ *
523
+ * @return int The current sequence number.
524
+ */
525
+ private function get_next_sequence_number ( &$ startlines , $ context_key , $ context ) {
526
+ // Get the current sequence number before incrementing/decrementing
527
+ $ sequence_number = $ startlines [ $ context_key ];
528
+
529
+ // Update the counter for the next call
530
+ if ( $ context ->field ->reverse ) {
531
+ $ startlines [ $ context_key ]--;
532
+ } else {
533
+ $ startlines [ $ context_key ]++;
284
534
}
285
535
286
- return $ context ->field ->reverse ? $ startlines [ $ context_key ]-- : $ startlines [ $ context_key ]++;
536
+ /**
537
+ * Filter the sequence number before it's displayed.
538
+ *
539
+ * This allows developers to customize the sequence number output,
540
+ * for example to add prefixes, suffixes, or custom formatting.
541
+ *
542
+ * @since TODO
543
+ *
544
+ * @param int $sequence_number The calculated sequence number.
545
+ * @param \GV\Template_Context $context The template context.
546
+ * @param string $context_key The unique context key.
547
+ */
548
+ $ sequence_number = apply_filters ( 'gravityview/field/sequence/value ' , $ sequence_number , $ context , $ context_key );
549
+
550
+ return $ sequence_number ;
287
551
}
288
552
}
289
553
0 commit comments