%# BEGIN BPS TAGGED BLOCK {{{
%#
%# COPYRIGHT:
%#
%# This software is Copyright (c) 1996-2026 Best Practical Solutions, LLC
%#                                          <sales@bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
%#
%#
%# LICENSE:
%#
%# This work is made available to you under the terms of Version 2 of
%# the GNU General Public License. A copy of that license should have
%# been provided with this software, but in any event can be snarfed
%# from www.gnu.org.
%#
%# This work is distributed in the hope that it will be useful, but
%# WITHOUT ANY WARRANTY; without even the implied warranty of
%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%# General Public License for more details.
%#
%# You should have received a copy of the GNU General Public License
%# along with this program; if not, write to the Free Software
%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
%# 02110-1301 or visit their web page on the internet at
%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
%#
%#
%# CONTRIBUTION SUBMISSION POLICY:
%#
%# (The following paragraph is not intended to limit the rights granted
%# to you to modify and distribute this software under the terms of
%# the GNU General Public License and is only of importance to you if
%# you choose to contribute your changes and enhancements to the
%# community by submitting them to Best Practical Solutions, LLC.)
%#
%# By intentionally submitting any modifications, corrections or
%# derivatives to this work, or any other work intended for use with
%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
%# you are the copyright holder for those contributions and you grant
%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
%# royalty-free, perpetual, license to use, copy, create derivative
%# works based on those contributions, and sublicense and distribute
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
<%ARGS>
$Object
$ShowHeaders       => 0
$ShowTitle         => 1
$ShowDisplayModes  => 1
$ScrollShowHistory => 0
$SingleTransaction => 0
$Title             => loc('History')
$FilterURL         => ''
$FilterTarget      => ''
$ShowHistory       => $ScrollShowHistory ? 'scroll' : RT->Config->Get( 'ShowHistory', $session{CurrentUser} )
</%ARGS>
<%INIT>
my $record_type = $Object->RecordType;
my $histid      = "\L$record_type\E-" . $Object->id . "-history";
my $container_id = 'history-container-' . Digest::MD5::md5_hex(time . {} . $$ . rand(1024));

my $show_filtering = 0;
my $load_with_helper = 0;
my $is_selfservice = $m->request_comp->path =~ m{^/SelfService/};

if ($Object && ($Object->isa('RT::Ticket') || $Object->isa('RT::Asset'))) {
    # Only show filtering for tickets and assets, but not in SelfService
    $show_filtering = !$is_selfservice;
    $load_with_helper = 1;
}

$FilterURL ||= RT->Config->Get('WebPath')
    . ( $is_selfservice ? "/SelfService" : "" )
    . "/Helpers/"
    . ( $Object->isa('RT::Ticket') ? 'Ticket' : 'Asset' )
    . "HistoryPage"
    . ( $ShowHistory eq 'page' ? '' : '?loadAll=1' );

$FilterTarget ||= "#$container_id";
</%INIT>
<div class="history <% lc $record_type %>" id="<% $histid %>" data-history-mode="<% $ShowHistory %>">
<%perl>

my $oldestTransactionsFirst;
if ( my $reverse_txns = $ARGS{ReverseTxns} // $DECODED_ARGS->{ReverseTxns} ) {
    $oldestTransactionsFirst = $reverse_txns eq 'ASC' ? 1 : 0;
}
else {
    $oldestTransactionsFirst = RT->Config->Get("OldestTransactionsFirst", $session{CurrentUser});
}


if ( $ShowDisplayModes or $ShowTitle or $ScrollShowHistory ) {
    my $title = $ShowTitle ? $Title : '&nbsp;';
    my @elements;
    if ( $ScrollShowHistory ) {
        push( @elements, qq{<span id="LoadAllHistoryContainer">} .
                         qq{<a href="#" id="LoadAllHistory">} .
                         loc('Load all history') .
                         qq{</a>} .
                         qq{</span>} );
    }

    if ( $ShowDisplayModes ) {
        if ( RT->Config->Get( 'QuoteFolding', $session{CurrentUser} ) ) {
            my $open_all  = $m->interp->apply_escapes( loc("Show all quoted text"), 'j' );
            my $open_html = $m->interp->apply_escapes( loc("Show all quoted text"), 'h' );
            my $close_all = $m->interp->apply_escapes( loc("Hide all quoted text"), 'j' );
            push( @elements, qq{<a href="#" class="toggle-quoted-text" data-direction="open" } .
                             qq{onclick="return toggle_all_folds(this, $open_all, $close_all);"} .
                             qq{>$open_html</a>} );
        }

        # Don't show header options in SelfService
        unless ($is_selfservice) {
            my $brief_text = $m->interp->apply_escapes( loc("Show brief headers") );
            my $full_text  = $m->interp->apply_escapes( loc("Show full headers") );
            push @elements,
                qq{<a href="#" hx-boost="false" class="history-show-headers" data-history-headers-brief="$brief_text" data-history-headers-full="$full_text" data-history-headers="}
                . ( $ShowHeaders ? 0           : 1 ) . q{">}
                . ( $ShowHeaders ? $brief_text : $full_text )
                . qq{</a>};
        }
    }

    # Don't need to reverse history when showing a single transaction
    unless ( $SingleTransaction ) {
        # Calculate reverse transaction value
        my $reverse_txns = $ARGS{'ReverseTxns'} // $DECODED_ARGS->{ReverseTxns};
        if ($reverse_txns) {
            # If we got something, reverse it for the link
            $reverse_txns = $reverse_txns eq 'ASC' ? 'DESC' : 'ASC';
        }
        else {
            # Default the link to the opposite of the config setting
            # Oldest Txns first is ASC, so reverse it for this option default
            $reverse_txns
                = RT->Config->Get( "OldestTransactionsFirst", $session{'CurrentUser'} ) ? 'DESC' : 'ASC';
        }
        my $reverse_value = $reverse_txns eq 'ASC' ? 'ASC' : 'DESC';

        # For pages without helpers (admin pages), use traditional link
        my $href = $ARGS{ReverseHistoryOrderLink} || do {
            my $reverse_txns = $ARGS{'ReverseTxns'} // $DECODED_ARGS->{ReverseTxns};
            if ($reverse_txns) {
                # If we got something, reverse it for the link
                $reverse_txns = $reverse_txns eq 'ASC' ? 'DESC' : 'ASC';
            }
            else {
                # Default the link to the opposite of the config setting
                # Oldest Txns first is ASC, so reverse it for this option default
                $reverse_txns
                    = RT->Config->Get( "OldestTransactionsFirst", $session{'CurrentUser'} ) ? 'DESC' : 'ASC';
            }
            qq{?ForceShowHistory=1;ReverseTxns=$reverse_txns;id=} . $Object->id;
        };
        if ( $load_with_helper ) {
            push @elements, qq{<a hx-boost="false" class="history-reverse-order" href="#">} . loc("Reverse history order") . qq{</a>};
        }
        else {
            push @elements, qq{<a href="$href#$histid">} . loc("Reverse history order") . qq{</a>};
        }
    }

    # page layout changes can only be seen on the config history page
    push @elements,
          '<a href="'
        . RT->Config->Get('WebPath')
        . '/Admin/Tools/ConfigHistory.html">'
        . loc('Page layout history') . '</a>'
        if $Object->isa('RT::Queue')
        && $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'SuperUser' );

    my $titleright;

    my $has_form;

    # Escape URL-supplied values before interpolating into HTML attributes below.
    my $escape_filter_url     = $m->interp->apply_escapes( $FilterURL,    'h' );
    my $escape_filter_target  = $m->interp->apply_escapes( $FilterTarget, 'h' );
    my $escape_search_history = $m->interp->apply_escapes( $ARGS{SearchHistory} // '', 'h' );

    # Show search form for tickets in SelfService (search only, no transaction filter)
    if ( $is_selfservice && $Object ) {
        $titleright .= qq{<form class="d-inline-block align-top transaction-filter-form" hx-post="$escape_filter_url" hx-target="$escape_filter_target" hx-indicator="#$container_id">};

        if ( RT->Config->Get('FullTextSearch')->{Enable} && $Object->isa('RT::Ticket') ) {
            my $search_icon = GetSVGImage(Name => 'search');
            $titleright .= qq{<span class="input-group d-inline-flex history-search-input align-top">}
                        . qq{<span class="input-group-text">$search_icon</span>}
                        . qq{<input type="text" name="SearchHistory" class="form-control" placeholder="} . loc('Search history...') . qq{" aria-label="} . loc('Search History') . qq{" value="$escape_search_history">}
                        . qq{</span>&nbsp;};
        }

        $titleright .= qq{<input type="hidden" name="id" value="} . $Object->id . qq{">};
        $titleright .= qq{<input type="hidden" name="ReverseTxns" value="}
            . ( $oldestTransactionsFirst ? 'ASC' : 'DESC' ) . q{">};
        $has_form = 1;
    } elsif ($show_filtering) {
        # Full filtering interface for non-SelfService pages
        my $search_icon = GetSVGImage(Name => 'search');
        $titleright .= qq{<form class="d-inline-block transaction-filter-form" hx-post="$escape_filter_url" hx-target="$escape_filter_target" hx-indicator="#$container_id">};

        # Only show search input for tickets (assets don't have searchable content)
        if ( RT->Config->Get('FullTextSearch')->{Enable} && $Object->isa('RT::Ticket') ) {
            $titleright .= qq{<span class="input-group d-inline-flex history-search-input">}
                           . qq{<span class="input-group-text">$search_icon</span>}
                           . qq{<input type="text" name="SearchHistory" class="form-control" placeholder="} . loc('Search history...') . qq{" aria-label="} . loc('Search History') . qq{" value="$escape_search_history">}
                           . qq{</span>&nbsp;};
        }

        $titleright .= qq{<input type="hidden" name="id" value="} . $Object->id . qq{">};
        $titleright .= qq{<input type="hidden" name="ShowHeaders" value="} . ( $ShowHeaders ? 1 : 0 ) . q{">};
        $titleright .= qq{<input type="hidden" name="ReverseTxns" value="}
            . ( $oldestTransactionsFirst ? 'ASC' : 'DESC' ) . q{">};

        if ( $ShowHistory eq 'page' ) {
            $titleright .= qq{<input type="hidden" name="ShowPagination" value="1">};
            $titleright .= qq{<input type="hidden" name="Page" value="1">};
            $titleright .= qq{<input type="hidden" name="focusTransactionId" value="">};
            if ( $ARGS{PerPage} ) {
                my $escape_per_page = $m->interp->apply_escapes( $ARGS{PerPage}, 'h' );
                $titleright .= qq{<input type="hidden" name="PerPage" value="$escape_per_page">};
            }
        }
        $titleright .= $m->scomp('/Elements/TransactionTypeFilterDropdown', %ARGS);
        $has_form = 1;
    }

    if ( @elements ) {
        # build the new link
        my $alt = loc('Options');
        $titleright .= qq{<span class="rt-inline-icon dropdown border rounded"><a id="history-dropdown" class="menu-item" href="#" aria-label="$alt" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">}
        . GetSVGImage(Name => 'gear', Title => $alt)
        . qq{</a><ul class="dropdown-menu dropdown-menu-end">};

        # foreach of the elements, build a new <li>$element</li> and append to the output.
        foreach my $element ( @elements ) {
            $titleright .= qq{<li class="dropdown-item">$element</li>};
        }

        $titleright .= q{</ul></span>};
    }

    $titleright .= qq{</form>} if $has_form;
</%perl>
% $m->callback( CallbackName => 'BeforeTitle', %ARGS, title => \$title, titleright => \$titleright, ARGSRef => \%ARGS );
<& /Widgets/TitleBoxStart, title => $title, titleright_raw => $titleright, class => 'fullwidth' &>
% }

% my $url = '';
% if ( $load_with_helper ) {

<%perl>
my %params = map { $_ => $ARGS{$_} } grep { !blessed $ARGS{$_} } keys %ARGS;
$url
    = RT->Config->Get('WebPath')
    . ($is_selfservice ? "/SelfService" : "")
    . "/Helpers/"
    . ( $Object->isa('RT::Ticket') ? 'Ticket' : 'Asset' )
    . "HistoryPage?"
    . $m->comp( '/Elements/QueryString', %params, id => $Object->id );
</%perl>

<script type="text/javascript">
jQuery(function(){
    var container = document.getElementById(<% $container_id | j%>);
    container.setAttribute('data-oldest-transactions-first', '<% $oldestTransactionsFirst %>');
% if ( !$ScrollShowHistory ) {
    container.setAttribute('data-disable-scroll-loading', '');
% }
    var isLoading = false, // prevent multiple simultaneous load events
        loadDistanceFromBottom = 1500, // to load before bottom of page is reached
        lastTransactionId = null,
        hash = window.location.hash,
        hashTransactionId = null,
        loadAll = false;

    var oldestTransactionsFirst = <% $oldestTransactionsFirst || 0 %>;

    var removeLoadingMessage = function() {
        jQuery('.loading-message').remove();
    };

    var removeLoadLink = function() {
        jQuery('.error-load-history').remove();
    };

    var showLoadingMessage = function() {
        removeLoadingMessage();
        var loadingMessage = jQuery('<span class="loading-message">' +
            loc_key('loading') + '</span>');
        jQuery(".history-container").append(loadingMessage);
    };

    var loadingError = function(reason) {
        removeLoadingMessage();
        container.setAttribute('data-disable-scroll-loading', '');
        removeLoadLink();
        var loadLink = jQuery('<div class="error-load-history">' +
            loc_key('history_scroll_error') + ' ' + reason +
            '<br/><a href="#">' + loc_key('try_again') + '</a></div>');
        jQuery(".history-container").append(loadLink);
    };

    var loadHistoryPage = function() {
        container = document.getElementById(<% $container_id |j %>);
        if (isLoading || !container || container.hasAttribute('data-disable-scroll-loading')) return;

        isLoading = true;
        showLoadingMessage();

        var queryString = '&oldestTransactionsFirst=' + oldestTransactionsFirst;
        var lastTransaction = container.querySelector('.transaction:last-of-type');
        if ( lastTransaction ) {
            lastTransactionId = lastTransaction.dataset.transactionId;
        }
        if (lastTransactionId) queryString += '&lastTransactionId=' + lastTransactionId;
        if (loadAll) queryString += '&loadAll=1';

        // don't load all over and over again
        loadAll = false;

        // check for link to specific transaction and make sure we load enough to focus it
        if (hash) {
            var matches = hash.match(/^#txn-(\d+)$/);
            if (matches) {
                hashTransactionId = matches[1];
                queryString += '&focusTransactionId=' + hashTransactionId;
            }
        }

        let transactions = container.querySelectorAll('.history-container div.transaction');
        htmx.ajax(
            'GET',
            "<% $url |n %>" + queryString, {
                source: '.history-container .loading-message',
                target: '.history-container',
                swap: 'beforeend',
            }).then(() => {
            const new_transactions = container.querySelectorAll('.history-container div.transaction');
            if (new_transactions.length > transactions.length) {
                transactions = new_transactions;
                if (transactions[transactions.length - 1]?.classList.contains('end-of-history-list')) {
                    isLoading = false;
                    loadHistoryPage();
                    return;
                }

                if (jQuery(document).height() <= jQuery(window).height() + loadDistanceFromBottom) {
                    // if there are still space left, automatically load more
                    isLoading = false;
                    loadHistoryPage();
                    return;
                }

                if (hashTransactionId) { // focus transaction if we are following a link to it
                    hashTransactionId = null;
                    location.href = hash;
                }
            }
            else {
                container.setAttribute('data-disable-scroll-loading', '');

                // hide 'Load All' link container if we're done loading
                var loadAllHistoryContainer = jQuery('#LoadAllHistoryContainer');
                loadAllHistoryContainer.hide();
            }

            isLoading = false;
            removeLoadingMessage();

            // make sure we load all if we clicked the "Load All" button while we were already loading
            if (loadAll) loadHistoryPage();
        });
    };

    jQuery(window).scroll(function() {
        if(jQuery(window).scrollTop() >= jQuery(document).height() - jQuery(window).height() - loadDistanceFromBottom) {
            loadHistoryPage();
        }
    });

    jQuery('.history-container').on('click', '.error-load-history a', function(e) {
        e.preventDefault();
        removeLoadLink();
        container.removeAttribute('data-disable-scroll-loading');
        loadHistoryPage();
    });

% if ( $ScrollShowHistory ) {

    var loadAllHistory = function() {
        // hide link container
        var loadAllHistoryContainer = jQuery('#LoadAllHistoryContainer');
        loadAllHistoryContainer.hide();
        loadAll = true;
        loadHistoryPage();
    };

    jQuery('div.history').on('click', '#LoadAllHistory', function(e) {
        e.preventDefault();
        loadAllHistory();
    });

    loadHistoryPage();

    // Catch clicks on unread messages buttons and load any messages not loaded "on scroll"
    jQuery('.new-messages-buttons > a').on('click', function (e) {
        var link = jQuery(this);
        if ( link[0].hash ) {
            hash = link[0].hash;
            loadHistoryPage();
        }
        return true;
    });
% }

});
</script>
% }

<div class="history-container" data-url="<% $url %>" id="<% $container_id %>">
