From 9f39c2e3c19b8dfe75b2924cba38f47dd707f599 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Thu, 4 Jan 2024 09:21:01 +0100 Subject: [PATCH] feat: add support for period item max limit (DHIS2-16399) --- i18n/en.pot | 10 +-- .../PeriodDimension/PeriodDimension.js | 5 +- .../PeriodDimension/PeriodTransfer.js | 88 ++++++++++++------- .../PeriodDimension.spec.js.snap | 2 +- .../__snapshots__/PeriodSelector.spec.js.snap | 13 +-- src/index.js | 2 + .../layoutUiRules/__tests__/rules.spec.js | 21 +++++ src/modules/layoutUiRules/index.js | 2 + src/modules/layoutUiRules/rules.js | 10 +++ src/modules/layoutUiRules/rulesHelper.js | 4 + src/modules/layoutUiRules/rulesUtils.js | 16 ++++ 11 files changed, 121 insertions(+), 52 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index aaa2c2e51..f706f9d55 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-12-18T09:34:16.389Z\n" -"PO-Revision-Date: 2023-12-18T09:34:16.389Z\n" +"POT-Creation-Date: 2024-01-03T12:42:34.756Z\n" +"PO-Revision-Date: 2024-01-03T12:42:34.756Z\n" msgid "view only" msgstr "view only" @@ -654,15 +654,15 @@ msgstr "Select year" msgid "Period" msgstr "Period" +msgid "Selected Periods" +msgstr "Selected Periods" + msgid "Relative periods" msgstr "Relative periods" msgid "Fixed periods" msgstr "Fixed periods" -msgid "Selected Periods" -msgstr "Selected Periods" - msgid "No periods selected" msgstr "No periods selected" diff --git a/src/components/PeriodDimension/PeriodDimension.js b/src/components/PeriodDimension/PeriodDimension.js index e60cdf029..7e20dbc6e 100644 --- a/src/components/PeriodDimension/PeriodDimension.js +++ b/src/components/PeriodDimension/PeriodDimension.js @@ -18,6 +18,7 @@ const PeriodDimension = ({ selectedPeriods, rightFooter, excludedPeriodTypes, + infoBoxMessage, }) => { const { systemInfo } = useConfig() const result = useDataQuery(userSettingsQuery) @@ -36,7 +37,8 @@ const PeriodDimension = ({ return ( ( + <> +

{i18n.t('Selected Periods')}

+ {infoText && ( +
+
+ +
+ {infoText} +
+ )} + + +) + +RightHeader.propTypes = { + infoText: PropTypes.string, +} + const PeriodTransfer = ({ onSelect, dataTest, - initialSelectedPeriods, + selectedItems, rightFooter, excludedPeriodTypes, periodsSettings, + infoBoxMessage, }) => { const defaultRelativePeriodType = excludedPeriodTypes.includes(MONTHLY) ? getRelativePeriodsOptionsById(QUARTERLY) @@ -46,9 +66,6 @@ const PeriodTransfer = ({ const [allPeriods, setAllPeriods] = useState( defaultRelativePeriodType.getPeriods() ) - const [selectedPeriods, setSelectedPeriods] = useState( - initialSelectedPeriods - ) const [isRelative, setIsRelative] = useState(true) const [relativeFilter, setRelativeFilter] = useState({ periodType: defaultRelativePeriodType.id, @@ -58,6 +75,11 @@ const PeriodTransfer = ({ year: defaultFixedPeriodYear.toString(), }) + const isActive = (value) => { + const item = selectedItems.find((item) => item.id === value) + return !item || item.isActive + } + const onIsRelativeClick = (state) => { if (state !== isRelative) { setIsRelative(state) @@ -132,13 +154,6 @@ const PeriodTransfer = ({ ) - const renderRightHeader = () => ( - <> -

{i18n.t('Selected Periods')}

- - - ) - const onSelectFixedPeriods = (filter) => { setFixedFilter(filter) setAllPeriods( @@ -162,35 +177,40 @@ const PeriodTransfer = ({ return ( { - const formattedItems = selected.map((id) => ({ - id, - name: [...allPeriods, ...selectedPeriods].find( + const formattedItems = selected.map((id) => { + const matchingItem = [...allPeriods, ...selectedItems].find( (item) => item.id === id - ).name, - })) - setSelectedPeriods(formattedItems) + ) + + return { + id, + name: matchingItem.name, + isActive: matchingItem.isActive, + } + }) onSelect(formattedItems) }} - selected={selectedPeriods.map((period) => period.id)} + selected={selectedItems.map((period) => period.id)} leftHeader={renderLeftHeader()} enableOrderChange height={TRANSFER_HEIGHT} optionsWidth={TRANSFER_OPTIONS_WIDTH} selectedWidth={TRANSFER_SELECTED_WIDTH} selectedEmptyComponent={renderEmptySelection()} - rightHeader={renderRightHeader()} + rightHeader={} rightFooter={rightFooter} - options={[...allPeriods, ...selectedPeriods].map( - ({ id, name }) => ({ - label: name, - value: id, - }) - )} + options={[...allPeriods, ...selectedItems].map(({ id, name }) => ({ + label: name, + value: id, + }))} renderOption={(props) => ( )} dataTest={`${dataTest}-transfer`} @@ -199,7 +219,7 @@ const PeriodTransfer = ({ } PeriodTransfer.defaultProps = { - initialSelectedPeriods: [], + selectedItems: [], excludedPeriodTypes: [], periodsSettings: { calendar: 'gregory', @@ -211,17 +231,19 @@ PeriodTransfer.propTypes = { onSelect: PropTypes.func.isRequired, dataTest: PropTypes.string, excludedPeriodTypes: PropTypes.arrayOf(PropTypes.string), - initialSelectedPeriods: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - }) - ), + infoBoxMessage: PropTypes.string, periodsSettings: PropTypes.shape({ calendar: PropTypes.string, locale: PropTypes.string, }), rightFooter: PropTypes.node, + selectedItems: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + isActive: PropTypes.bool, + name: PropTypes.string, + }) + ), } export default PeriodTransfer diff --git a/src/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap b/src/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap index 1bf93ba94..4ad9ab073 100644 --- a/src/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap +++ b/src/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap @@ -4,7 +4,6 @@ exports[`The Period Dimension component matches the snapshot 1`] = ` } + selectedItems={Array []} /> `; diff --git a/src/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap b/src/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap index fa5dbe8d4..d13930874 100644 --- a/src/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap +++ b/src/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap @@ -77,18 +77,7 @@ exports[`The Period Selector component matches the snapshot 1`] = ` optionsWidth="420px" renderOption={[Function]} rightFooter={} - rightHeader={ - -

- Selected Periods -

- -
- } + rightHeader={} selected={Array []} selectedEmptyComponent={ diff --git a/src/index.js b/src/index.js index 648b7b19d..059648293 100644 --- a/src/index.js +++ b/src/index.js @@ -238,10 +238,12 @@ export { export { getAvailableAxes, getDisallowedDimensions, + getDimensionMaxNumberOfItems, getAxisMaxNumberOfItems, getAxisMaxNumberOfDimensions, getAxisMinNumberOfDimensions, hasAxisTooManyItems, + hasDimensionTooManyItems, getAxisPerLockedDimension, getAllLockedDimensionIds, canDimensionBeAddedToAxis, diff --git a/src/modules/layoutUiRules/__tests__/rules.spec.js b/src/modules/layoutUiRules/__tests__/rules.spec.js index 0b4598e91..79565e748 100644 --- a/src/modules/layoutUiRules/__tests__/rules.spec.js +++ b/src/modules/layoutUiRules/__tests__/rules.spec.js @@ -25,6 +25,9 @@ const allArrayItemsAreValid = (allItems, validItems) => const allArrayItemsAreValidAxisIds = (array) => allArrayItemsAreValid(array, ALL_AXIS_IDS) +const allArrayItemsAreValidDimensionIds = (array) => + allArrayItemsAreValid(array, lockableDims) + const onlyRulesWithProp = (ruleProp) => testResourceRules.filter((rule) => rule[ruleProp]) @@ -49,6 +52,15 @@ const testPropIsArray = (ruleProp) => ).toBe(true) }) +const testKeysAreValidDimensionIds = (ruleProp) => + it('keys should be valid dimension ids', () => { + expect( + onlyRulesWithProp(ruleProp).every((rule) => + allArrayItemsAreValidDimensionIds(Object.keys(rule[ruleProp])) + ) + ).toBe(true) + }) + const testKeysAreValidAxisIds = (ruleProp) => it('keys should be valid axis ids', () => { expect( @@ -127,6 +139,15 @@ describe("verify each rule's ", () => { }) }) + describe('MAX_ITEMS_PER_DIMENSION', () => { + const ruleProp = testResourceAllRuleProps['MAX_ITEMS_PER_DIMENSION'] + + testPropHasKeysAndValues(ruleProp) + testKeysAreValidDimensionIds(ruleProp) + testNoValuesZero(ruleProp) + testNoValuesNegative(ruleProp) + }) + describe('AVAILABLE_AXES', () => { const ruleProp = testResourceAllRuleProps['AVAILABLE_AXES'] diff --git a/src/modules/layoutUiRules/index.js b/src/modules/layoutUiRules/index.js index 3d860c669..ceeb78759 100644 --- a/src/modules/layoutUiRules/index.js +++ b/src/modules/layoutUiRules/index.js @@ -6,10 +6,12 @@ export { getAxisMinNumberOfDimsByVisType as getAxisMinNumberOfDimensions, getAxisPerLockedDimByVisType as getAxisPerLockedDimension, getAllLockedDimIdsByVisType as getAllLockedDimensionIds, + getDimensionMaxNumberOfItemsByVisType as getDimensionMaxNumberOfItems, } from './rulesHelper.js' export { hasAxisTooManyItemsByVisType as hasAxisTooManyItems, + hasDimensionTooManyItemsByVisType as hasDimensionTooManyItems, isDimensionLockedByVisType as isDimensionLocked, isAxisFullByVisType as isAxisFull, canDimensionBeAddedToAxisByVisType as canDimensionBeAddedToAxis, diff --git a/src/modules/layoutUiRules/rules.js b/src/modules/layoutUiRules/rules.js index d7c6fd7dd..2e9f5ff87 100644 --- a/src/modules/layoutUiRules/rules.js +++ b/src/modules/layoutUiRules/rules.js @@ -7,6 +7,7 @@ import { DIMENSION_ID_PERIOD, DIMENSION_ID_DATA, DIMENSION_ID_ORGUNIT, + DIMENSION_ID_ASSIGNED_CATEGORIES, } from '../predefinedDimensions.js' import { VIS_TYPE_COLUMN, @@ -32,6 +33,7 @@ const RULE_PROP_AVAILABLE_AXES = 'availableAxes', RULE_PROP_MAX_DIMS_PER_AXIS = 'maxNumberOfDimsPerAxis', RULE_PROP_MIN_DIMS_PER_AXIS = 'minNumberOfDimsPerAxis', RULE_PROP_MAX_ITEMS_PER_AXIS = 'maxNumberOfItemsPerAxis', + RULE_PROP_MAX_ITEMS_PER_DIMENSION = 'maxNumberOfItemsPerDimension', RULE_PROP_DISALLOWED_DIMS = 'disallowedDims', RULE_PROP_LOCKED_DIMS = 'lockedDims' @@ -135,11 +137,15 @@ const outliersTableRules = { [RULE_PROP_MIN_DIMS_PER_AXIS]: { [AXIS_ID_COLUMNS]: 3, }, + [RULE_PROP_MAX_ITEMS_PER_DIMENSION]: { + [DIMENSION_ID_PERIOD]: 1, + }, [RULE_PROP_LOCKED_DIMS]: { [DIMENSION_ID_DATA]: AXIS_ID_COLUMNS, [DIMENSION_ID_PERIOD]: AXIS_ID_COLUMNS, [DIMENSION_ID_ORGUNIT]: AXIS_ID_COLUMNS, }, + [RULE_PROP_DISALLOWED_DIMS]: [DIMENSION_ID_ASSIGNED_CATEGORIES], } const visTypeToRules = { @@ -183,6 +189,9 @@ export const getMaxNumberOfDimsPerAxisByVisType = (visType) => export const getMinNumberOfDimsPerAxisByVisType = (visType) => getRulesByVisType(visType)[RULE_PROP_MIN_DIMS_PER_AXIS] || {} +export const getMaxNumberOfItemsPerDimensionByVisType = (visType) => + getRulesByVisType(visType)[RULE_PROP_MAX_ITEMS_PER_DIMENSION] || {} + export const getMaxNumberOfItemsPerAxisByVisType = (visType) => getRulesByVisType(visType)[RULE_PROP_MAX_ITEMS_PER_AXIS] || {} @@ -203,6 +212,7 @@ export const testResourceAllRuleProps = { MAX_DIMS_PER_AXIS: RULE_PROP_MAX_DIMS_PER_AXIS, MIN_DIMS_PER_AXIS: RULE_PROP_MIN_DIMS_PER_AXIS, MAX_ITEMS_PER_AXIS: RULE_PROP_MAX_ITEMS_PER_AXIS, + MAX_ITEMS_PER_DIMENSION: RULE_PROP_MAX_ITEMS_PER_DIMENSION, DISALLOWED_DIMS: RULE_PROP_DISALLOWED_DIMS, LOCKED_DIMS: RULE_PROP_LOCKED_DIMS, } diff --git a/src/modules/layoutUiRules/rulesHelper.js b/src/modules/layoutUiRules/rulesHelper.js index d0f85d90e..8d0a86bd9 100644 --- a/src/modules/layoutUiRules/rulesHelper.js +++ b/src/modules/layoutUiRules/rulesHelper.js @@ -1,4 +1,5 @@ import { + getMaxNumberOfItemsPerDimensionByVisType, getMaxNumberOfItemsPerAxisByVisType, getMaxNumberOfDimsPerAxisByVisType, getMinNumberOfDimsPerAxisByVisType, @@ -26,3 +27,6 @@ export const getAxisPerLockedDimByVisType = (visType, dimensionId) => export const getAllLockedDimIdsByVisType = (visType) => Object.keys(getLockedDimsByVisType(visType)) + +export const getDimensionMaxNumberOfItemsByVisType = (visType, dimensionId) => + getMaxNumberOfItemsPerDimensionByVisType(visType)[dimensionId] diff --git a/src/modules/layoutUiRules/rulesUtils.js b/src/modules/layoutUiRules/rulesUtils.js index 4f95f2831..be75b1e64 100644 --- a/src/modules/layoutUiRules/rulesUtils.js +++ b/src/modules/layoutUiRules/rulesUtils.js @@ -2,6 +2,7 @@ import { getLockedDimsByVisType } from './rules.js' import { getAxisMaxNumberOfDimsByVisType, getAxisMaxNumberOfItemsByVisType, + getDimensionMaxNumberOfItemsByVisType, getAllLockedDimIdsByVisType, } from './rulesHelper.js' @@ -20,6 +21,21 @@ export const hasAxisTooManyItemsByVisType = ( : false } +export const hasDimensionTooManyItemsByVisType = ( + visType, + dimensionId, + numberOfItems +) => { + const maxNumberOfItemsPerDimension = getDimensionMaxNumberOfItemsByVisType( + visType, + dimensionId + ) + + return maxNumberOfItemsPerDimension + ? numberOfItems > maxNumberOfItemsPerDimension + : false +} + export const isDimensionLockedByVisType = (visType, dimensionId) => getAllLockedDimIdsByVisType(visType).includes(dimensionId)