diff --git a/src/__demo__/PeriodDimension.stories.js b/src/__demo__/PeriodDimension.stories.js
index 9c70f91d6..2ac01b27d 100644
--- a/src/__demo__/PeriodDimension.stories.js
+++ b/src/__demo__/PeriodDimension.stories.js
@@ -1,4 +1,4 @@
-import { DataProvider } from '@dhis2/app-runtime'
+import { CustomDataProvider } from '@dhis2/app-runtime'
import { ConfigProvider } from '@dhis2/app-service-config'
import React from 'react'
import PeriodDimension from '../components/PeriodDimension/PeriodDimension.js'
@@ -13,32 +13,83 @@ import {
BIWEEKLY,
BIMONTHLY,
} from '../components/PeriodDimension/utils/index.js'
+import { periodTypesV44 } from './data/periodTypes.v44.js'
-const Wrapper = (story) => (
-
-
- {story()}
-
-
-)
+// The period selector has one implementation per server version (42, 43, 44),
+// selected by serverVersion.minor. The server responses are mocked.
+const withServer = ({ minor, data }) =>
+ function MockServer(Story) {
+ return (
+
+
+
+
+
+ )
+ }
+
+// Mimics the v44 server: relative period labels are stored per relative
+// period and returned under every period type that has the relative period
+const applyV44Labels = (
+ periodTypes,
+ { periodTypeLabels, relativePeriodLabels }
+) =>
+ periodTypes.map((pt) => ({
+ ...pt,
+ displayName: periodTypeLabels[pt.name] || pt.displayName,
+ relativePeriods: Object.fromEntries(
+ Object.entries(pt.relativePeriods).map(([id, rp]) => [
+ id,
+ { displayName: relativePeriodLabels[id] || rp.displayName },
+ ])
+ ),
+ }))
+
+const v44Data = ({
+ // the server returns all period types when none are configured
+ enabledPeriodTypes = periodTypesV44.map(({ name }) => name),
+ periodTypeLabels = {},
+ relativePeriodLabels = {},
+ weeklyStart = 'WEEKLY',
+ financialYearStart = 'FINANCIAL_YEAR_OCTOBER',
+ analysisRelativePeriod = 'LAST_12_MONTHS',
+} = {}) => ({
+ // PeriodDimension reads keyUiLocale for generating fixed periods
+ userSettings: { keyUiLocale: 'en' },
+ 'configuration/dataOutputPeriodTypes': applyV44Labels(
+ periodTypesV44.filter(({ name }) => enabledPeriodTypes.includes(name)),
+ { periodTypeLabels, relativePeriodLabels }
+ ),
+ 'systemSettings/keyAnalysisRelativePeriod': {
+ keyAnalysisRelativePeriod: analysisRelativePeriod,
+ },
+ 'systemSettings/analyticsWeeklyStart': {
+ analyticsWeeklyStart: weeklyStart,
+ },
+ 'systemSettings/analyticsFinancialYearStart': {
+ analyticsFinancialYearStart: financialYearStart,
+ },
+})
const selectedPeriods = [{ id: 'LAST_12_MONTHS', name: 'Last 12 months' }]
export default {
title: 'PeriodDimension',
- decorators: [Wrapper],
+ decorators: [withServer({ minor: 44, data: v44Data() })],
}
export const NoneSelected = () => {
return console.log(selected)} />
}
-NoneSelected.story = {
- name: 'None selected',
-}
+NoneSelected.storyName = 'None selected'
export const OneSelected = () => {
return (
@@ -49,9 +100,7 @@ export const OneSelected = () => {
)
}
-OneSelected.story = {
- name: 'One selected',
-}
+OneSelected.storyName = 'One selected'
export const MonthlyExcluded = () => {
return (
@@ -62,9 +111,7 @@ export const MonthlyExcluded = () => {
)
}
-MonthlyExcluded.story = {
- name: 'Monthly excluded',
-}
+MonthlyExcluded.storyName = 'Monthly excluded'
export const WeeklyExcluded = () => {
return (
@@ -81,9 +128,7 @@ export const WeeklyExcluded = () => {
)
}
-WeeklyExcluded.story = {
- name: 'Weekly excluded',
-}
+WeeklyExcluded.storyName = 'Weekly excluded'
export const AllBelowQuarterlyExcluded = () => {
return (
@@ -104,9 +149,7 @@ export const AllBelowQuarterlyExcluded = () => {
)
}
-AllBelowQuarterlyExcluded.story = {
- name: 'All below Quarterly excluded',
-}
+AllBelowQuarterlyExcluded.storyName = 'All below Quarterly excluded'
export const UsingRightFooter = () => {
return (
@@ -127,6 +170,151 @@ export const UsingRightFooter = () => {
)
}
-UsingRightFooter.story = {
- name: 'Using right footer',
+UsingRightFooter.storyName = 'Using right footer'
+
+export const V44CustomLabels = () => {
+ return (
+ console.log(selected)}
+ />
+ )
+}
+
+V44CustomLabels.storyName =
+ 'v44: custom labels, only Weekly Sunday and Financial April enabled'
+V44CustomLabels.decorators = [
+ withServer({
+ minor: 44,
+ data: v44Data({
+ enabledPeriodTypes: [
+ 'Daily',
+ 'WeeklySunday',
+ 'Monthly',
+ 'Quarterly',
+ 'Yearly',
+ 'FinancialApril',
+ ],
+ weeklyStart: 'WEEKLY_SUNDAY',
+ financialYearStart: 'FINANCIAL_YEAR_APRIL',
+ // opens the weekly relative periods
+ analysisRelativePeriod: 'LAST_4_WEEKS',
+ periodTypeLabels: {
+ WeeklySunday: 'EPI week',
+ FinancialApril: 'Fiscal year',
+ },
+ relativePeriodLabels: {
+ THIS_WEEK: 'This EPI week',
+ LAST_WEEK: 'Last EPI week',
+ LAST_4_WEEKS: 'Last 4 EPI weeks',
+ LAST_12_WEEKS: 'Last 12 EPI weeks',
+ LAST_52_WEEKS: 'Last 52 EPI weeks',
+ WEEKS_THIS_YEAR: 'EPI weeks this year',
+ THIS_FINANCIAL_YEAR: 'This fiscal year',
+ LAST_FINANCIAL_YEAR: 'Last fiscal year',
+ LAST_5_FINANCIAL_YEARS: 'Last 5 fiscal years',
+ LAST_10_FINANCIAL_YEARS: 'Last 10 fiscal years',
+ },
+ }),
+ }),
+]
+
+export const V44Translated = () => {
+ return (
+ console.log(selected)}
+ />
+ )
+}
+
+V44Translated.storyName = 'v44: label translations (db locale fr)'
+V44Translated.decorators = [
+ withServer({
+ minor: 44,
+ data: v44Data({
+ enabledPeriodTypes: ['Daily', 'Weekly', 'Monthly', 'Yearly'],
+ periodTypeLabels: {
+ Daily: 'Quotidien',
+ Weekly: 'Hebdomadaire',
+ Monthly: 'Mensuel',
+ Yearly: 'Annuel',
+ },
+ relativePeriodLabels: {
+ TODAY: "Aujourd'hui",
+ YESTERDAY: 'Hier',
+ THIS_WEEK: 'Cette semaine',
+ LAST_WEEK: 'La semaine dernière',
+ THIS_MONTH: 'Ce mois-ci',
+ LAST_MONTH: 'Le mois dernier',
+ LAST_3_MONTHS: '3 derniers mois',
+ LAST_6_MONTHS: '6 derniers mois',
+ LAST_12_MONTHS: '12 derniers mois',
+ THIS_YEAR: 'Cette année',
+ LAST_YEAR: "L'année dernière",
+ },
+ }),
+ }),
+]
+
+export const V44WeeklyStartDisabled = () => {
+ return console.log(selected)} />
}
+
+V44WeeklyStartDisabled.storyName =
+ 'v44: weekly start Sunday, but only Weekly enabled (no weekly relative periods)'
+V44WeeklyStartDisabled.decorators = [
+ withServer({
+ minor: 44,
+ data: v44Data({
+ enabledPeriodTypes: ['Weekly', 'Monthly'],
+ weeklyStart: 'WEEKLY_SUNDAY',
+ }),
+ }),
+]
+
+export const V43Labels = () => {
+ return console.log(selected)} />
+}
+
+V43Labels.storyName =
+ 'v43: period type labels, relative period names derived from them'
+V43Labels.decorators = [
+ withServer({
+ minor: 43,
+ data: {
+ userSettings: { keyUiLocale: 'en' },
+ 'configuration/dataOutputPeriodTypes': [
+ { name: 'Daily' },
+ { name: 'WeeklySunday', displayLabel: 'EPI week' },
+ { name: 'Monthly' },
+ { name: 'Quarterly' },
+ { name: 'Yearly' },
+ { name: 'FinancialApril', displayLabel: 'Fiscal year' },
+ ],
+ 'systemSettings/keyAnalysisRelativePeriod': {
+ keyAnalysisRelativePeriod: 'LAST_12_MONTHS',
+ },
+ 'systemSettings/analyticsWeeklyStart': {
+ analyticsWeeklyStart: 'WEEKLY_SUNDAY',
+ },
+ 'systemSettings/analyticsFinancialYearStart': {
+ analyticsFinancialYearStart: 'FINANCIAL_YEAR_APRIL',
+ },
+ },
+ }),
+]
+
+export const V42 = () => {
+ return console.log(selected)} />
+}
+
+V42.storyName = 'v42: client period types and relative periods'
+V42.decorators = [
+ withServer({
+ minor: 42,
+ data: { userSettings: { keyUiLocale: 'en' } },
+ }),
+]
diff --git a/src/__demo__/data/periodTypes.v44.js b/src/__demo__/data/periodTypes.v44.js
new file mode 100644
index 000000000..fc0468060
--- /dev/null
+++ b/src/__demo__/data/periodTypes.v44.js
@@ -0,0 +1,472 @@
+// All period types from /api/periodTypes (v44) with default names, in server order.
+// Fields as requested by the v44 period selector: name,displayName,relativePeriods
+export const periodTypesV44 = [
+ {
+ name: 'Daily',
+ displayName: 'Daily',
+ relativePeriods: {
+ TODAY: {
+ displayName: 'Today',
+ },
+ YESTERDAY: {
+ displayName: 'Yesterday',
+ },
+ LAST_3_DAYS: {
+ displayName: 'Last 3 days',
+ },
+ LAST_7_DAYS: {
+ displayName: 'Last 7 days',
+ },
+ LAST_14_DAYS: {
+ displayName: 'Last 14 days',
+ },
+ LAST_30_DAYS: {
+ displayName: 'Last 30 days',
+ },
+ LAST_60_DAYS: {
+ displayName: 'Last 60 days',
+ },
+ LAST_90_DAYS: {
+ displayName: 'Last 90 days',
+ },
+ LAST_180_DAYS: {
+ displayName: 'Last 180 days',
+ },
+ },
+ },
+ {
+ name: 'Weekly',
+ displayName: 'Weekly',
+ relativePeriods: {
+ WEEKS_THIS_YEAR: {
+ displayName: 'Weeks this year',
+ },
+ THIS_WEEK: {
+ displayName: 'This week',
+ },
+ LAST_WEEK: {
+ displayName: 'Last week',
+ },
+ LAST_4_WEEKS: {
+ displayName: 'Last 4 weeks',
+ },
+ LAST_12_WEEKS: {
+ displayName: 'Last 12 weeks',
+ },
+ LAST_52_WEEKS: {
+ displayName: 'Last 52 weeks',
+ },
+ },
+ },
+ {
+ name: 'WeeklyWednesday',
+ displayName: 'Weekly Wednesday',
+ relativePeriods: {
+ WEEKS_THIS_YEAR: {
+ displayName: 'Weeks this year',
+ },
+ THIS_WEEK: {
+ displayName: 'This week',
+ },
+ LAST_WEEK: {
+ displayName: 'Last week',
+ },
+ LAST_4_WEEKS: {
+ displayName: 'Last 4 weeks',
+ },
+ LAST_12_WEEKS: {
+ displayName: 'Last 12 weeks',
+ },
+ LAST_52_WEEKS: {
+ displayName: 'Last 52 weeks',
+ },
+ },
+ },
+ {
+ name: 'WeeklyThursday',
+ displayName: 'Weekly Thursday',
+ relativePeriods: {
+ WEEKS_THIS_YEAR: {
+ displayName: 'Weeks this year',
+ },
+ THIS_WEEK: {
+ displayName: 'This week',
+ },
+ LAST_WEEK: {
+ displayName: 'Last week',
+ },
+ LAST_4_WEEKS: {
+ displayName: 'Last 4 weeks',
+ },
+ LAST_12_WEEKS: {
+ displayName: 'Last 12 weeks',
+ },
+ LAST_52_WEEKS: {
+ displayName: 'Last 52 weeks',
+ },
+ },
+ },
+ {
+ name: 'WeeklyFriday',
+ displayName: 'WeeklyFriday',
+ relativePeriods: {
+ WEEKS_THIS_YEAR: {
+ displayName: 'Weeks this year',
+ },
+ THIS_WEEK: {
+ displayName: 'This week',
+ },
+ LAST_WEEK: {
+ displayName: 'Last week',
+ },
+ LAST_4_WEEKS: {
+ displayName: 'Last 4 weeks',
+ },
+ LAST_12_WEEKS: {
+ displayName: 'Last 12 weeks',
+ },
+ LAST_52_WEEKS: {
+ displayName: 'Last 52 weeks',
+ },
+ },
+ },
+ {
+ name: 'WeeklySaturday',
+ displayName: 'Weekly Saturday',
+ relativePeriods: {
+ WEEKS_THIS_YEAR: {
+ displayName: 'Weeks this year',
+ },
+ THIS_WEEK: {
+ displayName: 'This week',
+ },
+ LAST_WEEK: {
+ displayName: 'Last week',
+ },
+ LAST_4_WEEKS: {
+ displayName: 'Last 4 weeks',
+ },
+ LAST_12_WEEKS: {
+ displayName: 'Last 12 weeks',
+ },
+ LAST_52_WEEKS: {
+ displayName: 'Last 52 weeks',
+ },
+ },
+ },
+ {
+ name: 'WeeklySunday',
+ displayName: 'Weekly Sunday',
+ relativePeriods: {
+ WEEKS_THIS_YEAR: {
+ displayName: 'Weeks this year',
+ },
+ THIS_WEEK: {
+ displayName: 'This week',
+ },
+ LAST_WEEK: {
+ displayName: 'Last week',
+ },
+ LAST_4_WEEKS: {
+ displayName: 'Last 4 weeks',
+ },
+ LAST_12_WEEKS: {
+ displayName: 'Last 12 weeks',
+ },
+ LAST_52_WEEKS: {
+ displayName: 'Last 52 weeks',
+ },
+ },
+ },
+ {
+ name: 'BiWeekly',
+ displayName: 'Bi-weekly',
+ relativePeriods: {
+ THIS_BIWEEK: {
+ displayName: 'This bi-week',
+ },
+ LAST_BIWEEK: {
+ displayName: 'Last bi-week',
+ },
+ LAST_4_BIWEEKS: {
+ displayName: 'Last 4 bi-weeks',
+ },
+ },
+ },
+ {
+ name: 'Monthly',
+ displayName: 'Monthly',
+ relativePeriods: {
+ THIS_MONTH: {
+ displayName: 'This month',
+ },
+ LAST_MONTH: {
+ displayName: 'Last month',
+ },
+ MONTHS_THIS_YEAR: {
+ displayName: 'Months this year',
+ },
+ MONTHS_LAST_YEAR: {
+ displayName: 'Months last year',
+ },
+ LAST_12_MONTHS: {
+ displayName: 'Last 12 months',
+ },
+ LAST_6_MONTHS: {
+ displayName: 'Last 6 months',
+ },
+ LAST_3_MONTHS: {
+ displayName: 'Last 3 months',
+ },
+ },
+ },
+ {
+ name: 'BiMonthly',
+ displayName: 'Bi-monthly',
+ relativePeriods: {
+ THIS_BIMONTH: {
+ displayName: 'This bi-month',
+ },
+ LAST_BIMONTH: {
+ displayName: 'Last bi-month',
+ },
+ BIMONTHS_THIS_YEAR: {
+ displayName: 'Bi-months this year',
+ },
+ LAST_6_BIMONTHS: {
+ displayName: 'Last 6 bi-months',
+ },
+ },
+ },
+ {
+ name: 'Quarterly',
+ displayName: 'Quarterly',
+ relativePeriods: {
+ THIS_QUARTER: {
+ displayName: 'This quarter',
+ },
+ LAST_QUARTER: {
+ displayName: 'Last quarter',
+ },
+ QUARTERS_THIS_YEAR: {
+ displayName: 'Quarters this year',
+ },
+ QUARTERS_LAST_YEAR: {
+ displayName: 'Quarters last year',
+ },
+ LAST_4_QUARTERS: {
+ displayName: 'Last 4 quarters',
+ },
+ },
+ },
+ {
+ name: 'QuarterlyNov',
+ displayName: 'Quarterly November',
+ relativePeriods: {
+ THIS_QUARTER: {
+ displayName: 'This quarter',
+ },
+ LAST_QUARTER: {
+ displayName: 'Last quarter',
+ },
+ QUARTERS_THIS_YEAR: {
+ displayName: 'Quarters this year',
+ },
+ QUARTERS_LAST_YEAR: {
+ displayName: 'Quarters last year',
+ },
+ LAST_4_QUARTERS: {
+ displayName: 'Last 4 quarters',
+ },
+ },
+ },
+ {
+ name: 'SixMonthly',
+ displayName: 'Six-monthly',
+ relativePeriods: {
+ THIS_SIX_MONTH: {
+ displayName: 'This six-month',
+ },
+ LAST_SIX_MONTH: {
+ displayName: 'Last six-month',
+ },
+ LAST_2_SIXMONTHS: {
+ displayName: 'Last 2 six-months',
+ },
+ },
+ },
+ {
+ name: 'SixMonthlyApril',
+ displayName: 'Six-monthly April',
+ relativePeriods: {
+ THIS_SIX_MONTH: {
+ displayName: 'This six-month',
+ },
+ LAST_SIX_MONTH: {
+ displayName: 'Last six-month',
+ },
+ LAST_2_SIXMONTHS: {
+ displayName: 'Last 2 six-months',
+ },
+ },
+ },
+ {
+ name: 'SixMonthlyNov',
+ displayName: 'Six-monthly November',
+ relativePeriods: {
+ THIS_SIX_MONTH: {
+ displayName: 'This six-month',
+ },
+ LAST_SIX_MONTH: {
+ displayName: 'Last six-month',
+ },
+ LAST_2_SIXMONTHS: {
+ displayName: 'Last 2 six-months',
+ },
+ },
+ },
+ {
+ name: 'Yearly',
+ displayName: 'Yearly',
+ relativePeriods: {
+ THIS_YEAR: {
+ displayName: 'This year',
+ },
+ LAST_YEAR: {
+ displayName: 'Last year',
+ },
+ LAST_5_YEARS: {
+ displayName: 'Last 5 years',
+ },
+ LAST_10_YEARS: {
+ displayName: 'Last 10 years',
+ },
+ },
+ },
+ {
+ name: 'FinancialFeb',
+ displayName: 'Financial February',
+ relativePeriods: {
+ THIS_FINANCIAL_YEAR: {
+ displayName: 'This financial year',
+ },
+ LAST_FINANCIAL_YEAR: {
+ displayName: 'Last financial year',
+ },
+ LAST_5_FINANCIAL_YEARS: {
+ displayName: 'Last 5 financial years',
+ },
+ LAST_10_FINANCIAL_YEARS: {
+ displayName: 'LAST_10_FINANCIAL_YEARS',
+ },
+ },
+ },
+ {
+ name: 'FinancialApril',
+ displayName: 'Financial April',
+ relativePeriods: {
+ THIS_FINANCIAL_YEAR: {
+ displayName: 'This financial year',
+ },
+ LAST_FINANCIAL_YEAR: {
+ displayName: 'Last financial year',
+ },
+ LAST_5_FINANCIAL_YEARS: {
+ displayName: 'Last 5 financial years',
+ },
+ LAST_10_FINANCIAL_YEARS: {
+ displayName: 'LAST_10_FINANCIAL_YEARS',
+ },
+ },
+ },
+ {
+ name: 'FinancialJuly',
+ displayName: 'Financial July',
+ relativePeriods: {
+ THIS_FINANCIAL_YEAR: {
+ displayName: 'This financial year',
+ },
+ LAST_FINANCIAL_YEAR: {
+ displayName: 'Last financial year',
+ },
+ LAST_5_FINANCIAL_YEARS: {
+ displayName: 'Last 5 financial years',
+ },
+ LAST_10_FINANCIAL_YEARS: {
+ displayName: 'LAST_10_FINANCIAL_YEARS',
+ },
+ },
+ },
+ {
+ name: 'FinancialAug',
+ displayName: 'Financial August',
+ relativePeriods: {
+ THIS_FINANCIAL_YEAR: {
+ displayName: 'This financial year',
+ },
+ LAST_FINANCIAL_YEAR: {
+ displayName: 'Last financial year',
+ },
+ LAST_5_FINANCIAL_YEARS: {
+ displayName: 'Last 5 financial years',
+ },
+ LAST_10_FINANCIAL_YEARS: {
+ displayName: 'LAST_10_FINANCIAL_YEARS',
+ },
+ },
+ },
+ {
+ name: 'FinancialSep',
+ displayName: 'FinancialSep',
+ relativePeriods: {
+ THIS_FINANCIAL_YEAR: {
+ displayName: 'This financial year',
+ },
+ LAST_FINANCIAL_YEAR: {
+ displayName: 'Last financial year',
+ },
+ LAST_5_FINANCIAL_YEARS: {
+ displayName: 'Last 5 financial years',
+ },
+ LAST_10_FINANCIAL_YEARS: {
+ displayName: 'LAST_10_FINANCIAL_YEARS',
+ },
+ },
+ },
+ {
+ name: 'FinancialOct',
+ displayName: 'Financial October',
+ relativePeriods: {
+ THIS_FINANCIAL_YEAR: {
+ displayName: 'This financial year',
+ },
+ LAST_FINANCIAL_YEAR: {
+ displayName: 'Last financial year',
+ },
+ LAST_5_FINANCIAL_YEARS: {
+ displayName: 'Last 5 financial years',
+ },
+ LAST_10_FINANCIAL_YEARS: {
+ displayName: 'LAST_10_FINANCIAL_YEARS',
+ },
+ },
+ },
+ {
+ name: 'FinancialNov',
+ displayName: 'Financial November',
+ relativePeriods: {
+ THIS_FINANCIAL_YEAR: {
+ displayName: 'This financial year',
+ },
+ LAST_FINANCIAL_YEAR: {
+ displayName: 'Last financial year',
+ },
+ LAST_5_FINANCIAL_YEARS: {
+ displayName: 'Last 5 financial years',
+ },
+ LAST_10_FINANCIAL_YEARS: {
+ displayName: 'LAST_10_FINANCIAL_YEARS',
+ },
+ },
+ },
+]
diff --git a/src/components/PeriodDimension/PeriodDimension.js b/src/components/PeriodDimension/PeriodDimension.js
index 9e7b47607..d7c4b6949 100644
--- a/src/components/PeriodDimension/PeriodDimension.js
+++ b/src/components/PeriodDimension/PeriodDimension.js
@@ -1,6 +1,8 @@
import { useConfig, useDataQuery } from '@dhis2/app-runtime'
+import { CircularLoader } from '@dhis2/ui'
import PropTypes from 'prop-types'
import React from 'react'
+import { TRANSFER_HEIGHT } from '../../modules/dimensionSelectorHelper.js'
import { DIMENSION_ID_PERIOD } from '../../modules/predefinedDimensions.js'
import PeriodTransfer from './PeriodTransfer.js'
import { useDataOutputPeriodTypes } from './useDataOutputPeriodTypes.js'
@@ -29,8 +31,7 @@ const PeriodDimension = ({
const { systemInfo } = config
const userSettingsResult = useDataQuery(userSettingsQuery)
- const { supportsEnabledPeriodTypes, enabledPeriodTypesData } =
- useDataOutputPeriodTypes()
+ const { enabledPeriodTypesData, loading } = useDataOutputPeriodTypes()
const { calendar = 'gregory' } = systemInfo
const { data: { userSettings: { keyUiLocale: locale } = {} } = {} } =
@@ -50,6 +51,24 @@ const PeriodDimension = ({
enabledPeriodTypesData?.metaData
)
+ // wait for the enabled period types, otherwise the transfer briefly shows
+ // the full client lists and picks its default period type from them
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
return (
)
}
diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js
index 7d776d982..5440d83c0 100644
--- a/src/components/PeriodDimension/PeriodTransfer.js
+++ b/src/components/PeriodDimension/PeriodTransfer.js
@@ -12,24 +12,9 @@ import {
import styles from '../styles/DimensionSelector.style.js'
import { TransferOption } from '../TransferOption.js'
import FixedPeriodFilter from './FixedPeriodFilter.js'
+import { getPeriodTypeOptions } from './periodTypeOptions.js'
import RelativePeriodFilter from './RelativePeriodFilter.js'
-import {
- applyDisplayLabelOverrides,
- applyFixedPeriodTypeDisplayLabels,
- filterEnabledFixedPeriodTypes,
- filterEnabledRelativePeriodTypes,
-} from './utils/enabledPeriodTypes.js'
-import { getFixedPeriodsOptions } from './utils/fixedPeriods.js'
-import {
- FYFEB,
- FYAUG,
- FYSEP,
- MONTHLY,
- QUARTERLY,
- WEEKLYFRI,
- filterPeriodTypesById,
-} from './utils/index.js'
-import { getRelativePeriodsOptions } from './utils/relativePeriods.js'
+import { MONTHLY, QUARTERLY } from './utils/index.js'
const RightHeader = ({ infoBoxMessage }) => (
<>
@@ -63,65 +48,19 @@ const PeriodTransfer = ({
infoBoxMessage,
height = TRANSFER_HEIGHT,
enabledPeriodTypesData = null,
- supportsEnabledPeriodTypes = false,
}) => {
- const { filteredFixedOptions, filteredRelativeOptions } = useMemo(() => {
- if (supportsEnabledPeriodTypes && enabledPeriodTypesData) {
- const {
- enabledTypes,
- financialYearStart,
- financialYearDisplayLabel,
- weeklyDisplayLabel,
- metaData,
- } = enabledPeriodTypesData
-
- const filteredFixed = applyFixedPeriodTypeDisplayLabels(
- filterEnabledFixedPeriodTypes(
- getFixedPeriodsOptions(periodsSettings),
- enabledTypes
- ),
- enabledTypes
- )
-
- const filteredRelative = applyDisplayLabelOverrides(
- filterEnabledRelativePeriodTypes(
- getRelativePeriodsOptions(),
- enabledTypes,
- financialYearStart
- ),
- { financialYearDisplayLabel, weeklyDisplayLabel, metaData }
- )
-
- return {
- filteredFixedOptions: filteredFixed,
- filteredRelativeOptions: filteredRelative,
- }
- } else {
- const allFixed = getFixedPeriodsOptions(periodsSettings)
- const allRelative = getRelativePeriodsOptions()
- const v43PeriodTypes = [WEEKLYFRI, FYFEB, FYAUG, FYSEP]
- const allExcludedPeriodTypes = [
- ...excludedPeriodTypes,
- ...v43PeriodTypes,
- ]
-
- return {
- filteredFixedOptions: filterPeriodTypesById(
- allFixed,
- allExcludedPeriodTypes
- ),
- filteredRelativeOptions: filterPeriodTypesById(
- allRelative,
- allExcludedPeriodTypes
- ),
- }
- }
- }, [
- supportsEnabledPeriodTypes,
- enabledPeriodTypesData,
- excludedPeriodTypes,
- periodsSettings,
- ])
+ const {
+ fixedOptions: filteredFixedOptions,
+ relativeOptions: filteredRelativeOptions,
+ } = useMemo(
+ () =>
+ getPeriodTypeOptions({
+ enabledPeriodTypesData,
+ periodsSettings,
+ excludedPeriodTypes,
+ }),
+ [enabledPeriodTypesData, excludedPeriodTypes, periodsSettings]
+ )
const analysisRelativePeriod =
enabledPeriodTypesData?.analysisRelativePeriod
@@ -389,9 +328,18 @@ PeriodTransfer.propTypes = {
enabledPeriodTypesData: PropTypes.shape({
analysisRelativePeriod: PropTypes.string,
enabledTypes: PropTypes.array,
+ // v43 only
financialYearDisplayLabel: PropTypes.string,
financialYearStart: PropTypes.string,
+ metaData: PropTypes.objectOf(
+ PropTypes.shape({ name: PropTypes.string })
+ ),
noEnabledTypes: PropTypes.bool,
+ // v44 only
+ relativePeriodStartTypes: PropTypes.objectOf(PropTypes.string),
+ version: PropTypes.oneOf([43, 44]),
+ // v43 only
+ weeklyDisplayLabel: PropTypes.string,
}),
excludedPeriodTypes: PropTypes.arrayOf(PropTypes.string),
height: PropTypes.string,
@@ -408,7 +356,6 @@ PeriodTransfer.propTypes = {
name: PropTypes.string,
})
),
- supportsEnabledPeriodTypes: PropTypes.bool,
}
export default PeriodTransfer
diff --git a/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js b/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js
index ad8b0506d..522bad6a7 100644
--- a/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js
+++ b/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js
@@ -3,10 +3,12 @@ import userEvent from '@testing-library/user-event'
import React from 'react'
import PeriodDimension from '../PeriodDimension.js'
+let mockServerMinorVersion = 42
+
jest.mock('@dhis2/app-runtime', () => ({
useConfig: () => ({
systemInfo: {},
- serverVersion: { minor: 42 },
+ serverVersion: { minor: mockServerMinorVersion },
}),
useDataQuery: jest.fn().mockImplementation((_query, options) => {
if (options?.lazy) {
@@ -27,7 +29,10 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
}))
-afterEach(jest.clearAllMocks)
+afterEach(() => {
+ jest.clearAllMocks()
+ mockServerMinorVersion = 42
+})
const props = {
selectedPeriods: [],
@@ -62,3 +67,14 @@ test('PeriodDimension can toggle between relative and fixed period tab', async (
screen.getByTestId('period-dimension-fixed-period-filter')
).toBeInTheDocument()
})
+
+test('PeriodDimension shows a loader while the enabled period types are loading', () => {
+ mockServerMinorVersion = 44
+
+ const { container } = render()
+
+ expect(
+ container.querySelector('[data-test="period-dimension-loader"]')
+ ).toBeInTheDocument()
+ expect(screen.queryByText('Relative periods')).not.toBeInTheDocument()
+})
diff --git a/src/components/PeriodDimension/__tests__/v43.spec.js b/src/components/PeriodDimension/__tests__/v43.spec.js
new file mode 100644
index 000000000..fe5e6568f
--- /dev/null
+++ b/src/components/PeriodDimension/__tests__/v43.spec.js
@@ -0,0 +1,141 @@
+import { getRelativePeriodsOptions } from '../utils/relativePeriods.js'
+import {
+ applyDisplayLabelOverrides,
+ applyFixedPeriodTypeDisplayLabels,
+ filterEnabledRelativePeriodTypes,
+ getV43PeriodTypeOptions,
+} from '../versions/v43.js'
+
+// Shape of /api/configuration/dataOutputPeriodTypes (v43)
+const enabledTypes = [
+ { name: 'Daily', displayLabel: 'Day' },
+ { name: 'Weekly', displayLabel: 'Week' },
+ { name: 'Monthly', displayLabel: 'Month' },
+]
+
+const getOptions = (excludedPeriodTypes) =>
+ getV43PeriodTypeOptions({
+ enabledPeriodTypesData: {
+ version: 43,
+ enabledTypes,
+ financialYearStart: null,
+ metaData: {},
+ },
+ periodsSettings: { calendar: 'gregory', locale: 'en' },
+ excludedPeriodTypes,
+ })
+
+describe('v43 period type options', () => {
+ it('shows all enabled period types when nothing is excluded', () => {
+ const { fixedOptions, relativeOptions } = getOptions([])
+
+ expect(fixedOptions.map(({ id }) => id)).toEqual([
+ 'DAILY',
+ 'WEEKLY',
+ 'MONTHLY',
+ ])
+ expect(relativeOptions.map(({ id }) => id)).toEqual([
+ 'DAILY',
+ 'WEEKLY',
+ 'MONTHLY',
+ ])
+ })
+
+ it('applies excludedPeriodTypes to both fixed and relative options', () => {
+ const { fixedOptions, relativeOptions } = getOptions([
+ 'DAILY',
+ 'WEEKLY',
+ ])
+
+ expect(fixedOptions.map(({ id }) => id)).toEqual(['MONTHLY'])
+ expect(relativeOptions.map(({ id }) => id)).toEqual(['MONTHLY'])
+ })
+})
+
+describe('applyFixedPeriodTypeDisplayLabels', () => {
+ const fixedOptions = [
+ { id: 'WEEKLY', name: 'Weekly' },
+ { id: 'MONTHLY', name: 'Monthly' },
+ ]
+
+ it('uses the displayLabel of the server period type as name', () => {
+ expect(
+ applyFixedPeriodTypeDisplayLabels(fixedOptions, [
+ { name: 'Weekly', displayLabel: 'Week' },
+ { name: 'Monthly' },
+ ])
+ ).toEqual([
+ { id: 'WEEKLY', name: 'Week' },
+ { id: 'MONTHLY', name: 'Monthly' },
+ ])
+ })
+
+ it('returns the options unchanged when there are no labels', () => {
+ expect(applyFixedPeriodTypeDisplayLabels(fixedOptions, null)).toBe(
+ fixedOptions
+ )
+ expect(
+ applyFixedPeriodTypeDisplayLabels(fixedOptions, [
+ { name: 'Weekly' },
+ ])
+ ).toBe(fixedOptions)
+ })
+})
+
+describe('filterEnabledRelativePeriodTypes', () => {
+ const filter = (enabledServerPeriodTypes, financialYearStart) =>
+ filterEnabledRelativePeriodTypes(
+ getRelativePeriodsOptions(),
+ enabledServerPeriodTypes,
+ financialYearStart
+ ).map(({ id }) => id)
+
+ it('returns no categories when no period types are enabled', () => {
+ expect(filter([])).toEqual([])
+ expect(filter(null)).toEqual([])
+ })
+
+ it('shows a category when any period type of its family is enabled', () => {
+ expect(
+ filter([{ name: 'WeeklySunday' }, { name: 'SixMonthlyApril' }])
+ ).toEqual(['WEEKLY', 'SIXMONTHLY'])
+ })
+
+ it('shows financial years only when the financial year start is set', () => {
+ const enabledTypes = [{ name: 'Monthly' }, { name: 'FinancialOct' }]
+
+ expect(filter(enabledTypes)).toEqual(['MONTHLY'])
+ expect(filter(enabledTypes, 'FINANCIAL_YEAR_OCTOBER')).toEqual([
+ 'MONTHLY',
+ 'FINANCIAL',
+ ])
+ })
+})
+
+describe('applyDisplayLabelOverrides', () => {
+ const relativeOptions = getRelativePeriodsOptions().filter(({ id }) =>
+ ['WEEKLY', 'MONTHLY'].includes(id)
+ )
+
+ it('renames the weekly category and its relative periods', () => {
+ const [weeklyOption, monthlyOption] = applyDisplayLabelOverrides(
+ relativeOptions,
+ {
+ weeklyDisplayLabel: 'EPI week',
+ metaData: { THIS_WEEK: { name: 'This EPI week' } },
+ }
+ )
+
+ expect(weeklyOption.name).toBe('EPI week')
+ expect(
+ weeklyOption.getPeriods().find(({ id }) => id === 'THIS_WEEK').name
+ ).toBe('This EPI week')
+ expect(monthlyOption).toBe(relativeOptions[1])
+ })
+
+ it('returns the options unchanged when there are no labels', () => {
+ expect(applyDisplayLabelOverrides(relativeOptions, {})).toBe(
+ relativeOptions
+ )
+ })
+})
diff --git a/src/components/PeriodDimension/__tests__/v44.spec.js b/src/components/PeriodDimension/__tests__/v44.spec.js
new file mode 100644
index 000000000..b233ddbe0
--- /dev/null
+++ b/src/components/PeriodDimension/__tests__/v44.spec.js
@@ -0,0 +1,420 @@
+import { renderHook } from '@testing-library/react'
+import {
+ getV44PeriodTypeOptions,
+ useV44DataOutputPeriodTypes,
+} from '../versions/v44.js'
+
+jest.mock('@dhis2/app-runtime', () => ({
+ useDataQuery: jest.fn(),
+}))
+
+// Shape of /api/configuration/dataOutputPeriodTypes?fields=name,displayName,relativePeriods (v44)
+const daily = {
+ name: 'Daily',
+ displayName: 'Quotidien',
+ relativePeriods: {
+ TODAY: { displayName: "Aujourd'hui" },
+ YESTERDAY: { displayName: 'Hier' },
+ },
+}
+// relative periods are nested under every period type of the family,
+// with the same labels
+const weeklyRelativePeriods = {
+ THIS_WEEK: { displayName: 'Cette semaine' },
+ LAST_WEEK: { displayName: 'Semaine dernière' },
+}
+const weekly = {
+ name: 'Weekly',
+ displayName: 'Hebdomadaire',
+ relativePeriods: weeklyRelativePeriods,
+}
+const weeklySunday = {
+ name: 'WeeklySunday',
+ displayName: 'EPI Week',
+ relativePeriods: weeklyRelativePeriods,
+}
+const monthly = {
+ name: 'Monthly',
+ displayName: 'Mois',
+ relativePeriods: {
+ THIS_MONTH: { displayName: 'Ce mois-ci' },
+ LAST_MONTH: { displayName: 'x-Last_Month' },
+ MONTHS_LAST_YEAR: { displayName: "Mois de l'an dernier" },
+ },
+}
+const quarterlyRelativePeriods = {
+ THIS_QUARTER: { displayName: 'Ce trimestre' },
+}
+const quarterly = {
+ name: 'Quarterly',
+ displayName: 'Trimestriel',
+ relativePeriods: quarterlyRelativePeriods,
+}
+const quarterlyNov = {
+ name: 'QuarterlyNov',
+ displayName: 'Quarterly Nov',
+ relativePeriods: quarterlyRelativePeriods,
+}
+const sixMonthlyApril = {
+ name: 'SixMonthlyApril',
+ displayName: 'Six-monthly April',
+ relativePeriods: {
+ THIS_SIX_MONTH: { displayName: 'Ce semestre' },
+ },
+}
+const financialRelativePeriods = {
+ THIS_FINANCIAL_YEAR: { displayName: 'Cet exercice' },
+ LAST_FINANCIAL_YEAR: { displayName: 'Exercice précédent' },
+}
+const financialApril = {
+ name: 'FinancialApril',
+ displayName: 'FY April',
+ relativePeriods: financialRelativePeriods,
+}
+const financialOct = {
+ name: 'FinancialOct',
+ displayName: 'FY October',
+ relativePeriods: financialRelativePeriods,
+}
+
+const DEFAULT_START_TYPES = { WEEKLY: 'Weekly', FINANCIAL: 'FinancialOct' }
+
+const getOptions = (
+ enabledTypes,
+ excludedPeriodTypes = [],
+ relativePeriodStartTypes = DEFAULT_START_TYPES
+) =>
+ getV44PeriodTypeOptions({
+ enabledPeriodTypesData: {
+ version: 44,
+ enabledTypes,
+ relativePeriodStartTypes,
+ },
+ periodsSettings: { calendar: 'gregory', locale: 'en' },
+ excludedPeriodTypes,
+ })
+
+const summarize = (options) =>
+ options.map(({ id, name, getPeriods }) => ({
+ id,
+ name,
+ periods: getPeriods().map((p) => `${p.id}:${p.name}`),
+ }))
+
+describe('v44 period type options', () => {
+ it('uses server displayName for fixed period types', () => {
+ const { fixedOptions } = getOptions([
+ daily,
+ weekly,
+ weeklySunday,
+ monthly,
+ financialApril,
+ ])
+
+ expect(fixedOptions.map(({ id, name }) => ({ id, name }))).toEqual([
+ { id: 'DAILY', name: 'Quotidien' },
+ { id: 'WEEKLY', name: 'Hebdomadaire' },
+ { id: 'WEEKLYSUN', name: 'EPI Week' },
+ { id: 'MONTHLY', name: 'Mois' },
+ { id: 'FYAPR', name: 'FY April' },
+ ])
+ expect(typeof fixedOptions[0].getPeriods).toBe('function')
+ })
+
+ it('skips server period types the client cannot generate periods for', () => {
+ const { fixedOptions } = getOptions([monthly, quarterlyNov])
+
+ expect(fixedOptions.map(({ id }) => id)).toEqual(['MONTHLY'])
+ })
+
+ it('uses the nested relative periods with server displayName', () => {
+ const { relativeOptions } = getOptions([daily, weekly, monthly])
+
+ expect(summarize(relativeOptions)).toEqual([
+ {
+ id: 'DAILY',
+ name: 'Quotidien',
+ periods: ["TODAY:Aujourd'hui", 'YESTERDAY:Hier'],
+ },
+ {
+ id: 'WEEKLY',
+ name: 'Hebdomadaire',
+ periods: [
+ 'THIS_WEEK:Cette semaine',
+ 'LAST_WEEK:Semaine dernière',
+ ],
+ },
+ {
+ id: 'MONTHLY',
+ name: 'Mois',
+ periods: [
+ 'THIS_MONTH:Ce mois-ci',
+ 'LAST_MONTH:x-Last_Month',
+ "MONTHS_LAST_YEAR:Mois de l'an dernier",
+ ],
+ },
+ ])
+ })
+
+ it('shows one weekly category when several weekly types are enabled', () => {
+ const { relativeOptions } = getOptions([weekly, weeklySunday])
+
+ expect(relativeOptions.map(({ id, name }) => ({ id, name }))).toEqual([
+ { id: 'WEEKLY', name: 'Hebdomadaire' },
+ ])
+ })
+
+ it('names the weekly category after the weekly start variant from the system setting', () => {
+ const { relativeOptions } = getOptions([weekly, weeklySunday], [], {
+ ...DEFAULT_START_TYPES,
+ WEEKLY: 'WeeklySunday',
+ })
+
+ expect(summarize(relativeOptions)).toEqual([
+ {
+ id: 'WEEKLY',
+ name: 'EPI Week',
+ periods: [
+ 'THIS_WEEK:Cette semaine',
+ 'LAST_WEEK:Semaine dernière',
+ ],
+ },
+ ])
+ })
+
+ it('hides weekly relative periods when the weekly start variant is disabled', () => {
+ const { relativeOptions } = getOptions([weeklySunday, monthly])
+
+ expect(relativeOptions.map(({ id }) => id)).toEqual(['MONTHLY'])
+ })
+
+ it('shows weekly relative periods when only the weekly start variant is enabled', () => {
+ const { relativeOptions } = getOptions([weeklySunday], [], {
+ ...DEFAULT_START_TYPES,
+ WEEKLY: 'WeeklySunday',
+ })
+
+ expect(summarize(relativeOptions)).toEqual([
+ {
+ id: 'WEEKLY',
+ name: 'EPI Week',
+ periods: [
+ 'THIS_WEEK:Cette semaine',
+ 'LAST_WEEK:Semaine dernière',
+ ],
+ },
+ ])
+ })
+
+ it('shows financial year relative periods for the financial year start variant from the system setting', () => {
+ const { relativeOptions } = getOptions([financialApril, financialOct])
+
+ expect(summarize(relativeOptions)).toEqual([
+ {
+ id: 'FINANCIAL',
+ name: 'FY October',
+ periods: [
+ 'THIS_FINANCIAL_YEAR:Cet exercice',
+ 'LAST_FINANCIAL_YEAR:Exercice précédent',
+ ],
+ },
+ ])
+ })
+
+ it('shows quarterly relative periods for the standard quarterly type', () => {
+ expect(
+ summarize(getOptions([quarterly, quarterlyNov]).relativeOptions)
+ ).toEqual([
+ {
+ id: 'QUARTERLY',
+ name: 'Trimestriel',
+ periods: ['THIS_QUARTER:Ce trimestre'],
+ },
+ ])
+ })
+
+ it('hides quarterly and six-monthly relative periods when only a start variant is enabled', () => {
+ const { fixedOptions, relativeOptions } = getOptions([
+ quarterlyNov,
+ sixMonthlyApril,
+ ])
+
+ expect(fixedOptions.map(({ id }) => id)).toEqual(['SIXMONTHLYAPR'])
+ expect(relativeOptions).toEqual([])
+ })
+
+ it('hides a category when the server returns no relative periods for it', () => {
+ const { relativeOptions } = getOptions([
+ { ...monthly, relativePeriods: {} },
+ ])
+
+ expect(relativeOptions).toEqual([])
+ })
+
+ it('hides financial year relative periods when the start variant is disabled or not set', () => {
+ expect(getOptions([financialApril]).relativeOptions).toEqual([])
+ expect(
+ getOptions([financialOct], [], {
+ ...DEFAULT_START_TYPES,
+ FINANCIAL: null,
+ }).relativeOptions
+ ).toEqual([])
+ })
+
+ it('applies excludedPeriodTypes to both fixed and relative options', () => {
+ const { fixedOptions, relativeOptions } = getOptions(
+ [daily, weekly, monthly],
+ ['DAILY', 'WEEKLY']
+ )
+
+ expect(fixedOptions.map(({ id }) => id)).toEqual(['MONTHLY'])
+ expect(relativeOptions.map(({ id }) => id)).toEqual(['MONTHLY'])
+ })
+
+ it('returns no options when no period types are enabled', () => {
+ expect(getOptions([])).toEqual({
+ fixedOptions: [],
+ relativeOptions: [],
+ })
+ })
+})
+
+describe('useV44DataOutputPeriodTypes', () => {
+ const mockQueryState = {}
+
+ beforeEach(() => {
+ const { useDataQuery } = require('@dhis2/app-runtime')
+ mockQueryState.v44Refetch = jest.fn()
+ mockQueryState.v44Data = undefined
+ mockQueryState.v44Error = undefined
+
+ useDataQuery.mockImplementation(() => ({
+ data: mockQueryState.v44Data,
+ error: mockQueryState.v44Error,
+ refetch: mockQueryState.v44Refetch,
+ }))
+ })
+
+ it('does not fetch when disabled', () => {
+ const { result } = renderHook(() =>
+ useV44DataOutputPeriodTypes({ enabled: false })
+ )
+
+ expect(mockQueryState.v44Refetch).not.toHaveBeenCalled()
+ expect(result.current).toEqual({
+ enabledPeriodTypesData: null,
+ loading: false,
+ })
+ })
+
+ it('is loading until the period types have been fetched', () => {
+ const { result } = renderHook(() =>
+ useV44DataOutputPeriodTypes({ enabled: true })
+ )
+
+ expect(result.current).toEqual({
+ enabledPeriodTypesData: null,
+ loading: true,
+ })
+ })
+
+ it('stops loading without data when the request fails', () => {
+ mockQueryState.v44Error = new Error('Request failed')
+
+ const { result } = renderHook(() =>
+ useV44DataOutputPeriodTypes({ enabled: true })
+ )
+
+ expect(result.current).toEqual({
+ enabledPeriodTypesData: null,
+ loading: false,
+ })
+ })
+
+ it('fetches period types without a locale (server uses the user db locale)', () => {
+ const { useDataQuery } = require('@dhis2/app-runtime')
+
+ renderHook(() => useV44DataOutputPeriodTypes({ enabled: true }))
+
+ expect(mockQueryState.v44Refetch).toHaveBeenCalledTimes(1)
+ expect(mockQueryState.v44Refetch).toHaveBeenCalledWith()
+ const [query] = useDataQuery.mock.calls[0]
+ expect(query.periodTypes.params).not.toHaveProperty('locale')
+ })
+
+ it('returns enabled types and relative period name overrides', () => {
+ mockQueryState.v44Data = {
+ periodTypes: [daily, monthly],
+ analysisRelativePeriod: {
+ keyAnalysisRelativePeriod: 'LAST_12_MONTHS',
+ },
+ weeklyStart: { analyticsWeeklyStart: 'WEEKLY_SUNDAY' },
+ financialYearStart: {
+ analyticsFinancialYearStart: 'FINANCIAL_YEAR_APRIL',
+ },
+ }
+
+ const { result } = renderHook(() =>
+ useV44DataOutputPeriodTypes({ enabled: true })
+ )
+
+ expect(result.current.enabledPeriodTypesData).toEqual({
+ version: 44,
+ enabledTypes: [daily, monthly],
+ analysisRelativePeriod: 'LAST_12_MONTHS',
+ relativePeriodStartTypes: {
+ WEEKLY: 'WeeklySunday',
+ FINANCIAL: 'FinancialApril',
+ },
+ noEnabledTypes: false,
+ metaData: {
+ TODAY: { name: "Aujourd'hui" },
+ YESTERDAY: { name: 'Hier' },
+ THIS_MONTH: { name: 'Ce mois-ci' },
+ LAST_MONTH: { name: 'x-Last_Month' },
+ MONTHS_LAST_YEAR: { name: "Mois de l'an dernier" },
+ },
+ })
+ })
+
+ it('defaults the weekly start to Weekly when the setting is missing', () => {
+ mockQueryState.v44Data = { periodTypes: [weekly] }
+
+ const { result } = renderHook(() =>
+ useV44DataOutputPeriodTypes({ enabled: true })
+ )
+
+ expect(
+ result.current.enabledPeriodTypesData.relativePeriodStartTypes
+ ).toEqual({
+ WEEKLY: 'Weekly',
+ FINANCIAL: null,
+ })
+ })
+
+ it('keeps the server order of period types', () => {
+ mockQueryState.v44Data = {
+ periodTypes: [daily, monthly, financialOct],
+ }
+
+ const { result } = renderHook(() =>
+ useV44DataOutputPeriodTypes({ enabled: true })
+ )
+
+ expect(
+ result.current.enabledPeriodTypesData.enabledTypes.map(
+ ({ name }) => name
+ )
+ ).toEqual(['Daily', 'Monthly', 'FinancialOct'])
+ })
+
+ it('flags when no period types are enabled', () => {
+ mockQueryState.v44Data = { periodTypes: [] }
+
+ const { result } = renderHook(() =>
+ useV44DataOutputPeriodTypes({ enabled: true })
+ )
+
+ expect(result.current.enabledPeriodTypesData.noEnabledTypes).toBe(true)
+ })
+})
diff --git a/src/components/PeriodDimension/periodTypeOptions.js b/src/components/PeriodDimension/periodTypeOptions.js
new file mode 100644
index 000000000..6603a0fe0
--- /dev/null
+++ b/src/components/PeriodDimension/periodTypeOptions.js
@@ -0,0 +1,31 @@
+import { getV42PeriodTypeOptions } from './versions/v42.js'
+import { getV43PeriodTypeOptions } from './versions/v43.js'
+import { getV44PeriodTypeOptions } from './versions/v44.js'
+
+const getPeriodTypeOptions = ({
+ enabledPeriodTypesData,
+ periodsSettings,
+ excludedPeriodTypes,
+}) => {
+ switch (enabledPeriodTypesData?.version) {
+ case 44:
+ return getV44PeriodTypeOptions({
+ enabledPeriodTypesData,
+ periodsSettings,
+ excludedPeriodTypes,
+ })
+ case 43:
+ return getV43PeriodTypeOptions({
+ enabledPeriodTypesData,
+ periodsSettings,
+ excludedPeriodTypes,
+ })
+ default:
+ return getV42PeriodTypeOptions({
+ periodsSettings,
+ excludedPeriodTypes,
+ })
+ }
+}
+
+export { getPeriodTypeOptions }
diff --git a/src/components/PeriodDimension/useDataOutputPeriodTypes.js b/src/components/PeriodDimension/useDataOutputPeriodTypes.js
index f76031fdb..9659c6362 100644
--- a/src/components/PeriodDimension/useDataOutputPeriodTypes.js
+++ b/src/components/PeriodDimension/useDataOutputPeriodTypes.js
@@ -1,162 +1,29 @@
-import { useConfig, useDataQuery } from '@dhis2/app-runtime'
-import { useEffect, useMemo } from 'react'
-
-const v43Query = {
- enabledPeriodTypes: {
- resource: 'configuration/dataOutputPeriodTypes',
- },
- // v43-only: analyticsFinancialYearStart is removed in v44
- financialYearStart: {
- resource: 'systemSettings/analyticsFinancialYearStart',
- },
- analysisRelativePeriod: {
- resource: 'systemSettings/keyAnalysisRelativePeriod',
- },
- // v43-only: analyticsWeeklyStart is removed in v44
- weeklyStart: {
- resource: 'systemSettings/analyticsWeeklyStart',
- },
-}
-
-// v43-only: analyticsFinancialYearStart is removed in v44
-const FY_SETTING_TO_SERVER_PT = {
- FINANCIAL_YEAR_FEBRUARY: 'FinancialFeb',
- FINANCIAL_YEAR_APRIL: 'FinancialApril',
- FINANCIAL_YEAR_JULY: 'FinancialJuly',
- FINANCIAL_YEAR_AUGUST: 'FinancialAug',
- FINANCIAL_YEAR_SEPTEMBER: 'FinancialSep',
- FINANCIAL_YEAR_OCTOBER: 'FinancialOct',
- FINANCIAL_YEAR_NOVEMBER: 'FinancialNov',
-}
-
-// v43-only: analyticsWeeklyStart is removed in v44
-const WEEKLY_START_TO_SERVER_PT = {
- WEEKLY: 'Weekly',
- WEEKLY_WEDNESDAY: 'WeeklyWednesday',
- WEEKLY_THURSDAY: 'WeeklyThursday',
- WEEKLY_FRIDAY: 'WeeklyFriday',
- WEEKLY_SATURDAY: 'WeeklySaturday',
- WEEKLY_SUNDAY: 'WeeklySunday',
-}
+import { useConfig } from '@dhis2/app-runtime'
+import { useV43DataOutputPeriodTypes } from './versions/v43.js'
+import { useV44DataOutputPeriodTypes } from './versions/v44.js'
const useDataOutputPeriodTypes = () => {
const { serverVersion } = useConfig()
const supportsEnabledPeriodTypes = serverVersion.minor >= 43
+ const supportsPeriodLabelTranslations = serverVersion.minor >= 44
- const {
- data: v43Data,
- error: v43Error,
- refetch: v43Refetch,
- } = useDataQuery(v43Query, { lazy: true })
-
- useEffect(() => {
- if (supportsEnabledPeriodTypes) {
- v43Refetch()
- }
- }, [supportsEnabledPeriodTypes, v43Refetch])
-
- const enabledPeriodTypesData = useMemo(() => {
- if (!supportsEnabledPeriodTypes) {
- return null
- }
-
- if (v43Error || !v43Data?.enabledPeriodTypes) {
- return null
- }
-
- const enabledTypes = v43Data.enabledPeriodTypes
-
- if (!enabledTypes || enabledTypes.length === 0) {
- return {
- enabledTypes: [],
- financialYearStart: null,
- analysisRelativePeriod: null,
- noEnabledTypes: true,
- }
- }
-
- // v43-only: financial year logic goes away in v44
- let financialYearStart = null
- let financialYearDisplayLabel = null
- if (v43Data.financialYearStart?.analyticsFinancialYearStart) {
- const fyStartValue =
- v43Data.financialYearStart.analyticsFinancialYearStart
-
- const mappedFyPt = FY_SETTING_TO_SERVER_PT[fyStartValue]
- const matchingPt = enabledTypes.find((pt) => pt.name === mappedFyPt)
- if (matchingPt) {
- financialYearStart = fyStartValue
- if (matchingPt.displayLabel) {
- financialYearDisplayLabel = matchingPt.displayLabel
- }
- }
- }
-
- // v43-only: weekly start logic goes away in v44
- let weeklyDisplayLabel = null
- if (v43Data.weeklyStart?.analyticsWeeklyStart) {
- const weeklyStartValue = v43Data.weeklyStart.analyticsWeeklyStart
-
- const mappedWeeklyPt = WEEKLY_START_TO_SERVER_PT[weeklyStartValue]
- const matchingWeeklyPt = enabledTypes.find(
- (pt) => pt.name === mappedWeeklyPt
- )
- if (matchingWeeklyPt?.displayLabel) {
- weeklyDisplayLabel = matchingWeeklyPt.displayLabel
- }
- }
-
- const analysisRelativePeriod =
- v43Data.analysisRelativePeriod?.keyAnalysisRelativePeriod || null
-
- const metaData = {
- ...(financialYearDisplayLabel && {
- THIS_FINANCIAL_YEAR: {
- name: `This ${financialYearDisplayLabel}`,
- },
- LAST_FINANCIAL_YEAR: {
- name: `Last ${financialYearDisplayLabel}`,
- },
- LAST_5_FINANCIAL_YEARS: {
- name: `Last 5 ${financialYearDisplayLabel}`,
- },
- }),
- ...(weeklyDisplayLabel && {
- THIS_WEEK: {
- name: `This ${weeklyDisplayLabel}`,
- },
- LAST_WEEK: {
- name: `Last ${weeklyDisplayLabel}`,
- },
- LAST_4_WEEKS: {
- name: `Last 4 ${weeklyDisplayLabel}`,
- },
- LAST_12_WEEKS: {
- name: `Last 12 ${weeklyDisplayLabel}`,
- },
- LAST_52_WEEKS: {
- name: `Last 52 ${weeklyDisplayLabel}`,
- },
- WEEKS_THIS_YEAR: {
- name: `${weeklyDisplayLabel} this year`,
- },
- }),
- }
+ const v43 = useV43DataOutputPeriodTypes({
+ enabled: supportsEnabledPeriodTypes && !supportsPeriodLabelTranslations,
+ })
+ const v44 = useV44DataOutputPeriodTypes({
+ enabled: supportsPeriodLabelTranslations,
+ })
- return {
- enabledTypes,
- financialYearStart,
- financialYearDisplayLabel,
- weeklyDisplayLabel,
- analysisRelativePeriod,
- metaData,
- noEnabledTypes: false,
- }
- }, [supportsEnabledPeriodTypes, v43Data, v43Error])
+ // enabledPeriodTypesData stays null on v42 and when the request fails,
+ // loading tells consumers whether to wait for it
+ const { enabledPeriodTypesData, loading } = supportsPeriodLabelTranslations
+ ? v44
+ : v43
return {
supportsEnabledPeriodTypes,
enabledPeriodTypesData,
+ loading,
}
}
diff --git a/src/components/PeriodDimension/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js
index 7cadc0833..2737949a7 100644
--- a/src/components/PeriodDimension/utils/enabledPeriodTypes.js
+++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js
@@ -25,6 +25,27 @@ export const SERVER_PT_TO_MULTI_CALENDAR_PT = {
FinancialNov: 'FYNOV',
}
+// Mapping from the analyticsFinancialYearStart system setting to server period type names
+export const FY_SETTING_TO_SERVER_PT = {
+ FINANCIAL_YEAR_FEBRUARY: 'FinancialFeb',
+ FINANCIAL_YEAR_APRIL: 'FinancialApril',
+ FINANCIAL_YEAR_JULY: 'FinancialJuly',
+ FINANCIAL_YEAR_AUGUST: 'FinancialAug',
+ FINANCIAL_YEAR_SEPTEMBER: 'FinancialSep',
+ FINANCIAL_YEAR_OCTOBER: 'FinancialOct',
+ FINANCIAL_YEAR_NOVEMBER: 'FinancialNov',
+}
+
+// Mapping from the analyticsWeeklyStart system setting to server period type names
+export const WEEKLY_START_TO_SERVER_PT = {
+ WEEKLY: 'Weekly',
+ WEEKLY_WEDNESDAY: 'WeeklyWednesday',
+ WEEKLY_THURSDAY: 'WeeklyThursday',
+ WEEKLY_FRIDAY: 'WeeklyFriday',
+ WEEKLY_SATURDAY: 'WeeklySaturday',
+ WEEKLY_SUNDAY: 'WeeklySunday',
+}
+
// Mapping from relative period categories to their corresponding fixed period types
export const RP_CATEGORY_TO_FP_DEPENDENCIES = {
DAILY: ['Daily'],
@@ -77,48 +98,8 @@ export const filterEnabledFixedPeriodTypes = (
)
}
-/**
- * Apply displayLabel overrides to fixed period type names
- * v43-only: in v44 the API provides these names directly
- */
-export const applyFixedPeriodTypeDisplayLabels = (
- filteredFixedOptions,
- enabledServerPeriodTypes
-) => {
- if (!enabledServerPeriodTypes) {
- return filteredFixedOptions
- }
-
- const displayLabelMap = new Map()
- enabledServerPeriodTypes.forEach((pt) => {
- if (pt.displayLabel) {
- const multiCalendarPt = SERVER_PT_TO_MULTI_CALENDAR_PT[pt.name]
- if (multiCalendarPt) {
- displayLabelMap.set(multiCalendarPt, pt.displayLabel)
- }
- }
- })
-
- if (displayLabelMap.size === 0) {
- return filteredFixedOptions
- }
-
- return filteredFixedOptions.map((option) => {
- const displayLabel = displayLabelMap.get(option.id)
- return displayLabel ? { ...option, name: displayLabel } : option
- })
-}
-
-/**
- * Filter relative period categories based on enabled server period types
- * @param {Array} allRelativePeriodOptions - All available relative period options
- * @param {Array} enabledServerPeriodTypes - Enabled period types from server
- * @param {string|null} financialYearStart - Financial year start setting (if enabled)
- * @returns {Array} Filtered relative period options
- */
/**
* Apply metaData name overrides to a list of periods
- * v43-only: in v44 the API provides these names directly
*/
export const applyPeriodNameOverrides = (periods, metaData) => {
if (!metaData) {
@@ -130,69 +111,3 @@ export const applyPeriodNameOverrides = (periods, metaData) => {
: period
)
}
-
-/**
- * Apply display label overrides to relative period options
- * v43-only: in v44 the API provides these names directly
- */
-export const applyDisplayLabelOverrides = (
- filteredRelativeOptions,
- { financialYearDisplayLabel, weeklyDisplayLabel, metaData }
-) => {
- const overrides = {}
-
- if (financialYearDisplayLabel) {
- overrides['FINANCIAL'] = { name: financialYearDisplayLabel }
- }
- if (weeklyDisplayLabel) {
- overrides['WEEKLY'] = { name: weeklyDisplayLabel }
- }
-
- if (Object.keys(overrides).length === 0) {
- return filteredRelativeOptions
- }
-
- return filteredRelativeOptions.map((option) => {
- const override = overrides[option.id]
- if (!override) {
- return option
- }
- return {
- ...option,
- name: override.name,
- getPeriods: () =>
- applyPeriodNameOverrides(option.getPeriods(), metaData),
- }
- })
-}
-
-export const filterEnabledRelativePeriodTypes = (
- allRelativePeriodOptions,
- enabledServerPeriodTypes,
- financialYearStart = null
-) => {
- if (!enabledServerPeriodTypes || enabledServerPeriodTypes.length === 0) {
- return []
- }
-
- const enabledServerPtNames = new Set(
- enabledServerPeriodTypes.map((pt) => pt.name)
- )
-
- return allRelativePeriodOptions.filter((option) => {
- // Special handling for financial years
- if (option.id === 'FINANCIAL') {
- return financialYearStart !== null
- }
-
- // Check if any of the required FP dependencies are enabled
- const requiredFpTypes = RP_CATEGORY_TO_FP_DEPENDENCIES[option.id]
- if (!requiredFpTypes) {
- return true // Show if no dependency mapping (shouldn't happen)
- }
-
- return requiredFpTypes.some((fpType) =>
- enabledServerPtNames.has(fpType)
- )
- })
-}
diff --git a/src/components/PeriodDimension/versions/v42.js b/src/components/PeriodDimension/versions/v42.js
new file mode 100644
index 000000000..a91f4effa
--- /dev/null
+++ b/src/components/PeriodDimension/versions/v42.js
@@ -0,0 +1,33 @@
+// v42: no enabled period types API, all period types and relative periods
+// come from the hardcoded client lists.
+// Remove this file (and its branch in periodTypeOptions.js) when v42 is no longer supported.
+import { getFixedPeriodsOptions } from '../utils/fixedPeriods.js'
+import {
+ FYAUG,
+ FYFEB,
+ FYSEP,
+ WEEKLYFRI,
+ filterPeriodTypesById,
+} from '../utils/index.js'
+import { getRelativePeriodsOptions } from '../utils/relativePeriods.js'
+
+// period types that were introduced in v43
+const V43_PERIOD_TYPES = [WEEKLYFRI, FYFEB, FYAUG, FYSEP]
+
+export const getV42PeriodTypeOptions = ({
+ periodsSettings,
+ excludedPeriodTypes,
+}) => {
+ const allExcludedPeriodTypes = [...excludedPeriodTypes, ...V43_PERIOD_TYPES]
+
+ return {
+ fixedOptions: filterPeriodTypesById(
+ getFixedPeriodsOptions(periodsSettings),
+ allExcludedPeriodTypes
+ ),
+ relativeOptions: filterPeriodTypesById(
+ getRelativePeriodsOptions(),
+ allExcludedPeriodTypes
+ ),
+ }
+}
diff --git a/src/components/PeriodDimension/versions/v43.js b/src/components/PeriodDimension/versions/v43.js
new file mode 100644
index 000000000..3bcb990c6
--- /dev/null
+++ b/src/components/PeriodDimension/versions/v43.js
@@ -0,0 +1,292 @@
+// v43: enabled period types from /configuration/dataOutputPeriodTypes, with a
+// single `label` per period type. Relative period names are derived client side
+// from that label ("This {label}", "Last {label}", ...).
+// Remove this file (and its branches in useDataOutputPeriodTypes and
+// periodTypeOptions.js) when v43 is no longer supported.
+import { useDataQuery } from '@dhis2/app-runtime'
+import { useEffect, useMemo } from 'react'
+import {
+ RP_CATEGORY_TO_FP_DEPENDENCIES,
+ SERVER_PT_TO_MULTI_CALENDAR_PT,
+ FY_SETTING_TO_SERVER_PT,
+ WEEKLY_START_TO_SERVER_PT,
+ applyPeriodNameOverrides,
+ filterEnabledFixedPeriodTypes,
+} from '../utils/enabledPeriodTypes.js'
+import { getFixedPeriodsOptions } from '../utils/fixedPeriods.js'
+import { filterPeriodTypesById } from '../utils/index.js'
+import { getRelativePeriodsOptions } from '../utils/relativePeriods.js'
+
+const v43Query = {
+ enabledPeriodTypes: {
+ resource: 'configuration/dataOutputPeriodTypes',
+ },
+ financialYearStart: {
+ resource: 'systemSettings/analyticsFinancialYearStart',
+ },
+ analysisRelativePeriod: {
+ resource: 'systemSettings/keyAnalysisRelativePeriod',
+ },
+ weeklyStart: {
+ resource: 'systemSettings/analyticsWeeklyStart',
+ },
+}
+
+export const useV43DataOutputPeriodTypes = ({ enabled }) => {
+ const {
+ data: v43Data,
+ error: v43Error,
+ refetch: v43Refetch,
+ } = useDataQuery(v43Query, { lazy: true })
+
+ useEffect(() => {
+ if (enabled) {
+ v43Refetch()
+ }
+ }, [enabled, v43Refetch])
+
+ const enabledPeriodTypesData = useMemo(() => {
+ if (!enabled) {
+ return null
+ }
+
+ if (v43Error || !v43Data?.enabledPeriodTypes) {
+ return null
+ }
+
+ const enabledTypes = v43Data.enabledPeriodTypes
+
+ if (!enabledTypes || enabledTypes.length === 0) {
+ return {
+ version: 43,
+ enabledTypes: [],
+ financialYearStart: null,
+ analysisRelativePeriod: null,
+ noEnabledTypes: true,
+ }
+ }
+
+ let financialYearStart = null
+ let financialYearDisplayLabel = null
+ if (v43Data.financialYearStart?.analyticsFinancialYearStart) {
+ const fyStartValue =
+ v43Data.financialYearStart.analyticsFinancialYearStart
+
+ const mappedFyPt = FY_SETTING_TO_SERVER_PT[fyStartValue]
+ const matchingPt = enabledTypes.find((pt) => pt.name === mappedFyPt)
+ if (matchingPt) {
+ financialYearStart = fyStartValue
+ if (matchingPt.displayLabel) {
+ financialYearDisplayLabel = matchingPt.displayLabel
+ }
+ }
+ }
+
+ let weeklyDisplayLabel = null
+ if (v43Data.weeklyStart?.analyticsWeeklyStart) {
+ const weeklyStartValue = v43Data.weeklyStart.analyticsWeeklyStart
+
+ const mappedWeeklyPt = WEEKLY_START_TO_SERVER_PT[weeklyStartValue]
+ const matchingWeeklyPt = enabledTypes.find(
+ (pt) => pt.name === mappedWeeklyPt
+ )
+ if (matchingWeeklyPt?.displayLabel) {
+ weeklyDisplayLabel = matchingWeeklyPt.displayLabel
+ }
+ }
+
+ const analysisRelativePeriod =
+ v43Data.analysisRelativePeriod?.keyAnalysisRelativePeriod || null
+
+ const metaData = {
+ ...(financialYearDisplayLabel && {
+ THIS_FINANCIAL_YEAR: {
+ name: `This ${financialYearDisplayLabel}`,
+ },
+ LAST_FINANCIAL_YEAR: {
+ name: `Last ${financialYearDisplayLabel}`,
+ },
+ LAST_5_FINANCIAL_YEARS: {
+ name: `Last 5 ${financialYearDisplayLabel}`,
+ },
+ }),
+ ...(weeklyDisplayLabel && {
+ THIS_WEEK: {
+ name: `This ${weeklyDisplayLabel}`,
+ },
+ LAST_WEEK: {
+ name: `Last ${weeklyDisplayLabel}`,
+ },
+ LAST_4_WEEKS: {
+ name: `Last 4 ${weeklyDisplayLabel}`,
+ },
+ LAST_12_WEEKS: {
+ name: `Last 12 ${weeklyDisplayLabel}`,
+ },
+ LAST_52_WEEKS: {
+ name: `Last 52 ${weeklyDisplayLabel}`,
+ },
+ WEEKS_THIS_YEAR: {
+ name: `${weeklyDisplayLabel} this year`,
+ },
+ }),
+ }
+
+ return {
+ version: 43,
+ enabledTypes,
+ financialYearStart,
+ financialYearDisplayLabel,
+ weeklyDisplayLabel,
+ analysisRelativePeriod,
+ metaData,
+ noEnabledTypes: false,
+ }
+ }, [enabled, v43Data, v43Error])
+
+ return {
+ enabledPeriodTypesData,
+ loading: enabled && !v43Data && !v43Error,
+ }
+}
+
+/**
+ * Apply displayLabel overrides to fixed period type names
+ */
+export const applyFixedPeriodTypeDisplayLabels = (
+ filteredFixedOptions,
+ enabledServerPeriodTypes
+) => {
+ if (!enabledServerPeriodTypes) {
+ return filteredFixedOptions
+ }
+
+ const displayLabelMap = new Map()
+ enabledServerPeriodTypes.forEach((pt) => {
+ if (pt.displayLabel) {
+ const multiCalendarPt = SERVER_PT_TO_MULTI_CALENDAR_PT[pt.name]
+ if (multiCalendarPt) {
+ displayLabelMap.set(multiCalendarPt, pt.displayLabel)
+ }
+ }
+ })
+
+ if (displayLabelMap.size === 0) {
+ return filteredFixedOptions
+ }
+
+ return filteredFixedOptions.map((option) => {
+ const displayLabel = displayLabelMap.get(option.id)
+ return displayLabel ? { ...option, name: displayLabel } : option
+ })
+}
+
+/**
+ * Apply display label overrides to relative period options
+ */
+export const applyDisplayLabelOverrides = (
+ filteredRelativeOptions,
+ { financialYearDisplayLabel, weeklyDisplayLabel, metaData }
+) => {
+ const overrides = {}
+
+ if (financialYearDisplayLabel) {
+ overrides['FINANCIAL'] = { name: financialYearDisplayLabel }
+ }
+ if (weeklyDisplayLabel) {
+ overrides['WEEKLY'] = { name: weeklyDisplayLabel }
+ }
+
+ if (Object.keys(overrides).length === 0) {
+ return filteredRelativeOptions
+ }
+
+ return filteredRelativeOptions.map((option) => {
+ const override = overrides[option.id]
+ if (!override) {
+ return option
+ }
+ return {
+ ...option,
+ name: override.name,
+ getPeriods: () =>
+ applyPeriodNameOverrides(option.getPeriods(), metaData),
+ }
+ })
+}
+
+/**
+ * Filter relative period categories based on enabled server period types
+ * @param {Array} allRelativePeriodOptions - All available relative period options
+ * @param {Array} enabledServerPeriodTypes - Enabled period types from server
+ * @param {string|null} financialYearStart - Financial year start setting (if enabled)
+ * @returns {Array} Filtered relative period options
+ */
+export const filterEnabledRelativePeriodTypes = (
+ allRelativePeriodOptions,
+ enabledServerPeriodTypes,
+ financialYearStart = null
+) => {
+ if (!enabledServerPeriodTypes || enabledServerPeriodTypes.length === 0) {
+ return []
+ }
+
+ const enabledServerPtNames = new Set(
+ enabledServerPeriodTypes.map((pt) => pt.name)
+ )
+
+ return allRelativePeriodOptions.filter((option) => {
+ // Special handling for financial years
+ if (option.id === 'FINANCIAL') {
+ return financialYearStart !== null
+ }
+
+ // Check if any of the required FP dependencies are enabled
+ const requiredFpTypes = RP_CATEGORY_TO_FP_DEPENDENCIES[option.id]
+ if (!requiredFpTypes) {
+ return true // Show if no dependency mapping (shouldn't happen)
+ }
+
+ return requiredFpTypes.some((fpType) =>
+ enabledServerPtNames.has(fpType)
+ )
+ })
+}
+
+export const getV43PeriodTypeOptions = ({
+ enabledPeriodTypesData,
+ periodsSettings,
+ excludedPeriodTypes,
+}) => {
+ const {
+ enabledTypes,
+ financialYearStart,
+ financialYearDisplayLabel,
+ weeklyDisplayLabel,
+ metaData,
+ } = enabledPeriodTypesData
+
+ return {
+ fixedOptions: filterPeriodTypesById(
+ applyFixedPeriodTypeDisplayLabels(
+ filterEnabledFixedPeriodTypes(
+ getFixedPeriodsOptions(periodsSettings),
+ enabledTypes
+ ),
+ enabledTypes
+ ),
+ excludedPeriodTypes
+ ),
+ relativeOptions: filterPeriodTypesById(
+ applyDisplayLabelOverrides(
+ filterEnabledRelativePeriodTypes(
+ getRelativePeriodsOptions(),
+ enabledTypes,
+ financialYearStart
+ ),
+ { financialYearDisplayLabel, weeklyDisplayLabel, metaData }
+ ),
+ excludedPeriodTypes
+ ),
+ }
+}
diff --git a/src/components/PeriodDimension/versions/v44.js b/src/components/PeriodDimension/versions/v44.js
new file mode 100644
index 000000000..b0c7775bb
--- /dev/null
+++ b/src/components/PeriodDimension/versions/v44.js
@@ -0,0 +1,194 @@
+// v44: /configuration/dataOutputPeriodTypes returns the enabled period types
+// with their relative periods nested, all with a `displayName` that already
+// resolves label translation -> label -> default name. No locale is passed, so
+// the server uses the user's db locale (keyDbLocale), like for other metadata.
+// The client no longer derives any names, it only maps server period types to
+// the client period type ids used for generating fixed periods.
+// Weekly and financial year relative periods have one set of labels; the
+// analyticsWeeklyStart and analyticsFinancialYearStart system settings decide
+// which start variant they refer to.
+import { useDataQuery } from '@dhis2/app-runtime'
+import { useEffect, useMemo } from 'react'
+import {
+ FY_SETTING_TO_SERVER_PT,
+ RP_CATEGORY_TO_FP_DEPENDENCIES,
+ SERVER_PT_TO_MULTI_CALENDAR_PT,
+ WEEKLY_START_TO_SERVER_PT,
+} from '../utils/enabledPeriodTypes.js'
+import { getFixedPeriodsOptions } from '../utils/fixedPeriods.js'
+import { FINANCIAL, WEEKLY, filterPeriodTypesById } from '../utils/index.js'
+
+const v44Query = {
+ periodTypes: {
+ resource: 'configuration/dataOutputPeriodTypes',
+ params: {
+ fields: 'name,displayName,relativePeriods',
+ },
+ },
+ analysisRelativePeriod: {
+ resource: 'systemSettings/keyAnalysisRelativePeriod',
+ },
+ weeklyStart: {
+ resource: 'systemSettings/analyticsWeeklyStart',
+ },
+ financialYearStart: {
+ resource: 'systemSettings/analyticsFinancialYearStart',
+ },
+}
+
+// Server period types grouped by the relative period category they belong to.
+// The category ids (e.g. WEEKLY, FINANCIAL) are part of the public API
+// (excludedPeriodTypes, default selection) and the server has no equivalent.
+// The first period type of each family is the standard one.
+const RP_CATEGORY_TO_SERVER_PTS = {
+ ...RP_CATEGORY_TO_FP_DEPENDENCIES,
+ [FINANCIAL]: Object.values(FY_SETTING_TO_SERVER_PT),
+}
+
+// Relative period categories whose relative periods refer to the start variant
+// selected in the system settings (see relativePeriodStartTypes)
+const START_SETTING_CATEGORIES = new Set([WEEKLY, FINANCIAL])
+
+const getRelativeCategoryId = (serverPtName) =>
+ Object.keys(RP_CATEGORY_TO_SERVER_PTS).find((categoryId) =>
+ RP_CATEGORY_TO_SERVER_PTS[categoryId].includes(serverPtName)
+ )
+
+const getRelativePeriodsFromPeriodType = (periodType) =>
+ Object.entries(periodType.relativePeriods || {}).map(
+ ([id, { displayName }]) => ({ id, name: displayName })
+ )
+
+export const useV44DataOutputPeriodTypes = ({ enabled }) => {
+ const { data, error, refetch } = useDataQuery(v44Query, { lazy: true })
+
+ useEffect(() => {
+ if (enabled) {
+ refetch()
+ }
+ }, [enabled, refetch])
+
+ const enabledPeriodTypesData = useMemo(() => {
+ if (!enabled || error || !data?.periodTypes) {
+ return null
+ }
+
+ // the server returns period types in natural order (days -> financial years)
+ const enabledTypes = data.periodTypes
+
+ // relative period id -> name, used for overriding stored names
+ // (i.e. selected items in a saved visualization)
+ const metaData = enabledTypes
+ .flatMap(getRelativePeriodsFromPeriodType)
+ .reduce((acc, { id, name }) => {
+ acc[id] = { name }
+ return acc
+ }, {})
+
+ return {
+ version: 44,
+ enabledTypes,
+ analysisRelativePeriod:
+ data.analysisRelativePeriod?.keyAnalysisRelativePeriod || null,
+ // server period type names of the start variants the weekly and
+ // financial year relative periods refer to
+ relativePeriodStartTypes: {
+ [WEEKLY]:
+ WEEKLY_START_TO_SERVER_PT[
+ data.weeklyStart?.analyticsWeeklyStart
+ ] || WEEKLY_START_TO_SERVER_PT.WEEKLY,
+ [FINANCIAL]:
+ FY_SETTING_TO_SERVER_PT[
+ data.financialYearStart?.analyticsFinancialYearStart
+ ] || null,
+ },
+ metaData,
+ noEnabledTypes: enabledTypes.length === 0,
+ }
+ }, [enabled, data, error])
+
+ return { enabledPeriodTypesData, loading: enabled && !data && !error }
+}
+
+const getV44FixedOptions = ({ enabledTypes, periodsSettings }) => {
+ const clientOptionsById = getFixedPeriodsOptions(periodsSettings).reduce(
+ (acc, option) => {
+ acc[option.id] = option
+ return acc
+ },
+ {}
+ )
+
+ return enabledTypes
+ .map((pt) => {
+ const option =
+ clientOptionsById[SERVER_PT_TO_MULTI_CALENDAR_PT[pt.name]]
+
+ // server period types without client support for generating periods are skipped
+ return option && { ...option, name: pt.displayName || option.name }
+ })
+ .filter(Boolean)
+}
+
+const getV44RelativeOption = (
+ categoryId,
+ enabledTypes,
+ relativePeriodStartTypes
+) => {
+ // The server nests the relative periods of a family under every period
+ // type of the family, but they only refer to one of them: the start variant
+ // selected in the system settings for weekly and financial years, and the
+ // standard period type (first in the family) for the others, as there are
+ // no settings for e.g. quarterly or six-monthly start variants. The
+ // relative periods are only available when that period type is enabled.
+ const sourceTypeName = START_SETTING_CATEGORIES.has(categoryId)
+ ? relativePeriodStartTypes?.[categoryId]
+ : RP_CATEGORY_TO_SERVER_PTS[categoryId][0]
+
+ const sourceType = enabledTypes.find((pt) => pt.name === sourceTypeName)
+
+ const periods = sourceType
+ ? getRelativePeriodsFromPeriodType(sourceType)
+ : []
+
+ if (!periods.length) {
+ return null
+ }
+
+ return {
+ id: categoryId,
+ name: sourceType.displayName,
+ getPeriods: () => periods,
+ }
+}
+
+const getV44RelativeOptions = ({ enabledTypes, relativePeriodStartTypes }) =>
+ [...new Set(enabledTypes.map((pt) => getRelativeCategoryId(pt.name)))]
+ .filter(Boolean)
+ .map((categoryId) =>
+ getV44RelativeOption(
+ categoryId,
+ enabledTypes,
+ relativePeriodStartTypes
+ )
+ )
+ .filter(Boolean)
+
+export const getV44PeriodTypeOptions = ({
+ enabledPeriodTypesData,
+ periodsSettings,
+ excludedPeriodTypes,
+}) => {
+ const { enabledTypes, relativePeriodStartTypes } = enabledPeriodTypesData
+
+ return {
+ fixedOptions: filterPeriodTypesById(
+ getV44FixedOptions({ enabledTypes, periodsSettings }),
+ excludedPeriodTypes
+ ),
+ relativeOptions: filterPeriodTypesById(
+ getV44RelativeOptions({ enabledTypes, relativePeriodStartTypes }),
+ excludedPeriodTypes
+ ),
+ }
+}