diff --git a/src/group-configurations/common/UsageList.jsx b/src/group-configurations/common/UsageList.jsx index b55ad10756..8999e95e80 100644 --- a/src/group-configurations/common/UsageList.jsx +++ b/src/group-configurations/common/UsageList.jsx @@ -1,7 +1,10 @@ import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Hyperlink, Stack, Icon } from '@openedx/paragon'; -import { Warning as WarningIcon, Error as ErrorIcon } from '@openedx/paragon/icons'; +import { + Warning as WarningIcon, + Error as ErrorIcon, +} from '@openedx/paragon/icons'; import { MESSAGE_VALIDATION_TYPES } from '../constants'; import { formatUrlToUnitPage } from '../utils'; @@ -13,9 +16,13 @@ const UsageList = ({ className, itemList, isExperiment }) => { ? messages.experimentAccessTo : messages.accessTo; - const renderValidationMessage = ({ text }) => ( + const renderValidationMessage = ({ text, type }) => ( - + {text} ); diff --git a/src/group-configurations/experiment-configurations-section/ExperimentCard.jsx b/src/group-configurations/experiment-configurations-section/ExperimentCard.jsx index 7e3d0af9a8..0636779bde 100644 --- a/src/group-configurations/experiment-configurations-section/ExperimentCard.jsx +++ b/src/group-configurations/experiment-configurations-section/ExperimentCard.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useParams } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; @@ -26,6 +26,7 @@ import { initialExperimentConfiguration } from './constants'; const ExperimentCard = ({ configuration, experimentConfigurationActions, + isExpandedByDefault, onCreate, }) => { const { formatMessage } = useIntl(); @@ -34,6 +35,10 @@ const ExperimentCard = ({ const [isEditMode, switchOnEditMode, switchOffEditMode] = useToggle(false); const [isOpenDeleteModal, openDeleteModal, closeDeleteModal] = useToggle(false); + useEffect(() => { + setIsExpanded(isExpandedByDefault); + }, [isExpandedByDefault]); + const { id, groups: groupsControl, description, usage, } = configuration; @@ -85,7 +90,11 @@ const ExperimentCard = ({ onEditClick={handleEditConfiguration} /> ) : ( -
+
{formatMessage( diff --git a/src/group-configurations/experiment-configurations-section/constants.js b/src/group-configurations/experiment-configurations-section/constants.js index 3e04eb1f17..70ed39bc88 100644 --- a/src/group-configurations/experiment-configurations-section/constants.js +++ b/src/group-configurations/experiment-configurations-section/constants.js @@ -3,8 +3,12 @@ export const initialExperimentConfiguration = { name: '', description: '', groups: [ - { name: 'Group A', version: 1, usage: [] }, - { name: 'Group B', version: 1, usage: [] }, + { + name: 'Group A', version: 1, usage: [], idx: 0, + }, + { + name: 'Group B', version: 1, usage: [], idx: 1, + }, ], scheme: 'random', parameters: {}, diff --git a/src/group-configurations/experiment-configurations-section/index.jsx b/src/group-configurations/experiment-configurations-section/index.jsx index 18afb29231..a5ed9f6365 100644 --- a/src/group-configurations/experiment-configurations-section/index.jsx +++ b/src/group-configurations/experiment-configurations-section/index.jsx @@ -3,6 +3,7 @@ import { Button, useToggle } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Add as AddIcon } from '@openedx/paragon/icons'; +import { useScrollToHashElement } from '../../hooks'; import EmptyPlaceholder from '../empty-placeholder'; import ExperimentForm from './ExperimentForm'; import ExperimentCard from './ExperimentCard'; @@ -24,6 +25,8 @@ const ExperimentConfigurationsSection = ({ experimentConfigurationActions.handleCreate(configuration, hideNewConfiguration); }; + const { elementWithHash } = useScrollToHashElement({ isLoading: true }); + return (

@@ -36,6 +39,7 @@ const ExperimentConfigurationsSection = ({ key={configuration.id} configuration={configuration} experimentConfigurationActions={experimentConfigurationActions} + isExpandedByDefault={configuration.id === +elementWithHash} onCreate={handleCreateConfiguration} /> ))} diff --git a/src/group-configurations/experiment-configurations-section/messages.js b/src/group-configurations/experiment-configurations-section/messages.js index 9718c8fdbe..01a120fae0 100644 --- a/src/group-configurations/experiment-configurations-section/messages.js +++ b/src/group-configurations/experiment-configurations-section/messages.js @@ -108,7 +108,7 @@ const messages = defineMessages({ }, subtitleModalDelete: { id: 'course-authoring.group-configurations.experiment-card.delete-modal.subtitle', - defaultMessage: 'content group', + defaultMessage: 'group configurations', }, deleteRestriction: { id: 'course-authoring.group-configurations.experiment-card.delete-restriction', diff --git a/src/group-configurations/experiment-configurations-section/utils.js b/src/group-configurations/experiment-configurations-section/utils.js index be6db84ca6..18d070ecf6 100644 --- a/src/group-configurations/experiment-configurations-section/utils.js +++ b/src/group-configurations/experiment-configurations-section/utils.js @@ -12,27 +12,28 @@ const getNextGroupName = (groups, groupFieldName = 'name') => { const existingGroupNames = groups.map((group) => group.name); const lettersCount = ALPHABET_LETTERS.length; - let nextIndex = existingGroupNames.length + 1; + // Calculate the maximum index of existing groups + const maxIdx = groups.reduce((max, group) => Math.max(max, group.idx), -1); - let groupName = ''; - while (nextIndex > 0) { - groupName = ALPHABET_LETTERS[(nextIndex - 1) % lettersCount] + groupName; - nextIndex = Math.floor((nextIndex - 1) / lettersCount); - } + // Calculate the next index for the new group + const nextIndex = maxIdx + 1; + let groupName = ''; let counter = 0; - let newName = groupName; - while (existingGroupNames.includes(`Group ${newName}`)) { - counter++; - let newIndex = existingGroupNames.length + 1 + counter; + + do { + let tempIndex = nextIndex + counter; groupName = ''; - while (newIndex > 0) { - groupName = ALPHABET_LETTERS[(newIndex - 1) % lettersCount] + groupName; - newIndex = Math.floor((newIndex - 1) / lettersCount); + while (tempIndex >= 0) { + groupName = ALPHABET_LETTERS[tempIndex % lettersCount] + groupName; + tempIndex = Math.floor(tempIndex / lettersCount) - 1; } - newName = groupName; - } - return { [groupFieldName]: `Group ${newName}`, version: 1, usage: [] }; + counter++; + } while (existingGroupNames.includes(`Group ${groupName}`)); + + return { + [groupFieldName]: `Group ${groupName}`, version: 1, usage: [], idx: nextIndex, + }; }; /** diff --git a/src/group-configurations/experiment-configurations-section/utils.test.js b/src/group-configurations/experiment-configurations-section/utils.test.js index aa0cc4ac82..4e0e5f9272 100644 --- a/src/group-configurations/experiment-configurations-section/utils.test.js +++ b/src/group-configurations/experiment-configurations-section/utils.test.js @@ -9,110 +9,141 @@ describe('utils module', () => { it('return correct next group name test-case-1', () => { const groups = [ { - name: 'Group A', + name: 'Group A', idx: 0, }, { - name: 'Group B', + name: 'Group B', idx: 1, }, ]; const nextGroup = getNextGroupName(groups); expect(nextGroup.name).toBe('Group C'); + expect(nextGroup.idx).toBe(2); }); it('return correct next group name test-case-2', () => { const groups = []; const nextGroup = getNextGroupName(groups); expect(nextGroup.name).toBe('Group A'); + expect(nextGroup.idx).toBe(0); }); it('return correct next group name test-case-3', () => { const groups = [ { - name: 'Some group', + name: 'Some group', idx: 0, + }, + { + name: 'Group B', idx: 1, }, ]; const nextGroup = getNextGroupName(groups); - expect(nextGroup.name).toBe('Group B'); + expect(nextGroup.name).toBe('Group C'); + expect(nextGroup.idx).toBe(2); }); it('return correct next group name test-case-4', () => { const groups = [ { - name: 'Group A', + name: 'Group A', idx: 0, }, { - name: 'Group A', + name: 'Group A', idx: 1, }, ]; const nextGroup = getNextGroupName(groups); expect(nextGroup.name).toBe('Group C'); + expect(nextGroup.idx).toBe(2); }); it('return correct next group name test-case-5', () => { const groups = [ { - name: 'Group A', + name: 'Group A', idx: 0, }, { - name: 'Group C', + name: 'Group C', idx: 1, }, { - name: 'Group B', + name: 'Group B', idx: 2, }, ]; const nextGroup = getNextGroupName(groups); expect(nextGroup.name).toBe('Group D'); + expect(nextGroup.idx).toBe(3); }); it('return correct next group name test-case-6', () => { const groups = [ { - name: '', + name: '', idx: 0, }, { - name: '', + name: '', idx: 1, }, ]; const nextGroup = getNextGroupName(groups); expect(nextGroup.name).toBe('Group C'); + expect(nextGroup.idx).toBe(2); }); it('return correct next group name test-case-7', () => { const groups = [ { - name: 'Group A', + name: 'Group A', idx: 0, }, { - name: 'Group C', + name: 'Group C', idx: 1, }, ]; const nextGroup = getNextGroupName(groups); expect(nextGroup.name).toBe('Group D'); + expect(nextGroup.idx).toBe(2); }); it('return correct next group name test-case-8', () => { const groups = [ { - name: 'Group D', + name: 'Group D', idx: 0, }, ]; const nextGroup = getNextGroupName(groups); expect(nextGroup.name).toBe('Group B'); + expect(nextGroup.idx).toBe(1); }); it('return correct next group name test-case-9', () => { + const groups = [ + { + name: 'Group E', idx: 4, + }, + ]; + const nextGroup = getNextGroupName(groups); + expect(nextGroup.name).toBe('Group F'); + }); + + it('return correct next group name test-case-10', () => { + const groups = [ + { + name: 'Group E', idx: 0, + }, + ]; + const nextGroup = getNextGroupName(groups); + expect(nextGroup.name).toBe('Group B'); + }); + + it('return correct next group name test-case-11', () => { const simulatedGroupWithAlphabetLength = Array.from( { length: 26 }, - () => ({ name: 'Test name' }), + (_, idx) => ({ name: 'Test name', idx }), ); const nextGroup = getNextGroupName(simulatedGroupWithAlphabetLength); expect(nextGroup.name).toBe('Group AA'); }); - it('return correct next group name test-case-10', () => { + it('return correct next group name test-case-12', () => { const simulatedGroupWithAlphabetLength = Array.from( { length: 702 }, - () => ({ name: 'Test name' }), + (_, idx) => ({ name: 'Test name', idx }), ); const nextGroup = getNextGroupName(simulatedGroupWithAlphabetLength); expect(nextGroup.name).toBe('Group AAA'); diff --git a/src/hooks.js b/src/hooks.js index 8c649ea06b..5967d9ace6 100644 --- a/src/hooks.js +++ b/src/hooks.js @@ -1,20 +1,24 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { history } from '@edx/frontend-platform'; // eslint-disable-next-line import/prefer-default-export export const useScrollToHashElement = ({ isLoading }) => { + const [elementWithHash, setElementWithHash] = useState(null); + useEffect(() => { - const currentHash = window.location.hash; + const currentHash = window.location.hash.substring(1); if (currentHash) { - const element = document.querySelector(currentHash); - + const element = document.getElementById(currentHash); if (element) { element.scrollIntoView(); history.replace({ hash: '' }); } + setElementWithHash(currentHash); } }, [isLoading]); + + return { elementWithHash }; }; export const useEscapeClick = ({ onEscape, dependency }) => {