Ask Reuben

Sort, Keep At Top

When sorting in a TABLE, how can I keep rows at the top?

The ui.Dialog.setGroupBy method was introduced in FGL=4.01.03.  This provides a way to define a grouping column that the rows in your Table will be grouped by.  Rows with the same group value will be kept together.

Any sorting of the table will still keep these rows together and the users sort will be a secondary sort.  It is the SQL equivalent of ORDER BY grouping-column [,sort-column].

Typically the values in the grouping column will not be unique, and will share a lot of common values.  For example city/state/country, or as in my example later a binary value to indicate if selected or not selected.

By selecting an appropriate column, it is possible to ensure that certain rows are always kept at the top of the Table. In the screenshot below, the user has sorted by a Quantity field but the rows that are “Selected” are kept at the top of the table. When a different sort is selected, the “Selected”rows are kept at the top of the table.

The implementation is very simple, simply at the beginning of the dialog add a call to ui.Dialog.setGroupBy with the desired grouping column as the parameter.

The example code for the screenshots is at the end of the article.

Like anything released mid-release, it has not had an Early Access Program to fine tune the concept.  One thing that has been pointed out is that the sort order is equivalent of ORDER BY grouping-column [,sort-column] where sort-column is the table-column chosen by the end-user to sort rows.  How do you implement equivalent of  ORDER BY grouping-column DESC [,sort-column]  ?, that is have the grouping column sorted descending.  The answer is to populate the grouping-column appropriately, so in my case it was 0=selected, 1=unselected, as opposed to the more intuitive 0=unselected, 1=selected.  Another approach you could have used is to add a column comparison function that reverses the sort collation …

CALL DIALOG.setColumnComparisonFunction(grouping-column, FUNCTION reverse_sort)
FUNCTION reverse_sort(s1 STRING, s2 STRING) RETURNS INT
    RETURN util.Strings.collate(s1, s2) * -1
END FUNCTION

… but that is a little more tricky with a PHANTOM column as in my example.

So if you have FGL=4.01.03 experiment with this new ui.Dialog.setGroupBy method, and report any improvements for future releases.

#! example 185.4gIMPORT util

CONSTANT IMAGE_SELECTED = "fa-check-circle"
CONSTANT IMAGE_UNSELECTED = "fa-circle-o"

MAIN
    DEFINE arr DYNAMIC ARRAY OF RECORD
        available BOOLEAN,
        selected_img STRING,
        id INTEGER,
        desc STRING,
        qty1 DECIMAL(11, 2),
        qty2 DECIMAL(11, 2),
        qty3 DECIMAL(11, 2),
        qty4 DECIMAL(11, 2)
    END RECORD
    DEFINE i INTEGER
    DEFINE last_row INTEGER

    WHENEVER ANY ERROR STOP
    DEFER INTERRUPT
    DEFER QUIT
    OPTIONS FIELD ORDER FORM
    OPTIONS INPUT WRAP

    CALL ui.Interface.loadStyles("askreuben185.4st")
    CLOSE WINDOW SCREEN

    FOR i = 1 TO 26
        LET arr[i].id = i
        LET arr[i].desc = ASCII (64 + i), ASCII (96 + i), ASCII (96 + i)
        LET arr[i].qty1 = util.Math.rand(100000) / 100
        LET arr[i].qty2 = util.Math.rand(100000) / 100
        LET arr[i].qty3 = util.Math.rand(100000) / 100
        LET arr[i].qty4 = util.Math.rand(100000) / 100
        LET arr[i].available = 1 -- all rows unselected to stary
        LET arr[i].selected_img = IMAGE_UNSELECTED
    END FOR

    OPEN WINDOW w WITH FORM "askreuben185" ATTRIBUTES(TEXT = "Ask Reuben 185")
    WHILE TRUE
        DISPLAY ARRAY arr TO scr.* ATTRIBUTES(UNBUFFERED)
            BEFORE DISPLAY
                CALL DIALOG.setGroupBy("scr.available") --group arrays on the value of this column
                
                IF last_row > 0 AND last_row <= arr.getLength() THEN
                    CALL DIALOG.setCurrentRow("scr", last_row) -- put cursor back on previously selected row
                END IF

            ON ACTION toggle -- User clicks on this image to change between available/unselected and unavailable/selected
                LET arr[arr_curr()].available = NOT arr[arr_curr()].available
                LET arr[arr_curr()].selected_img = IIF(arr[arr_curr()].available==1, IMAGE_UNSELECTED, IMAGE_SELECTED)
                LET last_row = DIALOG.getCurrentRow("scr")
                EXIT DISPLAY -- exit array to resort
        END DISPLAY
    END WHILE
END MAIN
#! example185.per
LAYOUT
TABLE
{
[f00     ][f01][f02      ][f03      ][f04       ][f05       ][f06     ]
}
END
END
ATTRIBUTES
PHANTOM formonly.available;
IMAGE f00 = formonly.selected_img, TITLE="Selected", ACTION=toggle;
EDIT f01 = formonly.id, TITLE="Id";
EDIT f02 = formonly.desc, TITLE="Description", STRETCH=X;
EDIT f03 = formonly.qty1, TITLE="Quantity 1";
EDIT f04 = formonly.qty2, TITLE="Quantity 2";
EDIT f05 = formonly.qty3, TITLE="Quantity 3";
EDIT f06 = formonly.qty4, TITLE="Quantity 4";

INSTRUCTIONS
SCREEN RECORD scr(available, selected_img, id, desc, qty1, qty2, qty3, qty4)
<!-- example185.4st -->
<?xml version="1.0" encoding="ANSI_X3.4-1968"?>
<StyleList>
  <Style name="Window">
     <StyleAttribute name="windowType" value="normal" />
  </Style>

   <Style name="Table">
     <StyleAttribute name="headerAlignment" value="auto" />
     <StyleAttribute name="alternateRows" value="yes" />
  </Style>
 
</StyleList>