Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: dates issues [DHIS2-18087] #102

Merged
merged 2 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "d2-app-scripts build",
"start": "d2-app-scripts start",
"start:nobrowser": "BROWSER=none d2-app-scripts start",
"test": "d2-app-scripts test",
"test": "TZ=Etc/UTC d2-app-scripts test",
"deploy": "d2-app-scripts deploy",
"lint": "d2-style check",
"lint:staged": "d2-style check --staged",
Expand Down
129 changes: 129 additions & 0 deletions src/components/edit/overview/__tests__/items-list.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import '@testing-library/jest-dom'
import { Provider } from '@dhis2/app-runtime'
import { render } from '@testing-library/react'
import React from 'react'
import { MemoryRouter, Route } from 'react-router-dom'
import { QueryParamProvider } from 'use-query-params'
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'
import { useAppContext } from '../../../../context/app-context/use-app-context.js'
import { testDataExchange } from '../../../../utils/builders.js'
import { EditItemsList } from '../items-list.js'

const mockLastAnalyticsTableSuccess = '2024-07-07T12:00:00.000'
const mockContextPath = 'debug.dhis2.org/dev'

jest.mock('../../../../context/app-context/use-app-context.js', () => ({
useAppContext: jest.fn(() => ({
aggregateDataExchanges: [],
})),
}))

const CONFIG_DEFAULTS = {
baseUrl: 'https://debug.dhis2.org/dev',
apiVersion: '41',
systemInfo: {
lastAnalyticsTableSuccess: mockLastAnalyticsTableSuccess,
serverTimeZoneId: 'Etc/UTC',
contextPath: `https://${mockContextPath}`,
},
}

const setUp = (ui, { timezone = 'Etc/UTC', dateFormat } = {}) => {
const routerParams = ''
const configUpdated = { ...CONFIG_DEFAULTS }
configUpdated.systemInfo.serverTimeZoneId = timezone
configUpdated.systemInfo.dateFormat = dateFormat

const screen = render(
<Provider config={configUpdated}>
<MemoryRouter initialEntries={[routerParams]}>
<QueryParamProvider
ReactRouterRoute={Route}
adapter={ReactRouter6Adapter}
>
{ui}
</QueryParamProvider>
</MemoryRouter>
</Provider>
)

return { screen }
}

describe('exchange card dates', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('shows in yyyy-mm-dd if specified', () => {
const mockExchange = testDataExchange({
created: '2021-10-11T12:00:00',
readDataAccess: true,
})
useAppContext.mockImplementationOnce(() => ({
aggregateDataExchanges: [mockExchange],
}))

const { screen } = setUp(<EditItemsList />, {
dateFormat: 'yyyy-mm-dd',
})
expect(screen.getByText('Created 2021-10-11')).toBeInTheDocument()
})

it('shows in dd-mm-yyyy if specified', () => {
const mockExchange = testDataExchange({
created: '2021-10-11T12:00:00',
readDataAccess: true,
})
useAppContext.mockImplementationOnce(() => ({
aggregateDataExchanges: [mockExchange],
}))

const { screen } = setUp(<EditItemsList />, {
dateFormat: 'dd-mm-yyyy',
})
expect(screen.getByText('Created 11-10-2021')).toBeInTheDocument()
})

it('shows in yyyy-mm-dd by default', () => {
const mockExchange = testDataExchange({
created: '2021-10-11T12:00:00',
readDataAccess: true,
})
useAppContext.mockImplementationOnce(() => ({
aggregateDataExchanges: [mockExchange],
}))

const { screen } = setUp(<EditItemsList />)
expect(screen.getByText('Created 2021-10-11')).toBeInTheDocument()
})

it('pads date values', () => {
const mockExchange = testDataExchange({
created: '2021-01-01T12:00:00',
readDataAccess: true,
})
useAppContext.mockImplementationOnce(() => ({
aggregateDataExchanges: [mockExchange],
}))

const { screen } = setUp(<EditItemsList />)
expect(screen.getByText('Created 2021-01-01')).toBeInTheDocument()
})

// if server is in Vientiane, and client is UTC, then 2021-01-11T01:00 Vientaine is 2021-01-10 UTC
it('displays date based on client time zone', () => {
const mockExchange = testDataExchange({
created: '2021-01-11T01:00:00',
readDataAccess: true,
})
useAppContext.mockImplementationOnce(() => ({
aggregateDataExchanges: [mockExchange],
}))

const { screen } = setUp(<EditItemsList />, {
timezone: 'Asia/Vientiane',
})
expect(screen.getByText('Created 2021-01-10')).toBeInTheDocument()
})
})
49 changes: 37 additions & 12 deletions src/components/edit/overview/items-list.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { useDataMutation, useTimeZoneConversion } from '@dhis2/app-runtime'
import {
useDataMutation,
useTimeZoneConversion,
useConfig,
} from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
import {
Button,
Expand All @@ -18,6 +22,30 @@ import { getNaturalCapitalization } from '../../../utils/helpers.js'
import { DeleteConfirmation } from './delete-confirmation.js'
import styles from './items-list.module.css'

export const getCreatedDateString = ({
fromServerDate,
createdDate,
dateFormat = 'yyyy-mm-dd',
}) => {
const createdClient = fromServerDate(createdDate)
const createdClientDate = new Date(createdClient.getClientZonedISOString())
const year = String(createdClientDate.getUTCFullYear())
const month = String(createdClientDate.getUTCMonth() + 1)
const day = String(createdClientDate.getUTCDate())

if (dateFormat === 'dd-mm-yyyy') {
return `${day.padStart(2, '0')}-${month.padStart(
2,
'0'
)}-${year.padStart(4, '0')}`
}

return `${year.padStart(4, '0')}-${month.padStart(2, '0')}-${day.padStart(
2,
'0'
)}`
}

const IconTextItem = ({ icon, text }) => (
<div className={styles.iconText}>
<div className={styles.icon}>{icon}</div>
Expand Down Expand Up @@ -50,19 +78,16 @@ const AggregateDataExchangeCard = React.memo(({ ade }) => {
}
)
const { canAddExchange, canDeleteExchange, keyUiLocale } = useUserContext()
const { systemInfo = {} } = useConfig()
const { dateFormat } = systemInfo

const { fromServerDate } = useTimeZoneConversion()
const createdClient = fromServerDate(ade?.created)
const createdClientDate = new Date(createdClient.getClientZonedISOString())

// keyUiLocale can be invalid, hence wrap in try/catch
let createdClientDateString
try {
createdClientDateString =
createdClientDate.toLocaleDateString(keyUiLocale)
} catch (e) {
createdClientDateString = createdClientDate.toLocaleDateString('en-GB')
}
const createdClientDateString = getCreatedDateString({
fromServerDate,
createdDate: ade.created,
dateFormat,
keyUiLocale,
})

const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false)
const [sharingSettingsOpen, setSharingSettingsOpen] = useState(false)
Expand Down
107 changes: 107 additions & 0 deletions src/components/view/data-workspace/__tests__/title-bar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import '@testing-library/jest-dom'
import { Provider } from '@dhis2/app-runtime'
import { render } from '@testing-library/react'
import React from 'react'
import { MemoryRouter, Route } from 'react-router-dom'
import { QueryParamProvider } from 'use-query-params'
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'
import { TitleBar } from '../title-bar/index.js'

const mockLastAnalyticsTableSuccess = '2024-07-07T12:00:00.000'
const mockContextPath = 'debug.dhis2.org/dev'

const CONFIG_DEFAULTS = {
baseUrl: 'https://debug.dhis2.org/dev',
apiVersion: '41',
systemInfo: {
lastAnalyticsTableSuccess: mockLastAnalyticsTableSuccess,
serverTimeZoneId: 'Etc/UTC',
contextPath: `https://${mockContextPath}`,
},
}

const setUp = (
ui,
{
timezone = 'Etc/UTC',
lastAnalyticsTableSuccess = mockLastAnalyticsTableSuccess,
} = {}
) => {
const routerParams = ''
const configUpdated = { ...CONFIG_DEFAULTS }
configUpdated.systemInfo.serverTimeZoneId = timezone
configUpdated.systemInfo.lastAnalyticsTableSuccess =
lastAnalyticsTableSuccess

const screen = render(
<Provider config={configUpdated}>
<MemoryRouter initialEntries={[routerParams]}>
<QueryParamProvider
ReactRouterRoute={Route}
adapter={ReactRouter6Adapter}
>
{ui}
</QueryParamProvider>
</MemoryRouter>
</Provider>
)

return { screen }
}

describe('Title Bar (last updated)', () => {
beforeEach(() => {
jest.useFakeTimers('modern')
})

afterEach(() => {
jest.useRealTimers()
})

it('shows time difference without correction if server/client is same time zone', () => {
jest.setSystemTime(new Date('2024-07-07T13:00:00.000').getTime())
const { screen } = setUp(<TitleBar />)
expect(
screen.getByText('Source data was generated an hour ago')
).toBeInTheDocument()
})

// server has last updated of 12:00 in Kampala, our system time is 13:00 UTC (which is 16:00 Kampala)
it('corrects for time zone (Kampala)', () => {
jest.setSystemTime(new Date('2024-07-07T13:00:00.000').getTime())
const { screen } = setUp(<TitleBar />, { timezone: 'Africa/Kampala' })
expect(
screen.getByText('Source data was generated 4 hours ago')
).toBeInTheDocument()
})

// server has last updated of 12:00 in Vientaine, our system time is 13:00 UTC (which is 20:00 Vientiane)
it('corrects for time zone (Vientiane)', () => {
jest.setSystemTime(new Date('2024-07-07T13:00:00.000').getTime())
const { screen } = setUp(<TitleBar />, { timezone: 'Asia/Vientiane' })
expect(
screen.getByText('Source data was generated 8 hours ago')
).toBeInTheDocument()
})

// server has last updated of 12:00 in Oslo, our system time is 13:00 UTC (which is 15:00 Oslo, summer time)
it('corrects for time zone (Oslo Summer)', () => {
jest.setSystemTime(new Date('2024-07-07T13:00:00.000').getTime())
const { screen } = setUp(<TitleBar />, { timezone: 'Europe/Oslo' })
expect(
screen.getByText('Source data was generated 3 hours ago')
).toBeInTheDocument()
})

// server has last updated of 12:00 in Oslo, our system time is 13:00 UTC (which is 14:00 Oslo, summer time)
it('corrects for time zone (Oslo Winter)', () => {
jest.setSystemTime(new Date('2024-01-07T13:00:00.000').getTime())
const { screen } = setUp(<TitleBar />, {
timezone: 'Europe/Oslo',
lastAnalyticsTableSuccess: '2024-01-07T12:00:00.000',
})
expect(
screen.getByText('Source data was generated 2 hours ago')
).toBeInTheDocument()
})
})
22 changes: 11 additions & 11 deletions src/components/view/data-workspace/title-bar/title-bar.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { useConfig } from '@dhis2/app-runtime'
import { useConfig, useTimeZoneConversion } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
import { IconInfo16, IconDimensionDataSet16, Tooltip } from '@dhis2/ui'
import moment from 'moment'
import React from 'react'
import { useExchangeContext } from '../../../../context/index.js'
import styles from './title-bar.module.css'

export const getRelativeTimeDifference = ({ startTimestamp, endTimestamp }) => {
if (!startTimestamp || !endTimestamp) {
export const getRelativeTimeDifference = ({ startTimeDate }) => {
if (!startTimeDate) {
return undefined
}
const startTime = new Date(startTimestamp)
const endTime = new Date(endTimestamp)

return moment(startTime).fromNow(endTime)
return moment(startTimeDate).fromNow(true)
}

// this formats to the styling specified by DHIS2 design principles
Expand All @@ -26,9 +24,12 @@ const formatTimestamp = ({ timestamp, timezone }) => {

const TitleBar = () => {
const { systemInfo } = useConfig()
const { lastAnalyticsTableSuccess, serverDate, serverTimeZoneId } =
systemInfo
const { lastAnalyticsTableSuccess, serverTimeZoneId } = systemInfo
const { exchange } = useExchangeContext()
const { fromServerDate } = useTimeZoneConversion()
const lastAnalyticsTableSuccessClient = fromServerDate(
lastAnalyticsTableSuccess
)
const requestsCount = exchange.source?.requests?.length

return (
Expand Down Expand Up @@ -60,9 +61,8 @@ const TitleBar = () => {
'Source data was generated {{timeDifference}} ago',
{
timeDifference: getRelativeTimeDifference({
startTimestamp:
lastAnalyticsTableSuccess,
endTimestamp: serverDate,
startTimeDate:
lastAnalyticsTableSuccessClient,
}),
}
)}
Expand Down
Loading
Loading