Skip to content

Commit e2d74c8

Browse files
zackkatzclaude
andcommitted
Fix Result Number field not respecting Start Number configuration
The Result Number (sequence) field was always starting at 0 instead of using the configured "Start Number" value in View settings. This only affected field display in Views - the {sequence} merge tag worked correctly. Changes: - Add configuration reading in get_sequence() method to load start/reverse settings from View - Refactor get_sequence() into smaller, testable methods - Add comprehensive test coverage including filters, pagination, and single entry views - Consolidate scattered sequence tests into single test file - Fix single entry sequence calculation to respect filters and sort order Fixes #2431 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 2602bc3 commit e2d74c8

File tree

5 files changed

+1391
-278
lines changed

5 files changed

+1391
-278
lines changed

includes/fields/class-gravityview-field-sequence.php

Lines changed: 298 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -220,70 +220,334 @@ public function replace_merge_tag( $matches = array(), $text = '', $form = array
220220
/**
221221
* Calculate the current sequence number for the context.
222222
*
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.
224231
*
225232
* @return int The sequence number for the field/entry within the view results.
226233
*/
227234
public function get_sequence( $context ) {
235+
// Static cache for starting line numbers per view/field combination.
228236
static $startlines = array();
229237

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(
231302
json_encode(
232303
array(
233304
$context->view->get_anchor_id(),
234305
\GV\Utils::get( $context, 'field/UID' ),
235306
)
236307
)
237308
);
309+
}
238310

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+
}
243323

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();
245338

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 );
253341

254-
$total = $context->view->get_entries()->total();
255-
remove_filter( 'gform_gf_query_sql', $callback );
342+
if ( empty( $sql_query ) ) {
343+
return 0;
344+
}
256345

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 );
258348

259-
global $wpdb;
349+
if ( is_null( $results ) ) {
350+
return 0;
351+
}
260352

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+
}
262356

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;
265375
}
376+
);
266377

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();
272382

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 );
277385

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
278438
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;
281441
} 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;
283497
}
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 ]++;
284534
}
285535

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;
287551
}
288552
}
289553

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Beautifully display your Gravity Forms entries. Learn more on [gravitykit.com](h
3434
* File Upload field display on the Edit Entry screen: icons are now aligned with the filename.
3535

3636
#### 🐛 Fixed
37+
* Result Number (sequence) field now properly respects the "Start Number" configuration setting in Views.
3738
* Date Range filters now work correctly when only a start or end date is entered. Also fixes the issue when using the DataTables layout.
3839
* Some Search Field icons were displaying too large.
3940

0 commit comments

Comments
 (0)