From 1963c5395068ec8e440067863e170e854f8baa34 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 5 Nov 2025 16:34:44 -0700 Subject: [PATCH 1/5] Allow custom tooltip date formats Changelog: fixed --- .../lib/utils/datetime/timeago_utility.js | 15 +++++++++++++-- app/helpers/application_helper.rb | 17 +++++++++++++---- app/views/projects/commits/_committer.html.haml | 4 ++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js index f8ef0b20cada65..b2c0fbd7110d1f 100644 --- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js @@ -1,6 +1,7 @@ import * as timeago from 'timeago.js'; import { diffSec } from 'timeago.js/lib/utils/date'; import { newDate } from '~/lib/utils/datetime/date_calculation_utility'; +import { formatDate } from '~/lib/utils/datetime/date_format_utility'; import { DEFAULT_DATE_TIME_FORMAT, DATE_ONLY_FORMAT, @@ -202,8 +203,18 @@ export const localTimeAgo = (elements, updateTooltip = true) => { function addTimeAgoTooltip() { elements.forEach((el) => { - // Recreate with custom template - el.setAttribute('title', localeDateFormat.asDateTimeFull.format(newDate(el.dateTime))); + const customFormat = el.dataset.tooltipFormat; + const dateTime = newDate(el.dateTime); + + if (customFormat) { + if (localeDateFormat[customFormat]) { + el.setAttribute('title', localeDateFormat[customFormat].format(dateTime)); + } else { + el.setAttribute('title', formatDate(dateTime, customFormat)); + } + } else { + el.setAttribute('title', localeDateFormat.asDateTimeFull.format(dateTime)); + } }); } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fca4359ec04f3d..1a377dc3ff7f9c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -198,22 +198,31 @@ def registry_config # `html_class` argument is provided. # # Returns an HTML-safe String - def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false) + def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false, tooltip_format: :timeago_tooltip) return "" if time.nil? css_classes = [short_format ? 'js-short-timeago' : 'js-timeago'] css_classes << html_class unless html_class.blank? + formatted_time = if tooltip_format == :timeago_tooltip + l(time.to_time.in_time_zone, format: tooltip_format) + elsif tooltip_format.is_a?(Symbol) + time.to_time.getutc.iso8601 + else + time.to_time.in_time_zone.strftime(tooltip_format) + end + content_tag :time, l(time, format: "%b %d, %Y"), class: css_classes.join(' '), - title: l(time.to_time.in_time_zone, format: :timeago_tooltip), + title: formatted_time, datetime: time.to_time.getutc.iso8601, tabindex: '0', - aria: { label: l(time.to_time.in_time_zone, format: :timeago_tooltip) }, + aria: { label: formatted_time }, data: { toggle: 'tooltip', placement: placement, - container: 'body' + container: 'body', + tooltip_format: (tooltip_format != :timeago_tooltip ? tooltip_format.to_s : nil) } end diff --git a/app/views/projects/commits/_committer.html.haml b/app/views/projects/commits/_committer.html.haml index 86574a18554874..a900a99c6b506d 100644 --- a/app/views/projects/commits/_committer.html.haml +++ b/app/views/projects/commits/_committer.html.haml @@ -1,9 +1,9 @@ .committer.gl-text-sm - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - - commit_authored_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom') + - commit_authored_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom', tooltip_format: 'mmm d, yyyy h:MMtt Z') - if commit.different_committer? && commit.committer - commit_committer_link = commit_committer_link(commit) - - commit_committer_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom') + - commit_committer_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom', tooltip_format: 'mmm d, yyyy h:MMtt Z') - commit_committer_avatar = commit_committer_avatar(commit.committer, size: 16, has_tooltip: false) - commit_text = _('%{commit_author_link} authored %{commit_authored_timeago} and %{commit_committer_avatar} %{commit_committer_link} committed %{commit_committer_timeago}') % { commit_author_link: commit_author_link, commit_authored_timeago: commit_authored_timeago, commit_committer_avatar: commit_committer_avatar, commit_committer_link: commit_committer_link, commit_committer_timeago: commit_committer_timeago } - else -- GitLab From f6e8e9def64848528b74e31d53ea669b37b61292 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 5 Nov 2025 17:11:52 -0700 Subject: [PATCH 2/5] Correct linting issues --- app/helpers/application_helper.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1a377dc3ff7f9c..7ef1f1be541a1e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -205,12 +205,12 @@ def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: css_classes << html_class unless html_class.blank? formatted_time = if tooltip_format == :timeago_tooltip - l(time.to_time.in_time_zone, format: tooltip_format) - elsif tooltip_format.is_a?(Symbol) - time.to_time.getutc.iso8601 - else - time.to_time.in_time_zone.strftime(tooltip_format) - end + l(time.to_time.in_time_zone, format: tooltip_format) + elsif tooltip_format.is_a?(Symbol) + time.to_time.getutc.iso8601 + else + time.to_time.in_time_zone.strftime(tooltip_format) + end content_tag :time, l(time, format: "%b %d, %Y"), class: css_classes.join(' '), -- GitLab From 4a36e3dc7a475c5eb0830f344ed087465294083b Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 5 Nov 2025 21:18:03 -0700 Subject: [PATCH 3/5] Add tests for new datetime tooltip formatting --- .../utils/datetime/timeago_utility_spec.js | 35 +++++++++++++++++++ spec/helpers/application_helper_spec.rb | 23 ++++++++++++ 2 files changed, 58 insertions(+) diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js index b3b088fce13e36..33159a7570a525 100644 --- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js +++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js @@ -229,5 +229,40 @@ describe('TimeAgo utils', () => { expect(element.innerText).toBe(text); }); }); + + describe('with custom tooltip format', () => { + beforeEach(() => { + window.gon = { time_display_relative: false }; + }); + + it('uses formatDate when data-tooltip-format is a custom string', () => { + const element = document.querySelector('time'); + element.dataset.tooltipFormat = 'mmm d, yyyy h:MMtt Z'; + + localTimeAgo([element]); + jest.runAllTimers(); + + expect(element.getAttribute('title')).toBe('Feb 18, 2020 10:22pm UTC'); + }); + + it('uses localeDateFormat when data-tooltip-format matches a known format key', () => { + const element = document.querySelector('time'); + element.dataset.tooltipFormat = 'asDateTimeFull'; + + localTimeAgo([element]); + jest.runAllTimers(); + + expect(element.getAttribute('title')).toBe('February 18, 2020 at 22:22:32 GMT'); + }); + + it('uses default asDateTimeFull when no data-tooltip-format is set', () => { + const element = document.querySelector('time'); + + localTimeAgo([element]); + jest.runAllTimers(); + + expect(element.getAttribute('title')).toBe('February 18, 2020 at 22:22:32 GMT'); + }); + }); }); }); diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index f4f95f5bfc461b..735fd993ecff16 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -180,6 +180,29 @@ def element(**arguments) expect(el).to eq('') expect(el.html_safe).to eq('') end + + context 'with custom tooltip_format' do + it 'uses strftime format when tooltip_format is a string' do + timeago_element = element(tooltip_format: '%B %d, %Y at %I:%M%p') + + expect(timeago_element.attr('title')).to eq('July 02, 2015 at 08:23AM') + expect(timeago_element.attr('data-tooltip-format')).to eq('%B %d, %Y at %I:%M%p') + end + + it 'uses ISO8601 format when tooltip_format is a non-default symbol' do + timeago_element = element(tooltip_format: :custom_format) + + expect(timeago_element.attr('title')).to eq('2015-07-02T08:23:00Z') + expect(timeago_element.attr('data-tooltip-format')).to eq('custom_format') + end + + it 'does not set data-tooltip-format when using default :timeago_tooltip' do + timeago_element = element(tooltip_format: :timeago_tooltip) + + expect(timeago_element.attr('title')).to eq('Jul 2, 2015 8:23am') + expect(timeago_element.attr('data-tooltip-format')).to be_nil + end + end end describe 'edited_time_ago_with_tooltip' do -- GitLab From 9475e0213296704a3daa7afe7e740f8ce06692ea Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 5 Nov 2025 21:29:29 -0700 Subject: [PATCH 4/5] Flatten some blocks and do lookups --- .../lib/utils/datetime/timeago_utility.js | 15 +++++++-------- app/helpers/application_helper.rb | 5 +++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js index b2c0fbd7110d1f..bb07760c064bf1 100644 --- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js @@ -205,16 +205,15 @@ export const localTimeAgo = (elements, updateTooltip = true) => { elements.forEach((el) => { const customFormat = el.dataset.tooltipFormat; const dateTime = newDate(el.dateTime); + let title = localeDateFormat.asDateTimeFull.format(dateTime); - if (customFormat) { - if (localeDateFormat[customFormat]) { - el.setAttribute('title', localeDateFormat[customFormat].format(dateTime)); - } else { - el.setAttribute('title', formatDate(dateTime, customFormat)); - } - } else { - el.setAttribute('title', localeDateFormat.asDateTimeFull.format(dateTime)); + if (customFormat && localeDateFormat[customFormat]) { + title = localeDateFormat[customFormat].format(dateTime); + } else if (customFormat) { + title = formatDate(dateTime, customFormat); } + + el.setAttribute('title', title); }); } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7ef1f1be541a1e..fa793e9722a6ea 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -204,9 +204,10 @@ def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: css_classes = [short_format ? 'js-short-timeago' : 'js-timeago'] css_classes << html_class unless html_class.blank? - formatted_time = if tooltip_format == :timeago_tooltip + formatted_time = case tooltip_format + when :timeago_tooltip l(time.to_time.in_time_zone, format: tooltip_format) - elsif tooltip_format.is_a?(Symbol) + when Symbol time.to_time.getutc.iso8601 else time.to_time.in_time_zone.strftime(tooltip_format) -- GitLab From 33d2d9699233d2d3cad198abc495a72bab460686 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 5 Nov 2025 21:34:42 -0700 Subject: [PATCH 5/5] Don't perform unnecessary work until it must be done --- app/assets/javascripts/lib/utils/datetime/timeago_utility.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js index bb07760c064bf1..9bdc78cf2156de 100644 --- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js @@ -205,12 +205,14 @@ export const localTimeAgo = (elements, updateTooltip = true) => { elements.forEach((el) => { const customFormat = el.dataset.tooltipFormat; const dateTime = newDate(el.dateTime); - let title = localeDateFormat.asDateTimeFull.format(dateTime); + let title = null; if (customFormat && localeDateFormat[customFormat]) { title = localeDateFormat[customFormat].format(dateTime); } else if (customFormat) { title = formatDate(dateTime, customFormat); + } else { + title = localeDateFormat.asDateTimeFull.format(dateTime); } el.setAttribute('title', title); -- GitLab