)
export const AnnotationAuthorshipLine = ({
- annotation
+ annotation,
+ showDateLabel
}: {
annotation: Annotation
+ showDateLabel: boolean
}) => (
-
+
{getAnnotationAuthorship(annotation)}
-
- {` • ${getAttributionDateLabel(annotation)}`}
-
+ {showDateLabel && (
+
+ {`• ${getAttributionDateLabel(annotation)}`}
+
+ )}
)
diff --git a/assets/js/dashboard/annotations/annotations.test.ts b/assets/js/dashboard/annotations/annotations.test.ts
index 0b36eb0b2a1b..086c6f08e16d 100644
--- a/assets/js/dashboard/annotations/annotations.test.ts
+++ b/assets/js/dashboard/annotations/annotations.test.ts
@@ -5,8 +5,9 @@ import {
canEditAnnotation,
getAnnotationAuthorship,
getAnnotationGranularity,
- getAnnotationTimeLabel,
- groupAnnotationsByTimeLabel
+ getAnnotationDatetimeGroup,
+ groupAnnotationsByDatetime,
+ allAnnotationsAreFromThisExactDatetime
} from './annotations'
import { Interval } from '../stats/graph/intervals'
import { Role, UserContextValue } from '../user-context'
@@ -199,7 +200,7 @@ describe(`${getAnnotationGranularity.name}`, () => {
})
})
-describe(`${getAnnotationTimeLabel.name}`, () => {
+describe(`${getAnnotationDatetimeGroup.name}`, () => {
// 2025-02-26 is a Wednesday
const dateAnnotation = {
datetime: '2025-02-26',
@@ -215,7 +216,9 @@ describe(`${getAnnotationTimeLabel.name}`, () => {
])(
`date-granularity annotation on ${dateAnnotation.datetime} bucketed to %s yields %s`,
(interval, expected) => {
- expect(getAnnotationTimeLabel(dateAnnotation, interval)).toBe(expected)
+ expect(getAnnotationDatetimeGroup(dateAnnotation, interval)).toBe(
+ expected
+ )
}
)
@@ -227,17 +230,19 @@ describe(`${getAnnotationTimeLabel.name}`, () => {
[Interval.month, '2025-02-01'],
[Interval.week, '2025-02-24'],
[Interval.day, '2025-02-26'],
- [Interval.hour, '2025-02-26 10:00:00'],
- [Interval.minute, '2025-02-26 10:30:00']
+ [Interval.hour, '2025-02-26T10:00:00'],
+ [Interval.minute, '2025-02-26T10:30:00']
])(
`minute granularity annotation with datetime ${minuteAnnotation.datetime} bucketed to %s yields %s`,
(interval, expected) => {
- expect(getAnnotationTimeLabel(minuteAnnotation, interval)).toBe(expected)
+ expect(getAnnotationDatetimeGroup(minuteAnnotation, interval)).toBe(
+ expected
+ )
}
)
})
-describe(`${groupAnnotationsByTimeLabel.name}`, () => {
+describe(`${groupAnnotationsByDatetime.name}`, () => {
const dateGranularity = AnnotationGranularity.date
const annotations = [
{ id: 1, datetime: '2025-02-24 00:00:00', granularity: dateGranularity }, // Mon
@@ -246,7 +251,7 @@ describe(`${groupAnnotationsByTimeLabel.name}`, () => {
]
it('groups annotations by day when the interval is day', () => {
- const grouped = groupAnnotationsByTimeLabel(annotations, Interval.day)
+ const grouped = groupAnnotationsByDatetime(annotations, Interval.day)
expect(Object.keys(grouped).sort()).toEqual([
'2025-02-24',
@@ -259,7 +264,7 @@ describe(`${groupAnnotationsByTimeLabel.name}`, () => {
})
it('collapses annotations from the same week into one bucket', () => {
- const grouped = groupAnnotationsByTimeLabel(annotations, Interval.week)
+ const grouped = groupAnnotationsByDatetime(annotations, Interval.week)
expect(Object.keys(grouped).sort()).toEqual(['2025-02-24', '2025-03-03'])
expect(grouped['2025-02-24']!.map((a) => a.id)).toEqual([1, 2])
@@ -267,7 +272,7 @@ describe(`${groupAnnotationsByTimeLabel.name}`, () => {
})
it('collapses annotations from the same month into one bucket', () => {
- const grouped = groupAnnotationsByTimeLabel(annotations, Interval.month)
+ const grouped = groupAnnotationsByDatetime(annotations, Interval.month)
expect(Object.keys(grouped).sort()).toEqual(['2025-02-01', '2025-03-01'])
expect(grouped['2025-02-01']!.map((a) => a.id)).toEqual([1, 2])
@@ -281,17 +286,58 @@ describe(`${groupAnnotationsByTimeLabel.name}`, () => {
{ id: 12, datetime: '2025-02-26 10:00:00', granularity: dateGranularity }
]
- const grouped = groupAnnotationsByTimeLabel(sameDay, Interval.day)
+ const grouped = groupAnnotationsByDatetime(sameDay, Interval.day)
expect(grouped['2025-02-26']!.map((a) => a.id)).toEqual([10, 11, 12])
})
it('returns an empty object when there are no annotations', () => {
expect(
- groupAnnotationsByTimeLabel(
+ groupAnnotationsByDatetime(
[] as { datetime: string; granularity: AnnotationGranularity }[],
Interval.day
)
).toEqual({})
})
})
+
+describe(`${allAnnotationsAreFromThisExactDatetime.name}`, () => {
+ it('returns true when every annotation shares the given datetime', () => {
+ expect(
+ allAnnotationsAreFromThisExactDatetime(
+ [
+ { datetime: '2025-02-26T10:30:00' },
+ { datetime: '2025-02-26T10:30:00' }
+ ],
+ '2025-02-26T10:30:00'
+ )
+ ).toBe(true)
+ })
+
+ it('returns true when every annotation shares the given date', () => {
+ expect(
+ allAnnotationsAreFromThisExactDatetime(
+ [{ datetime: '2026-07-20' }, { datetime: '2026-07-20' }],
+ '2026-07-20'
+ )
+ ).toBe(true)
+ })
+
+ it('returns false when some annotation has a different datetime', () => {
+ expect(
+ allAnnotationsAreFromThisExactDatetime(
+ [
+ { datetime: '2025-02-26T10:30:00' },
+ { datetime: '2025-02-26T10:31:00' }
+ ],
+ '2025-02-26T10:30:00'
+ )
+ ).toBe(false)
+ })
+
+ it('returns true when there are no annotations', () => {
+ expect(
+ allAnnotationsAreFromThisExactDatetime([], '2025-02-26T10:30:00')
+ ).toBe(true)
+ })
+})
diff --git a/assets/js/dashboard/annotations/annotations.ts b/assets/js/dashboard/annotations/annotations.ts
index 4ba1faf96606..b848ba056285 100644
--- a/assets/js/dashboard/annotations/annotations.ts
+++ b/assets/js/dashboard/annotations/annotations.ts
@@ -37,9 +37,9 @@ export type Annotation = {
note: string
id: number
- /** datetime in site timezone, example 2025-02-26 10:00:00 */
+ /** datetime in site timezone, example 2025-02-26T10:00:00 */
inserted_at: string
- /** datetime in site timezone, example 2025-02-26 10:00:00 */
+ /** datetime in site timezone, example 2025-02-26T10:00:00 */
updated_at: string
} & AnnotationOwnership
@@ -139,7 +139,7 @@ export function canAddAnnotation({
}
}
-export const getAnnotationTimeLabel = (
+export const getAnnotationDatetimeGroup = (
annotation: Pick
,
interval: Interval
): string => {
@@ -175,24 +175,24 @@ export const getAnnotationTimeLabel = (
case Interval.hour: {
const [dateYYYYMMDD, timeHHMMSS] = annotation.datetime.split('T')
// floors time to hour
- return `${dateYYYYMMDD} ${timeHHMMSS.substring(0, 'HH'.length)}:00:00`
+ return `${dateYYYYMMDD}T${timeHHMMSS.substring(0, 'HH'.length)}:00:00`
}
case Interval.minute:
- return annotation.datetime.split('T').join(' ')
+ return annotation.datetime
}
}
}
}
-export const groupAnnotationsByTimeLabel = <
+export const groupAnnotationsByDatetime = <
T extends Pick
>(
annotations: T[],
interval: Interval
): Record => {
return annotations.reduce>((acc, annotation) => {
- const timeLabel = getAnnotationTimeLabel(annotation, interval)
- return { ...acc, [timeLabel]: [...(acc[timeLabel] ?? []), annotation] }
+ const label = getAnnotationDatetimeGroup(annotation, interval)
+ return { ...acc, [label]: [...(acc[label] ?? []), annotation] }
}, {})
}
@@ -210,6 +210,11 @@ export const getAnnotationGranularity = (
}
}
+export const allAnnotationsAreFromThisExactDatetime = (
+ annotations: Pick[],
+ datetime: string
+): boolean => annotations.every((a) => a.datetime === datetime)
+
export const getApiFormattedPayload = ({
granularity,
datetime,
diff --git a/assets/js/dashboard/annotations/hover-annotations-list.tsx b/assets/js/dashboard/annotations/hover-annotations-list.tsx
index 2acc232c6103..01522197d943 100644
--- a/assets/js/dashboard/annotations/hover-annotations-list.tsx
+++ b/assets/js/dashboard/annotations/hover-annotations-list.tsx
@@ -1,5 +1,8 @@
import React from 'react'
-import { Annotation } from './annotations'
+import {
+ Annotation,
+ allAnnotationsAreFromThisExactDatetime
+} from './annotations'
import {
AnnotationAuthorshipLine,
AnnotationItemRow,
@@ -10,20 +13,29 @@ import {
const MAX_PREVIEW = 2
export const HoverAnnotationsList = ({
+ annotationDatetime,
annotations
}: {
+ annotationDatetime: string
annotations: Annotation[]
}) => {
const preview = annotations.slice(0, MAX_PREVIEW)
const extra = annotations.length - MAX_PREVIEW
+ const showDateLabel = !allAnnotationsAreFromThisExactDatetime(
+ preview,
+ annotationDatetime
+ )
return (
<>
{preview.map((annotation) => (
-
-
+
diff --git a/assets/js/dashboard/annotations/interactive-annotations-list.tsx b/assets/js/dashboard/annotations/interactive-annotations-list.tsx
index 987007df4d45..fd01220e74df 100644
--- a/assets/js/dashboard/annotations/interactive-annotations-list.tsx
+++ b/assets/js/dashboard/annotations/interactive-annotations-list.tsx
@@ -1,5 +1,9 @@
import React, { ReactNode } from 'react'
-import { Annotation, canEditAnnotation } from './annotations'
+import {
+ Annotation,
+ allAnnotationsAreFromThisExactDatetime,
+ canEditAnnotation
+} from './annotations'
import {
AnnotationAuthorshipLine,
AnnotationItemRow,
@@ -17,10 +21,12 @@ const ScrollableArea = (props: { children: ReactNode }) => (
)
export const InteractiveAnnotationsList = ({
+ annotationDatetime,
annotations,
isTouchDevice,
closeTooltip
}: {
+ annotationDatetime: string
annotations: Annotation[]
isTouchDevice: boolean
closeTooltip: () => void
@@ -31,6 +37,10 @@ export const InteractiveAnnotationsList = ({
closeTooltip()
setModal({ type: 'update-annotation', annotation })
}
+ const showDateLabel = !allAnnotationsAreFromThisExactDatetime(
+ annotations,
+ annotationDatetime
+ )
return (
@@ -39,7 +49,10 @@ export const InteractiveAnnotationsList = ({
const editable = canEditAnnotation({ type: annotation.type, user })
const content = (
<>
-
+
{editable && !isTouchDevice && (