diff --git a/lib/GADS/API.pm b/lib/GADS/API.pm index 465817a58..dc3b967c5 100644 --- a/lib/GADS/API.pm +++ b/lib/GADS/API.pm @@ -570,7 +570,7 @@ post '/api/settings/logo' => require_login sub { my $filecheck = GADS::Filecheck->instance; error __x"Files of mimetype {mimetype} are not allowed", mimetype => $filecheck->get_filetype($file) unless $filecheck->is_image($file); - + $site->update({ site_logo => $file->content }); content_type 'application/json'; @@ -584,6 +584,43 @@ post '/api/settings/logo' => require_login sub { ); }; +get '/api/chronology/:id' => require_login sub { + my $user = logged_in_user; + my $current_id = route_parameters->get('id'); + my $page = query_parameters->get('page') // 1; + my $last_record_id = query_parameters->get('last_record_id'); + + my $record = GADS::Record->new( + user => $user, + schema => schema, + ); + + my %params = ( + page => $page, + ); + + $params{last_record} = $last_record_id if $last_record_id; + + $record->find_chronology_id($current_id, %params); + + my $layout = $record->layout; + my $base_url = request->base; + my $last_page = $record->last_chronology_page; + $last_record_id = $record->last_chronology_record->{record_id}; + + trace "Last record ID: $last_record_id"; + + return template 'chronology/chronology.tt' => { + record => $record, + layout_obj => $layout, + page => $page, + last_page => $last_page == $page ? 1 : 0, + last_record_id => $last_record_id, + }, { + layout => undef, # Do not render page header, footer etc + }; +}; + sub _post_dashboard_widget { my $layout = shift; my $user = logged_in_user; @@ -666,7 +703,7 @@ sub _put_dashboard_widget_edit { my $widget = _get_widget_write(route_parameters->get('id'), route_parameters->get('dashboard_id'), $layout, $user); my $body = from_json(request->body); - + $widget->title($body->{'title'}); $widget->static(query_parameters->get('static') ? 1 : 0) if $widget->dashboard->is_shared; diff --git a/lib/GADS/Record.pm b/lib/GADS/Record.pm index 64e8fd5b4..ae361b5aa 100644 --- a/lib/GADS/Record.pm +++ b/lib/GADS/Record.pm @@ -730,7 +730,14 @@ sub find_chronology_id or error __x"Record ID {id} not found", id => $current_id; my $instance_id = $current->instance_id; $self->_set_instance_id($current->instance_id); - $self->_find(current_id => $current_id, chronology => 1, current => $current); + my %args = ( + current_id => $current_id, + chronology => 1, + chronology_page => $options{page} // 1, + ); + $args{last_record_id} = $options{last_record_id} + if defined $options{last_record_id}; + $self->_find(%args); } sub find_draftuser_id @@ -826,6 +833,24 @@ sub clear $self->clear_id_count; } +sub _get_record_for_chronology { + my ( $self, $record, $records, %find ) = @_; + return unless $find{chronology}; + + my @changed; + + $records->rewind($record->edited_time->values->[0], %find); + + $records->fetch_multivalues( + record_ids => [$record->record_id], + retrieved => [$record->{$record->record_id}], + records => [$record], + is_draft => $find{draftuser_id}, + already_seen => $records->already_seen, + chronology => 1, + ); +} + # XXX This whole section is getting messy and duplicating a lot of code from # GADS::Records. Ideally this needs to share the same code. sub _find @@ -838,6 +863,9 @@ sub _find error __"You do not have access to this deleted record" if $find{deleted} && !$self->layout->user_can("purge") && !$GADS::Schema::IGNORE_PERMISSIONS; + my $last_page; + my $last_record_id = $find{last_record_id}; + my %params = ( user => $self->user, layout => $self->layout, @@ -876,10 +904,28 @@ sub _find ); # No linked here so that we get the ones needed in accordance with this loop (could be either) my @prefetches = $records->jpfetch(search => 1, %common); # Still need search in case of view limit - last if !@prefetches && !$first_run; + last unless @prefetches || $first_run; # Inverted - didn't like how the condition was (slightly) confusing. my %options = $find{current_id} || $find{draftuser_id} ? () : (root_table => 'record', no_current => 1); my $search = $records->search_query(linked => 1, chronology => $find{chronology}, rewind => $records->rewind_values, %common, %options); - # Still need search in case of view limit + if ($find{chronology}) { + my $record_search = $self->schema->resultset('Current')->find( + $find{current_id}, + )->records->search( + {}, + { + select => [ 'id' ], + rows => 10, + page => $find{chronology_page} // 1, + result_class => 'DBIx::Class::ResultClass::HashRefInflator', + order_by => { '-desc' => 'created' }, + } + ); + my @ids = map { $_->{id} } $record_search->all; + unshift @ids, $last_record_id if defined $last_record_id; + $last_page = $record_search->pager->last_page == $find{chronology_page} ? 1: 0; + push @$search, { 'record_single.id' => { -in => \@ids } }; + } + # Still need search in case of view limit @prefetches = $records->jpfetch(search => 1, linked => 0, %common, %options); my $root_table; @@ -959,7 +1005,7 @@ sub _find join => [@prefetches], columns => \@columns_fetch, }; - $params->{order_by} = 'record_single.created' + $params->{order_by} = { -desc => 'record_single.created'} if $find{chronology}; my $result = $self->schema->resultset($root_table)->search( [ @@ -1045,7 +1091,7 @@ sub _find set_record_created_user => $self->set_record_created_user, ); # Set data for this original record to the current version (latest) - $self->record($record->{$record_ids[-1]}); + $self->record($record->{$record_ids[0]}); } } else { @@ -1074,23 +1120,58 @@ sub _find if ($find{chronology}) { my @chronology; my $last_record; - foreach my $record (@record_objects) + # First entry - this is the record as it currently stands + if ($find{chronology_page} == 1) { + my $record = $record_objects[0]; + my @changed; + + $self->_get_record_for_chronology($record, $records, %find); + foreach my $column ( @{ $record->columns_render } ) { + next if $column->internal; + my $datum = $record->fields->{ $column->id }; + if($column->type eq 'curval') { + my %new_ids = map { $_->{id} => $_ } @{$datum->values}; + my @values; + foreach my $id (keys %new_ids) + { + my $new_value = $new_ids{$id}; + $new_value->{status} = "current"; + $new_value->{version_id} = $new_value->{record}->record_id; + push @values, $new_value; + } + push @changed, $column->presentation(datum_presentation => $datum->presentation(values => \@values)); + } + else { + push @changed, $column->presentation(datum_presentation => $datum->presentation); + } + } + + push @chronology, + { + editor => $record->edited_user, + datetime => $record->edited_time, + changed => \@changed, + }; + } else { + unshift @record_objects, $self->last_chronology_record if $self->last_chronology_record; + } + # Show all changes from the original - ignore the last one as this will become the $last_record for comparison + foreach my $i (0 .. $#record_objects-1) { - $records->rewind($record->edited_time->values->[0]); - $records->fetch_multivalues( - record_ids => [$record->record_id], - retrieved => [$record->{$record->record_id}], - records => [$record], - is_draft => $find{draftuser_id}, - already_seen => $records->already_seen, - chronology => 1, - ); + $record = $record_objects[$i+1]; + + $self->_get_record_for_chronology($record, $records, %find); + $last_record = $record; + + $record = $record_objects[$i]; + $self->_get_record_for_chronology($record, $records, %find); + my @changed; foreach my $column (@{$record->columns_render}) { next if $column->internal; my $datum = $record->fields->{$column->id}; - my $last_datum = $last_record && $last_record->fields->{$column->id}; + my $last_datum = $last_record->fields->{$column->id}; if ( (!$last_record && !$datum->blank) @@ -1102,8 +1183,6 @@ sub _find my %old_ids = map { $_->{id} => $_ } @{$last_datum->values}; my %new_ids = map { $_->{id} => $_ } @{$datum->values}; my @values; - # Check each value for whether it is added, removed or changed. - # Look through old values first foreach my $id (keys %old_ids) { my $old_value = $old_ids{$id}; @@ -1135,16 +1214,12 @@ sub _find $new_value->{version_id} = $new_value->{record}->record_id; push @values, $new_value; } - foreach my $v (@values) - { - } - push @changed, $column->presentation(datum_presentation => $datum->presentation(values => \@values)) + push @changed, $column->presentation(datum_presentation => $datum->presentation(values => \@values)); } else { - push @changed, $column->presentation(datum_presentation => $datum->presentation) + push @changed, $column->presentation(datum_presentation => $datum->presentation); } } - } push @chronology, { editor => $record->edited_user, @@ -1153,12 +1228,54 @@ sub _find } if @changed; # There may have been changes, but not ones the user has access to $last_record = $record; } - $self->_set_chronology(\@chronology); + # If we're on the last page, return the record state as it was created + if($last_page) { + my $record = $record_objects[-1]; + $self->_get_record_for_chronology($record, $records, %find); + + my @changed; + foreach my $column ( @{ $record->columns_render } ) { + next if $column->internal; + my $datum = $record->fields->{ $column->id }; + if($column->type eq 'curval') { + my %new_ids = map { $_->{id} => $_ } @{$datum->values}; + my @values; + foreach my $id (keys %new_ids) + { + my $new_value = $new_ids{$id}; + $new_value->{status} = "created"; + $new_value->{version_id} = $new_value->{record}->record_id; + push @values, $new_value; + } + push @changed, $column->presentation(datum_presentation => $datum->presentation(values => \@values)); + } + else { + push @changed, $column->presentation(datum_presentation => $datum->presentation); + } + } + push @chronology, + { + editor => $record->edited_user, + datetime => $record->edited_time, + changed => \@changed, + }; + } + $self->_set_last_chronology_page($last_page // 0); + $self->_set_last_chronology_record($last_record) if $last_record; + $self->_set_chronology([@chronology]); } $self; # Allow chaining } +has last_chronology_record => ( + is => 'rwp', +); + +has last_chronology_page => ( + is => 'rwp', +); + has chronology => ( is => 'rwp', ); @@ -2418,7 +2535,7 @@ sub _field_write # child records. This is used in chronology view to # ensure the correct versions at the time of record # edit are used - version_datetime => $created, + version_datetime => $created, ); push @{$self->_records_to_write_after}, $record if $record->is_edited; diff --git a/lib/GADS/Role/Presentation/Datum/Person.pm b/lib/GADS/Role/Presentation/Datum/Person.pm index bf6f606f1..22c356e5a 100644 --- a/lib/GADS/Role/Presentation/Datum/Person.pm +++ b/lib/GADS/Role/Presentation/Datum/Person.pm @@ -16,17 +16,19 @@ sub _presentation_details { }; } - for ( - [$person->{freetext1}, $site->register_freetext1_name], - [$person->{freetext2}, $site->register_freetext2_name] - ) { - next unless $_->[0]; - - push @details, { - definition => $_->[1], - value => $_->[0], - type => 'text' - }; + if($site) { + for ( + [$person->{freetext1}, $site->register_freetext1_name], + [$person->{freetext2}, $site->register_freetext2_name] + ) { + next unless $_->[0]; + + push @details, { + definition => $_->[1], + value => $_->[0], + type => 'text' + }; + } } return { diff --git a/src/frontend/components/button/_button.scss b/src/frontend/components/button/_button.scss index f3de02843..3cceb605b 100644 --- a/src/frontend/components/button/_button.scss +++ b/src/frontend/components/button/_button.scss @@ -547,3 +547,7 @@ button.close { color: lighten($brand-primary, 0.1); } } + +.btn-js-load-chronology { + @extend .mt-3; +} diff --git a/src/frontend/components/button/lib/component.ts b/src/frontend/components/button/lib/component.ts index 93d852185..f51f0a0d9 100644 --- a/src/frontend/components/button/lib/component.ts +++ b/src/frontend/components/button/lib/component.ts @@ -111,6 +111,12 @@ class ButtonComponent extends Component { createCancelButton(el); }); }); + map.set("btn-js-load-chronology", (el) => { + import(/* webpackChunkName: "load-more-chronology-button" */ "./load-more-chronology-button") + .then(({default: createLoadMoreChronologyButton}) => { + createLoadMoreChronologyButton(el); + }); + }); ButtonComponent.staticButtonsMap = map; } diff --git a/src/frontend/components/button/lib/load-more-chronology-button.test.ts b/src/frontend/components/button/lib/load-more-chronology-button.test.ts new file mode 100644 index 000000000..223d8fd6c --- /dev/null +++ b/src/frontend/components/button/lib/load-more-chronology-button.test.ts @@ -0,0 +1,18 @@ +/* eslint-disable */ +import "../../../testing/globals.definitions"; +import { describe, it, expect } from '@jest/globals'; +import {LoadMoreChronologyButton, default as createLoadMoreChronologyButton} from './load-more-chronology-button'; + +describe('LoadMoreChronologyButton', () => { + it('should create a button', () => { + // Mock jQuery elements + const $button = $(''); + const $target = $('
'); + const $spinner = $(''); + $('body').append($button, $target, $spinner); + + const btn = createLoadMoreChronologyButton($button); + + expect(btn).toBeInstanceOf(LoadMoreChronologyButton); + }); +}); diff --git a/src/frontend/components/button/lib/load-more-chronology-button.ts b/src/frontend/components/button/lib/load-more-chronology-button.ts new file mode 100644 index 000000000..d2b66f060 --- /dev/null +++ b/src/frontend/components/button/lib/load-more-chronology-button.ts @@ -0,0 +1,94 @@ +import { initializeRegisteredComponents } from "component"; +import ErrorHandler from "util/errorHandler"; + +/** + * Button to load more chronology items + */ +export class LoadMoreChronologyButton { + private page: number = 1; + private $target: JQuery; + private $spinner: JQuery; + private errorHandler: ErrorHandler; + + /** + * Create a new LoadMoreChronologyButton instance + * @param $el The element to use as the button + */ + constructor(private $el: JQuery) { + // Where to put the chronology entries + this.$target = $(".chronology"); + // Loading spinner + this.$spinner = $(".chronology_spinner"); + // Error handler - attached to the chronology container + this.errorHandler = new ErrorHandler(this.$target[0]); + // Initialize the button + this.init(); + } + + /** + * Initialize the button and set up event handlers + */ + private init() { + // Set up the click event handler for the button + this.$el.on("click", async () => { + // When the button is clicked, fetch the next page of chronology data + await this.fetchChronology(); + }); + // Fetch the first page of chronology data on initialization + this.fetchChronology(); + } + + /** + * Fetch the next page of chronology data and append it to the chronology list. + * @returns {Promise} A promise that resolves when the chronology data has been fetched and appended. + */ + private async fetchChronology(): Promise { + // Disable the button + this.$el.prop("disabled", true); + // Get the record ID from the button's data attribute + const recordId = this.$el.data("record-id"); + // Get the current page number + const page = this.page; + const lastRecordId = $("[data-last-id]").last().data("last-id"); + // Download the chronology data for the next page + const url = `/api/chronology/${recordId}?page=${page}&last_record_id=${lastRecordId}`; + try { + // Show the spinner while loading + this.$spinner.show(); + // Fetch the data + const response = await fetch(url); + // Check if the response is successful + if (!response.ok) { + // display an error message using the error handler + if(response.status !== 400) { + this.errorHandler.addError(`Failed to load chronology data. Server responded with status: ${response.status}`); + } + return; + } + // If successful, get the response data as text + const html = await response.text(); + // Append the new data to the chronology list + this.$target.append(html); + // Initialize any new components in the newly loaded data (e.g., tooltips, popovers) + initializeRegisteredComponents(this.$target[0]); + // Increment the page number for the next fetch + this.page += 1; + } catch (error) { + // If there is an error, use the error handler to display an error message + this.errorHandler.addError(`An error occurred while loading chronology data. Please try again. Error: ${error}`); + } finally { + // Hide the spinner + this.$spinner.hide(); + // Re-enable the button + if(!$("[data-last-page]").length) { + this.$el.prop("disabled", false); + } + } + } +} + +const createLoadMoreChronologyButton = (el: JQuery) => { + return new LoadMoreChronologyButton(el); +}; + +export default createLoadMoreChronologyButton; diff --git a/src/frontend/components/list/_list.scss b/src/frontend/components/list/_list.scss index a8733d905..7b16b2c2f 100644 --- a/src/frontend/components/list/_list.scss +++ b/src/frontend/components/list/_list.scss @@ -135,6 +135,13 @@ .list__value { flex: 1; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + + & > * { + margin-left: 0.25rem; + } } } } diff --git a/src/frontend/css/stylesheets/base/_global.scss b/src/frontend/css/stylesheets/base/_global.scss index 5c6b28d40..35bedbe0e 100644 --- a/src/frontend/css/stylesheets/base/_global.scss +++ b/src/frontend/css/stylesheets/base/_global.scss @@ -110,3 +110,7 @@ table.table-bordered { @extend .border-bottom } } + +.text-primary { + color: $brand-secundary !important; +} diff --git a/src/frontend/js/lib/util/filedrag/lib/filedrag.ts b/src/frontend/js/lib/util/filedrag/lib/filedrag.ts index d0eaef153..481f17e8b 100644 --- a/src/frontend/js/lib/util/filedrag/lib/filedrag.ts +++ b/src/frontend/js/lib/util/filedrag/lib/filedrag.ts @@ -68,7 +68,7 @@ class FileDrag { showElement($("[data-draggable=\"true\"]")); if (this.options.debug) console.log(files); showElement(this.el); - console.log(files); + if(this.options.debug) console.log(e.originalEvent?.dataTransfer?.files ?? []); if (this.options.allowMultiple) { // For some reason the function will not accept a FileList, so we convert it to an array const f2 = Array.from(files); diff --git a/t/003_chronology.t b/t/003_chronology.t index b67adffd6..abb15115c 100644 --- a/t/003_chronology.t +++ b/t/003_chronology.t @@ -57,29 +57,30 @@ $record->clear; $record->find_chronology_id(1); my @changed = @{$record->chronology}; - is(@changed, 3, "Correct number of total versions"); + # This includes the initial write, which is shown as a change of all fields, and the two subsequent edits, and finally the current values + is(@changed, 4, "Correct number of total versions"); # Initial write my @changes = @{(shift @changed)->{changed}}; is(@changes, 2, "Correct number of changes"); my $changed_string = shift @changes; - is($changed_string->{name_short}, 'L1string1', "Showing initial string as change"); + ok(defined $changed_string->{string1}, "Showing initial string as change"); my $changed_integer = shift @changes; - is($changed_integer->{name_short}, 'L1integer1', "Showing initial integer as change"); + ok(defined $changed_integer->{integer1}, "Showing initial integer as change"); # First change @changes = @{(shift @changed)->{changed}}; is(@changes, 2, "Correct number of changes"); $changed_string = shift @changes; - is($changed_string->{name_short}, 'L1string1', "Showing change of string"); + ok(defined $changed_string->{string1}, "Showing change of string"); $changed_integer = shift @changes; - is($changed_integer->{name_short}, 'L1integer1', "Showing change of integer"); + ok(defined $changed_integer->{integer1}, "Showing change of integer"); # Second change @changes = @{(shift @changed)->{changed}}; is(@changes, 1, "Correct number of changes"); $changed_integer = shift @changes; - is($changed_integer->{name_short}, 'L1integer1', "Showing change of integer in second edit"); + ok(defined $changed_integer->{integer1}, "Showing change of integer in second edit"); } # Check view limits. If the user has view limits applied to their account, they @@ -152,19 +153,20 @@ $record->clear; $record->find_chronology_id(1); my @changed = @{$record->chronology}; - is(@changed, 2, "Correct number of total versions"); + # This includes the initial write, which is shown as a change of all fields, and the two subsequent edits, and finally the current values + is(@changed, 3, "Correct number of total versions"); # Initial write my @changes = @{(shift @changed)->{changed}}; is(@changes, 1, "Correct number of changes"); my $changed_string = shift @changes; - is($changed_string->{name_short}, 'L1string1', "Showing initial string as change"); + ok(defined $changed_string->{string1}, "Showing initial string as change"); # First change @changes = @{(shift @changed)->{changed}}; is(@changes, 1, "Correct number of changes"); $changed_string = shift @changes; - is($changed_string->{name_short}, 'L1string1', "Showing change of string"); + ok(defined $changed_string->{string1}, "Showing change of string"); # Second change not shown as integer not visible } diff --git a/views/chronology.tt b/views/chronology.tt index 06d3d5caf..30819e223 100644 --- a/views/chronology.tt +++ b/views/chronology.tt @@ -1,46 +1,15 @@ [% - PROCESS snippets/datum.tt; - PROCESS snippets/record_readmore.tt; - # add standardized page header INCLUDE layouts/page_header.tt title = "Chronology of edits for record " _ record.current_id; %]
- [% - FOREACH version IN record.chronology; - initial = loop.first ? 1 : 0; - action_datetime = version.datetime.as_string; - action_type = initial ? 'created' : 'updated'; - action_by = version.editor.id ? " by " _ version.editor.as_string : ''; - - %] -
-
-

- [% action_datetime _ " - " _ " record " _ action_type _ action_by | html %] -

-
- -
-
-
-
    - [% - FOREACH field IN version.changed; - label_changed_to = field.type == "curval" OR initial ? '' : 'changed to '; - %] -
  • - [% field.name | html %] - [% label_changed_to; render_datum(field); %] -
  • - [% END %] -
-
-
-
+
+
+
+
Loading...
- [% END %] +
diff --git a/views/chronology/chronology.tt b/views/chronology/chronology.tt new file mode 100644 index 000000000..c12bdb841 --- /dev/null +++ b/views/chronology/chronology.tt @@ -0,0 +1,44 @@ +[% + PROCESS snippets/datum.tt; + PROCESS snippets/record_readmore.tt; +%] + +[% + FOREACH version IN record.chronology; + initial = loop.first ? page == 1 : 0; + action_datetime = version.datetime.as_string; + final_page = loop.last ? last_page : 0; + action_type = initial ? 'Current Version' : final_page ? 'created' : 'updated'; + action_by = version.editor.id ? " by " _ version.editor.as_string : ''; + next_record_id = loop.last ? last_record_id : undef; +%] +
+
+

+ [% IF initial %] + Current Version + [% ELSE %] + [% action_datetime _ " - " _ " record " _ action_type _ action_by | html %] + [% END %] +

+
+ +
+
+
+
    + [% + FOREACH field IN version.changed; + label_changed_to = field.type == "curval" OR initial ? '' : final_page ? 'Set as: ' : 'Changed to: '; + %] +
  • + [% field.name | html %] + [% label_changed_to; render_datum(field); %] +
  • + [% END %] +
+
+
+
+
+[% END %]