diff --git a/app/src/components/inputs/inputDropdown/inputDropdown.jsx b/app/src/components/inputs/inputDropdown/inputDropdown.jsx index 01a92171ec..4a2ab0d4e9 100644 --- a/app/src/components/inputs/inputDropdown/inputDropdown.jsx +++ b/app/src/components/inputs/inputDropdown/inputDropdown.jsx @@ -45,6 +45,7 @@ export class InputDropdown extends Component { independentGroupSelection: PropTypes.bool, customClasses: PropTypes.object, title: PropTypes.string, + readOnly: PropTypes.bool, }; static defaultProps = { @@ -70,6 +71,7 @@ export class InputDropdown extends Component { opened: '', }, title: '', + readOnly: false, }; state = { opened: false, @@ -233,48 +235,51 @@ export class InputDropdown extends Component { error, touched, 'mobile-disabled': mobileDisabled, + readonly: this.props.readOnly, })} - onClick={this.onClickSelectBlock} + onClick={this.props.readOnly ? () => {} : this.onClickSelectBlock} > {this.displayedValue()} - + {!this.props.readOnly && } )} - - {({ placement, ref, style, scheduleUpdate }) => { - this.updatePosition = scheduleUpdate; - return ( -
- {multiple && selectAll && ( -
- All -
- )} - - {this.renderOptions()} - -
- ); - }} -
+ {!this.props.readOnly && ( + + {({ placement, ref, style, scheduleUpdate }) => { + this.updatePosition = scheduleUpdate; + return ( +
+ {multiple && selectAll && ( +
+ All +
+ )} + + {this.renderOptions()} + +
+ ); + }} +
+ )} ); diff --git a/app/src/components/inputs/inputDropdown/inputDropdown.scss b/app/src/components/inputs/inputDropdown/inputDropdown.scss index 4f5ecbd10d..8903db1365 100644 --- a/app/src/components/inputs/inputDropdown/inputDropdown.scss +++ b/app/src/components/inputs/inputDropdown/inputDropdown.scss @@ -18,21 +18,25 @@ display: inline-block; width: 100%; } + .dropdown { position: relative; display: inline-block; width: 100%; height: 30px; } + .opened { .arrow { transform: rotate(180deg); } + .select-block { border-color: $COLOR--topaz; box-shadow: 0 0 2px 0 rgba($COLOR--topaz, 0.7); } } + .select-block { display: inline-block; width: 100%; @@ -42,6 +46,7 @@ border: 1px solid $COLOR--gray-80; background-color: $COLOR--white-two; cursor: pointer; + &.dark-view { border: none; border-radius: 4px; @@ -58,20 +63,30 @@ box-shadow: 0 0 2px 0 rgba($COLOR--orange-red, 0.7); } + &.readonly { + cursor: default; + border: none; + padding-left: 0; + } + &.disabled { background-color: $COLOR--gray-91; cursor: default; + & .value.dark-view { color: $COLOR--dirty-gray; } + &.dark-view { background-color: $COLOR--black-1; } + &:hover { border-color: $COLOR--gray-80; } } } + .value { display: inline-block; height: 100%; @@ -83,10 +98,12 @@ text-overflow: ellipsis; white-space: nowrap; color: $COLOR--charcoal-grey; + &.dark-view { color: $COLOR--white-two; } } + .arrow { position: absolute; right: 12px; @@ -101,10 +118,12 @@ box-sizing: border-box; color: $COLOR--charcoal-grey; transition: transform 200ms linear; + &.dark-view { color: $COLOR--white-two; } } + .select-list { position: absolute; top: 100%; @@ -119,16 +138,19 @@ box-shadow: 0 6px 12px $COLOR--gray-80; z-index: $Z-INDEX-POPUP; cursor: pointer; + &.dark-view { background-color: $COLOR--darkmode-gray-400; color: $COLOR--darkmode-gray-100; box-shadow: none; border: 0; } + &.opened { display: block; } } + .select-all-block { width: 100%; height: 37px; @@ -144,6 +166,7 @@ box-sizing: border-box; } } + .select-all { display: inline-block; width: 100%; @@ -159,6 +182,7 @@ background-color: $COLOR--tealish-hover; } } + .select-item { width: 100%; height: 30px; @@ -168,6 +192,7 @@ background-color: $COLOR--tealish-hover; } } + .mobile-disabled { @media (max-width: $SCREEN_XS_MAX) { background-color: $COLOR--gray-91; diff --git a/app/src/pages/inside/common/launchSuiteGrid/launchSuiteGrid.jsx b/app/src/pages/inside/common/launchSuiteGrid/launchSuiteGrid.jsx index 7b1e181987..a8d2ec4a51 100644 --- a/app/src/pages/inside/common/launchSuiteGrid/launchSuiteGrid.jsx +++ b/app/src/pages/inside/common/launchSuiteGrid/launchSuiteGrid.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2019 EPAM Systems + * Copyright 2024 EPAM Systems * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,9 @@ * limitations under the License. */ -import React, { PureComponent, Fragment } from 'react'; -import track from 'react-tracking'; -import { injectIntl } from 'react-intl'; +import React, { useCallback, useMemo } from 'react'; +import { useTracking } from 'react-tracking'; +import { useIntl } from 'react-intl'; import classNames from 'classnames/bind'; import PropTypes from 'prop-types'; import { @@ -50,6 +50,9 @@ import { STATS_TI_TOTAL, } from 'common/constants/statistics'; import { formatAttribute } from 'common/utils/attributeUtils'; +import { useSelector } from 'react-redux'; +import { canBulkEditItems } from 'common/utils/permissions'; +import { userRolesSelector } from 'controllers/pages'; import { Hamburger } from './hamburger'; import { ExecutionStatistics } from './executionStatistics'; import { DefectStatistics } from './defectStatistics'; @@ -66,18 +69,19 @@ HamburgerColumn.propTypes = { className: PropTypes.string.isRequired, }; -const NameColumn = ({ className, ...rest }) => ( +const NameColumn = ({ className, customProps: { hideEdit }, ...rest }) => ( <> - +
- +
); NameColumn.propTypes = { className: PropTypes.string.isRequired, + customProps: PropTypes.object, }; const StartTimeColumn = ({ className, ...rest }) => ( @@ -215,308 +219,264 @@ TiColumn.propTypes = { className: PropTypes.string.isRequired, }; -@injectIntl -@track() -export class LaunchSuiteGrid extends PureComponent { - static propTypes = { - intl: PropTypes.object.isRequired, - data: PropTypes.array, - sortingColumn: PropTypes.string, - sortingDirection: PropTypes.string, - onChangeSorting: PropTypes.func, - onDeleteItem: PropTypes.func, - onMove: PropTypes.func, - onEditItem: PropTypes.func, - onForceFinish: PropTypes.func, - selectedItems: PropTypes.arrayOf(PropTypes.object), - onItemSelect: PropTypes.func, - onItemsSelect: PropTypes.func, - onAllItemsSelect: PropTypes.func, - withHamburger: PropTypes.bool, - loading: PropTypes.bool, - onFilterClick: PropTypes.func, - events: PropTypes.object, - onAnalysis: PropTypes.func, - onPatternAnalysis: PropTypes.func, - rowHighlightingConfig: PropTypes.shape({ - onGridRowHighlighted: PropTypes.func, - isGridRowHighlighted: PropTypes.bool, - highlightedRowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - noItemsBlock: PropTypes.element, - tracking: PropTypes.shape({ - trackEvent: PropTypes.func, - getTrackingData: PropTypes.func, - }).isRequired, - }; - static defaultProps = { - data: [], - sortingColumn: null, - sortingDirection: null, - onChangeSorting: () => {}, - onDeleteItem: () => {}, - onMove: () => {}, - onEditItem: () => {}, - onForceFinish: () => {}, - selectedItems: [], - onItemSelect: () => {}, - onItemsSelect: () => {}, - onAllItemsSelect: () => {}, - withHamburger: false, - loading: false, - onFilterClick: () => {}, - events: {}, - onAnalysis: () => {}, - onPatternAnalysis: () => {}, - rowHighlightingConfig: PropTypes.shape({ - onGridRowHighlighted: () => {}, - isGridRowHighlighted: false, - highlightedRowId: null, - }), - noItemsBlock: null, - }; +export const LaunchSuiteGrid = React.memo( + ({ + data, + sortingColumn, + sortingDirection, + onChangeSorting, + onDeleteItem, + onMove, + onEditItem, + onForceFinish, + selectable, + selectedItems = [], + onItemSelect, + onItemsSelect, + onAllItemsSelect, + withHamburger, + loading, + onFilterClick, + events, + onAnalysis, + onPatternAnalysis, + rowHighlightingConfig, + noItemsBlock, + }) => { + const { formatMessage } = useIntl(); + const { trackEvent } = useTracking(); + const userRoles = useSelector(userRolesSelector); - getColumns() { - const { - events, - withHamburger, - onEditItem, - onDeleteItem, - onMove, - onForceFinish, - onAnalysis, - onPatternAnalysis, - } = this.props; - const hamburgerColumn = { - component: HamburgerColumn, - customProps: { - onDeleteItem, - onMove, - onForceFinish, - onAnalysis, - onPatternAnalysis, - }, - }; - const columns = [ - { - id: ENTITY_NAME, - title: { - full: 'name', - short: 'name', - }, - maxHeight: 170, - component: NameColumn, - sortable: true, - withFilter: true, - filterEventInfo: events.NAME_FILTER, - customProps: { - onEditItem, - onClickAttribute: this.handleAttributeFilterClick, - onOwnerClick: this.handleOwnerFilterClick, - events, - withExtensions: withHamburger, // Use extensions for launch level only - }, - sortingEventInfo: events.NAME_SORTING, - }, - { - id: ENTITY_START_TIME, - title: { - full: 'start time', - short: 'start', - }, - component: StartTimeColumn, - sortable: true, - withFilter: true, - filterEventInfo: events.START_TIME_FILTER, - sortingEventInfo: events.START_TIME_SORTING, - }, - { - id: STATS_TOTAL, - title: { - full: 'total', - short: 'ttl', - }, - component: TotalColumn, - sortable: true, - withFilter: true, - filterEventInfo: events.TOTAL_FILTER, - sortingEventInfo: events.TOTAL_SORTING, + const handleAttributeFilterClick = useCallback( + (attribute) => { + onFilterClick( + [ + { + id: ENTITY_ATTRIBUTE, + value: { + filteringField: ENTITY_ATTRIBUTE, + condition: CONDITION_HAS, + value: formatAttribute(attribute), + }, + }, + ], + true, + ); + + if (events.CLICK_ATTRIBUTES) { + trackEvent(events.CLICK_ATTRIBUTES); + } }, - { - id: STATS_PASSED, - title: { - full: 'passed', - short: 'ps', - }, - component: PassedColumn, - sortable: true, - withFilter: true, - filterEventInfo: events.PASSED_FILTER, - sortingEventInfo: events.PASSED_SORTING, + [onFilterClick, events, trackEvent], + ); + + const handleOwnerFilterClick = useCallback( + (owner) => { + onFilterClick({ + id: ENTITY_USER, + value: { + filteringField: ENTITY_NAME, + condition: CONDITION_IN, + value: owner || '', + }, + }); }, - { - id: STATS_FAILED, - title: { - full: 'failed', - short: 'fl', + [onFilterClick], + ); + + const columns = useMemo(() => { + const hamburgerColumn = { + component: HamburgerColumn, + customProps: { + onDeleteItem, + onMove, + onForceFinish, + onAnalysis, + onPatternAnalysis, }, - component: FailedColumn, - sortable: true, - withFilter: true, - filterEventInfo: events.FAILED_FILTER, - sortingEventInfo: events.FAILED_SORTING, - }, - { - id: STATS_SKIPPED, - title: { - full: 'skipped', - short: 'skp', + }; + + const baseColumns = [ + { + id: ENTITY_NAME, + title: { + full: 'name', + short: 'name', + }, + maxHeight: 170, + component: NameColumn, + sortable: true, + withFilter: true, + filterEventInfo: events.NAME_FILTER, + customProps: { + onEditItem, + onClickAttribute: handleAttributeFilterClick, + onOwnerClick: handleOwnerFilterClick, + events, + withExtensions: withHamburger, + hideEdit: !canBulkEditItems(userRoles), + }, + sortingEventInfo: events.NAME_SORTING, }, - component: SkippedColumn, - sortable: true, - withFilter: true, - filterEventInfo: events.SKIPPED_FILTER, - sortingEventInfo: events.SKIPPED_SORTING, - }, - { - id: STATS_PB_TOTAL, - title: { - full: 'product bug', - short: 'product bug', + { + id: ENTITY_START_TIME, + title: { + full: 'start time', + short: 'start', + }, + component: StartTimeColumn, + sortable: true, + withFilter: true, + filterEventInfo: events.START_TIME_FILTER, + sortingEventInfo: events.START_TIME_SORTING, }, - component: PbColumn, - customProps: { - abbreviation: 'pb', - events, + { + id: STATS_TOTAL, + title: { + full: 'total', + short: 'ttl', + }, + component: TotalColumn, + sortable: true, + withFilter: true, + filterEventInfo: events.TOTAL_FILTER, + sortingEventInfo: events.TOTAL_SORTING, }, - sortable: true, - withFilter: true, - filterEventInfo: events.PB_FILTER, - sortingEventInfo: events.PB_SORTING, - }, - { - id: STATS_AB_TOTAL, - title: { - full: 'auto bug', - short: 'auto bug', + { + id: STATS_PASSED, + title: { + full: 'passed', + short: 'ps', + }, + component: PassedColumn, + sortable: true, + withFilter: true, + filterEventInfo: events.PASSED_FILTER, + sortingEventInfo: events.PASSED_SORTING, }, - component: AbColumn, - customProps: { - abbreviation: 'ab', - events, + { + id: STATS_FAILED, + title: { + full: 'failed', + short: 'fl', + }, + component: FailedColumn, + sortable: true, + withFilter: true, + filterEventInfo: events.FAILED_FILTER, + sortingEventInfo: events.FAILED_SORTING, }, - sortable: true, - withFilter: true, - filterEventInfo: events.AB_FILTER, - sortingEventInfo: events.AB_SORTING, - }, - { - id: STATS_SI_TOTAL, - title: { - full: 'system issue', - short: 'system issue', + { + id: STATS_SKIPPED, + title: { + full: 'skipped', + short: 'skp', + }, + component: SkippedColumn, + sortable: true, + withFilter: true, + filterEventInfo: events.SKIPPED_FILTER, + sortingEventInfo: events.SKIPPED_SORTING, }, - component: SiColumn, - customProps: { - abbreviation: 'si', - events, + { + id: STATS_PB_TOTAL, + title: { + full: 'product bug', + short: 'product bug', + }, + component: PbColumn, + customProps: { + abbreviation: 'pb', + events, + }, + sortable: true, + withFilter: true, + filterEventInfo: events.PB_FILTER, + sortingEventInfo: events.PB_SORTING, }, - sortable: true, - withFilter: true, - filterEventInfo: events.SI_FILTER, - sortingEventInfo: events.SI_SORTING, - }, - { - id: STATS_TI_TOTAL, - title: { - full: 'to investigate', - short: 'to invest', + { + id: STATS_AB_TOTAL, + title: { + full: 'auto bug', + short: 'auto bug', + }, + component: AbColumn, + customProps: { + abbreviation: 'ab', + events, + }, + sortable: true, + withFilter: true, + filterEventInfo: events.AB_FILTER, + sortingEventInfo: events.AB_SORTING, }, - component: TiColumn, - customProps: { - abbreviation: 'ti', - events, + { + id: STATS_SI_TOTAL, + title: { + full: 'system issue', + short: 'system issue', + }, + component: SiColumn, + customProps: { + abbreviation: 'si', + events, + }, + sortable: true, + withFilter: true, + filterEventInfo: events.SI_FILTER, + sortingEventInfo: events.SI_SORTING, }, - sortable: true, - withFilter: true, - filterEventInfo: events.TI_FILTER, - sortingEventInfo: events.TI_SORTING, - }, - ]; - if (withHamburger) { - columns.splice(0, 0, hamburgerColumn); - } - return columns; - } - - handleAttributeFilterClick = (attribute) => { - const { tracking, events, onFilterClick } = this.props; - - onFilterClick( - [ { - id: ENTITY_ATTRIBUTE, - value: { - filteringField: ENTITY_ATTRIBUTE, - condition: CONDITION_HAS, - value: formatAttribute(attribute), + id: STATS_TI_TOTAL, + title: { + full: 'to investigate', + short: 'to invest', + }, + component: TiColumn, + customProps: { + abbreviation: 'ti', + events, }, + sortable: true, + withFilter: true, + filterEventInfo: events.TI_FILTER, + sortingEventInfo: events.TI_SORTING, }, - ], - true, - ); - - events.CLICK_ATTRIBUTES && tracking.trackEvent(events.CLICK_ATTRIBUTES); - }; - - handleOwnerFilterClick = (owner) => - this.props.onFilterClick({ - id: ENTITY_USER, - value: { - filteringField: ENTITY_NAME, - condition: CONDITION_IN, - value: owner || '', - }, - }); - - renderNoItemsBlock = () => { - const { - intl: { formatMessage }, - noItemsBlock, - } = this.props; + ]; - if (noItemsBlock) { - return noItemsBlock; - } + if (withHamburger) { + baseColumns.unshift(hamburgerColumn); + } - return ; - }; + return baseColumns; + }, [ + events, + withHamburger, + onDeleteItem, + onMove, + onForceFinish, + onAnalysis, + onPatternAnalysis, + onEditItem, + ]); - render() { - const { - data, - onChangeSorting, - sortingColumn, - sortingDirection, - selectedItems, - onItemSelect, - onItemsSelect, - onAllItemsSelect, - loading, - onFilterClick, - rowHighlightingConfig, - } = this.props; + const renderNoItemsBlock = useCallback(() => { + if (noItemsBlock) { + return noItemsBlock; + } + return ; + }, [noItemsBlock, formatMessage]); return ( - + <> - {!data.length && !loading && this.renderNoItemsBlock()} - + {!data.length && !loading && renderNoItemsBlock()} + ); - } -} + }, +); + +LaunchSuiteGrid.propTypes = { + data: PropTypes.array, + sortingColumn: PropTypes.string, + sortingDirection: PropTypes.string, + onChangeSorting: PropTypes.func, + onDeleteItem: PropTypes.func, + onMove: PropTypes.func, + onEditItem: PropTypes.func, + onForceFinish: PropTypes.func, + selectable: PropTypes.bool, + selectedItems: PropTypes.arrayOf(PropTypes.object), + onItemSelect: PropTypes.func, + onItemsSelect: PropTypes.func, + onAllItemsSelect: PropTypes.func, + withHamburger: PropTypes.bool, + loading: PropTypes.bool, + onFilterClick: PropTypes.func, + events: PropTypes.object, + onAnalysis: PropTypes.func, + onPatternAnalysis: PropTypes.func, + rowHighlightingConfig: PropTypes.shape({ + onGridRowHighlighted: PropTypes.func, + isGridRowHighlighted: PropTypes.bool, + highlightedRowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }), + noItemsBlock: PropTypes.element, +}; + +LaunchSuiteGrid.defaultProps = { + data: [], + sortingColumn: null, + sortingDirection: null, + onChangeSorting: () => {}, + onDeleteItem: () => {}, + onMove: () => {}, + onEditItem: () => {}, + onForceFinish: () => {}, + selectable: true, + selectedItems: [], + onItemSelect: () => {}, + onItemsSelect: () => {}, + onAllItemsSelect: () => {}, + withHamburger: false, + loading: false, + onFilterClick: () => {}, + events: {}, + onAnalysis: () => {}, + onPatternAnalysis: () => {}, + rowHighlightingConfig: { + onGridRowHighlighted: () => {}, + isGridRowHighlighted: false, + highlightedRowId: null, + }, + noItemsBlock: null, +}; diff --git a/app/src/pages/inside/common/statusDropdown/statusDropdown.jsx b/app/src/pages/inside/common/statusDropdown/statusDropdown.jsx index a8d2df981b..3c6cda1f3b 100644 --- a/app/src/pages/inside/common/statusDropdown/statusDropdown.jsx +++ b/app/src/pages/inside/common/statusDropdown/statusDropdown.jsx @@ -131,10 +131,7 @@ export class StatusDropdown extends Component { return STATUS_TYPES.map((item) => ({ label: withIndicator ? ( - + ) : ( formatStatus(intl.formatMessage, item) @@ -147,12 +144,8 @@ export class StatusDropdown extends Component { const { status, withIndicator, disabled, readOnly } = this.props; return (
- {readOnly ? ( - + {readOnly && withIndicator ? ( + ) : ( )} diff --git a/app/src/pages/inside/common/suiteTestToolbar/actionPanel/testItemActionPanel.jsx b/app/src/pages/inside/common/suiteTestToolbar/actionPanel/testItemActionPanel.jsx index ac148f327d..4605048f22 100644 --- a/app/src/pages/inside/common/suiteTestToolbar/actionPanel/testItemActionPanel.jsx +++ b/app/src/pages/inside/common/suiteTestToolbar/actionPanel/testItemActionPanel.jsx @@ -14,21 +14,20 @@ * limitations under the License. */ -import React, { Component } from 'react'; -import track from 'react-tracking'; +import React from 'react'; +import { useTracking } from 'react-tracking'; import PropTypes from 'prop-types'; import classNames from 'classnames/bind'; -import { connect } from 'react-redux'; -import { injectIntl, FormattedMessage } from 'react-intl'; +import { useDispatch, useSelector } from 'react-redux'; +import { FormattedMessage, useIntl } from 'react-intl'; import { breadcrumbsSelector, levelSelector, restorePathAction } from 'controllers/testItem'; -import { userRolesType } from 'common/constants/projectRoles'; import { userRolesSelector } from 'controllers/pages'; import { availableBtsIntegrationsSelector, isBtsPluginsExistSelector, enabledBtsPluginsSelector, } from 'controllers/plugins'; -import { Breadcrumbs, breadcrumbDescriptorShape } from 'components/main/breadcrumbs'; +import { Breadcrumbs } from 'components/main/breadcrumbs'; import { STEP_PAGE_EVENTS } from 'components/main/analytics/events'; import { GhostButton } from 'components/buttons/ghostButton'; import { GhostMenuButton } from 'components/buttons/ghostMenuButton'; @@ -40,101 +39,55 @@ import { createStepActionDescriptors } from 'pages/inside/common/utils'; import { ParentInfo } from 'pages/inside/common/infoLine/parentInfo'; import { pageEventsMap } from 'components/main/analytics'; import { TO_INVESTIGATE_LOCATOR_PREFIX } from 'common/constants/defectTypes'; +import { canWorkWithTests } from 'common/utils/permissions/permissions'; import styles from './testItemActionPanel.scss'; const cx = classNames.bind(styles); -@connect( - (state) => ({ - breadcrumbs: breadcrumbsSelector(state), - level: levelSelector(state), - btsIntegrations: availableBtsIntegrationsSelector(state), - userRoles: userRolesSelector(state), - isBtsPluginsExist: isBtsPluginsExistSelector(state), - enabledBtsPlugins: enabledBtsPluginsSelector(state), - }), - { - restorePath: restorePathAction, - }, -) -@injectIntl -@track() -export class TestItemActionPanel extends Component { - static propTypes = { - debugMode: PropTypes.bool, - onRefresh: PropTypes.func, - breadcrumbs: PropTypes.arrayOf(breadcrumbDescriptorShape), - userRoles: userRolesType, - restorePath: PropTypes.func, - showBreadcrumbs: PropTypes.bool, - hasErrors: PropTypes.bool, - intl: PropTypes.object.isRequired, - hasValidItems: PropTypes.bool, - level: PropTypes.string, - onProceedValidItems: PropTypes.func, - selectedItems: PropTypes.array, - onEditItems: PropTypes.func, - onEditDefects: PropTypes.func, - onPostIssue: PropTypes.func, - onLinkIssue: PropTypes.func, - onUnlinkIssue: PropTypes.func, - onIgnoreInAA: PropTypes.func, - onIncludeInAA: PropTypes.func, - onDelete: PropTypes.func, - btsIntegrations: PropTypes.array, - deleteDisabled: PropTypes.bool, - tracking: PropTypes.shape({ - trackEvent: PropTypes.func, - getTrackingData: PropTypes.func, - }).isRequired, - isBtsPluginsExist: PropTypes.bool, - enabledBtsPlugins: PropTypes.array, - parentItem: PropTypes.object, +export const TestItemActionPanel = ({ + debugMode, + onRefresh, + showBreadcrumbs, + hasErrors, + hasValidItems, + onProceedValidItems, + selectedItems, + onEditItems, + onEditDefects, + onPostIssue, + onLinkIssue, + onUnlinkIssue, + onIgnoreInAA, + onIncludeInAA, + onDelete, + deleteDisabled, + parentItem, +}) => { + const breadcrumbs = useSelector(breadcrumbsSelector); + const level = useSelector(levelSelector); + const btsIntegrations = useSelector(availableBtsIntegrationsSelector); + const userRoles = useSelector(userRolesSelector); + const isBtsPluginsExist = useSelector(isBtsPluginsExistSelector); + const enabledBtsPlugins = useSelector(enabledBtsPluginsSelector); + const { formatMessage } = useIntl(); + const { trackEvent } = useTracking(); + const dispatch = useDispatch(); + + const hasAccessToActions = canWorkWithTests(userRoles); + const onClickRefresh = () => { + trackEvent(pageEventsMap[level].CLICK_REFRESH_BTN); + onRefresh(); }; - static defaultProps = { - debugMode: false, - onRefresh: () => {}, - breadcrumbs: [], - userRoles: {}, - restorePath: () => {}, - level: '', - showBreadcrumbs: true, - hasErrors: false, - hasValidItems: false, - onProceedValidItems: () => {}, - selectedItems: [], - onEditItems: () => {}, - onEditDefects: () => {}, - onPostIssue: () => {}, - onLinkIssue: () => {}, - onUnlinkIssue: () => {}, - onIgnoreInAA: () => {}, - onIncludeInAA: () => {}, - onDelete: () => {}, - btsIntegrations: [], - deleteDisabled: false, - isBtsPluginsExist: false, - enabledBtsPlugins: [], - parentItem: null, - }; - - onClickRefresh = () => { - this.props.tracking.trackEvent(pageEventsMap[this.props.level].CLICK_REFRESH_BTN); - this.props.onRefresh(); - }; - - onEditDefects = () => { - const { tracking, selectedItems, onEditDefects, hasErrors } = this.props; - + const handleEditDefects = () => { if (!hasErrors) { const defectFromTIGroup = selectedItems.length > 1 && selectedItems.some(({ issue }) => issue) ? undefined : selectedItems[0].issue?.issueType.startsWith(TO_INVESTIGATE_LOCATOR_PREFIX); - tracking.trackEvent( - pageEventsMap[this.props.level].MAKE_DECISION_MODAL_EVENTS.getOpenModalEvent( + trackEvent( + pageEventsMap[level].MAKE_DECISION_MODAL_EVENTS.getOpenModalEvent( defectFromTIGroup, 'actions', ), @@ -144,34 +97,17 @@ export class TestItemActionPanel extends Component { onEditDefects(selectedItems); }; - onPostIssue = () => { - const { tracking, onPostIssue } = this.props; - tracking.trackEvent(STEP_PAGE_EVENTS.POST_ISSUE_ACTION); + const handlePostIssue = () => { + trackEvent(STEP_PAGE_EVENTS.POST_ISSUE_ACTION); onPostIssue(); }; - onLinkIssue = () => { - const { tracking, onLinkIssue } = this.props; - tracking.trackEvent(STEP_PAGE_EVENTS.LINK_ISSUE_ACTION); + const handleLinkIssue = () => { + trackEvent(STEP_PAGE_EVENTS.LINK_ISSUE_ACTION); onLinkIssue(); }; - getStepActionDescriptors = () => { - const { - intl: { formatMessage }, - debugMode, - onEditItems, - onUnlinkIssue, - onIgnoreInAA, - onIncludeInAA, - onDelete, - btsIntegrations, - isBtsPluginsExist, - enabledBtsPlugins, - userRoles, - selectedItems, - } = this.props; - + const getStepActionDescriptors = () => { return createStepActionDescriptors({ formatMessage, debugMode, @@ -185,100 +121,117 @@ export class TestItemActionPanel extends Component { enabledBtsPlugins, userRoles, selectedItems, - onEditDefects: this.onEditDefects, - onPostIssue: this.onPostIssue, - onLinkIssue: this.onLinkIssue, + onEditDefects: handleEditDefects, + onPostIssue: handlePostIssue, + onLinkIssue: handleLinkIssue, }); }; - createSuiteActionDescriptors = () => { - const { intl, deleteDisabled, onDelete, onEditItems, userRoles } = this.props; - - return [ - { - label: intl.formatMessage(COMMON_LOCALE_KEYS.EDIT_ITEMS), - value: 'action-edit', - hidden: !canBulkEditItems(userRoles), - onClick: onEditItems, - }, - { - label: intl.formatMessage(COMMON_LOCALE_KEYS.DELETE), - value: 'action-delete', - hidden: deleteDisabled, - onClick: onDelete, - }, - ]; + const createSuiteActionDescriptors = () => [ + { + label: formatMessage(COMMON_LOCALE_KEYS.EDIT_ITEMS), + value: 'action-edit', + hidden: !canBulkEditItems(userRoles), + onClick: onEditItems, + }, + { + label: formatMessage(COMMON_LOCALE_KEYS.DELETE), + value: 'action-delete', + hidden: deleteDisabled, + onClick: onDelete, + }, + ]; + + const onClickActionsButton = () => { + trackEvent(pageEventsMap[level].CLICK_ACTIONS_BTN); }; - checkVisibility = (levels) => levels.some((level) => this.props.level === level); - - onClickActionsButton = () => { - const { tracking, level } = this.props; - tracking.trackEvent(pageEventsMap[level].CLICK_ACTIONS_BTN); - }; - - render() { - const { - breadcrumbs, - restorePath, - showBreadcrumbs, - hasErrors, - intl, - hasValidItems, - onProceedValidItems, - selectedItems, - level, - parentItem, - } = this.props; - const actionDescriptors = - level === LEVEL_STEP ? this.getStepActionDescriptors() : this.createSuiteActionDescriptors(); - - return ( -
- {showBreadcrumbs && ( - - )} - {hasErrors && ( - - {intl.formatMessage(COMMON_LOCALE_KEYS.PROCEED_VALID_ITEMS)} - - )} -
- {parentItem && } + const actionDescriptors = + level === LEVEL_STEP ? getStepActionDescriptors() : createSuiteActionDescriptors(); + + return ( +
+ {showBreadcrumbs && ( + dispatch(restorePathAction())} + /> + )} + {hasErrors && ( + + {formatMessage(COMMON_LOCALE_KEYS.PROCEED_VALID_ITEMS)} + + )} +
+ {parentItem && } + {hasAccessToActions && (
-
- - - -
+ )} +
+ + +
- ); - } -} +
+ ); +}; + +TestItemActionPanel.propTypes = { + debugMode: PropTypes.bool, + onRefresh: PropTypes.func, + showBreadcrumbs: PropTypes.bool, + hasErrors: PropTypes.bool, + hasValidItems: PropTypes.bool, + onProceedValidItems: PropTypes.func, + selectedItems: PropTypes.array, + onEditItems: PropTypes.func, + onEditDefects: PropTypes.func, + onPostIssue: PropTypes.func, + onLinkIssue: PropTypes.func, + onUnlinkIssue: PropTypes.func, + onIgnoreInAA: PropTypes.func, + onIncludeInAA: PropTypes.func, + onDelete: PropTypes.func, + deleteDisabled: PropTypes.bool, + parentItem: PropTypes.object, +}; + +TestItemActionPanel.defaultProps = { + debugMode: false, + onRefresh: () => {}, + showBreadcrumbs: true, + hasErrors: false, + hasValidItems: false, + onProceedValidItems: () => {}, + selectedItems: [], + onEditItems: () => {}, + onEditDefects: () => {}, + onPostIssue: () => {}, + onLinkIssue: () => {}, + onUnlinkIssue: () => {}, + onIgnoreInAA: () => {}, + onIncludeInAA: () => {}, + onDelete: () => {}, + deleteDisabled: false, + parentItem: null, +}; diff --git a/app/src/pages/inside/stepPage/stepGrid/defectType/defectType.jsx b/app/src/pages/inside/stepPage/stepGrid/defectType/defectType.jsx index 63910ea33a..ffb0adb202 100644 --- a/app/src/pages/inside/stepPage/stepGrid/defectType/defectType.jsx +++ b/app/src/pages/inside/stepPage/stepGrid/defectType/defectType.jsx @@ -25,6 +25,9 @@ import PencilIcon from 'common/img/pencil-icon-inline.svg'; import { DefectTypeItem } from 'pages/inside/common/defectTypeItem'; import { PatternAnalyzedLabel } from 'pages/inside/common/patternAnalyzedLabel'; import { TO_INVESTIGATE_LOCATOR_PREFIX } from 'common/constants/defectTypes'; +import { canManageBTSIssues, canWorkWithDefectTypes } from 'common/utils/permissions/permissions'; +import { useSelector } from 'react-redux'; +import { userRolesSelector } from 'controllers/pages'; import { AutoAnalyzedLabel } from './autoAnalyzedLabel'; import { IssueList } from './issueList'; import styles from './defectType.scss'; @@ -72,6 +75,9 @@ PALabel.propTypes = { export const DefectType = ({ issue, onEdit, onRemove, patternTemplates, events, disabled }) => { const { trackEvent } = useTracking(); + const userRoles = useSelector(userRolesSelector); + const canUnlinkIssue = canManageBTSIssues(userRoles); + const canChangeDefectTypes = canWorkWithDefectTypes(userRoles); const eventData = issue.issueType.startsWith(TO_INVESTIGATE_LOCATOR_PREFIX); const onClickEdit = (event) => { event && trackEvent(event); @@ -92,18 +98,29 @@ export const DefectType = ({ issue, onEdit, onRemove, patternTemplates, events, {issue.issueType && ( onClickEdit(events.onEditEvent?.(eventData))} + onClick={ + disabled || !canChangeDefectTypes + ? null + : () => onClickEdit(events.onEditEvent?.(eventData)) + } /> )} -
onClickEdit(events.onEditEvent?.(eventData, 'edit'))} - > - {Parser(PencilIcon)} -
+ {canChangeDefectTypes && ( +
onClickEdit(events.onEditEvent?.(eventData, 'edit'))} + > + {Parser(PencilIcon)} +
+ )}
- +
diff --git a/app/src/pages/inside/stepPage/stepGrid/itemInfoWithRetries/itemInfoWithRetries.jsx b/app/src/pages/inside/stepPage/stepGrid/itemInfoWithRetries/itemInfoWithRetries.jsx index 6387c100b2..c4ef6e5c9a 100644 --- a/app/src/pages/inside/stepPage/stepGrid/itemInfoWithRetries/itemInfoWithRetries.jsx +++ b/app/src/pages/inside/stepPage/stepGrid/itemInfoWithRetries/itemInfoWithRetries.jsx @@ -36,10 +36,12 @@ export class ItemInfoWithRetries extends Component { trackEvent: PropTypes.func, getTrackingData: PropTypes.func, }).isRequired, + hideEdit: PropTypes.bool, }; static defaultProps = { expanded: false, + hideEdit: false, }; constructor(props) { diff --git a/app/src/pages/inside/stepPage/stepGrid/stepGrid.jsx b/app/src/pages/inside/stepPage/stepGrid/stepGrid.jsx index 01fc638a39..9b64fa4859 100644 --- a/app/src/pages/inside/stepPage/stepGrid/stepGrid.jsx +++ b/app/src/pages/inside/stepPage/stepGrid/stepGrid.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2019 EPAM Systems + * Copyright 2024 EPAM Systems * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ * limitations under the License. */ -import React, { Component, Fragment } from 'react'; -import track from 'react-tracking'; +import React, { useMemo } from 'react'; +import { useTracking } from 'react-tracking'; import PropTypes from 'prop-types'; -import { injectIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; import classNames from 'classnames/bind'; import { Grid } from 'components/main/grid'; import { AbsRelTime } from 'components/main/absRelTime'; @@ -36,10 +36,13 @@ import { import { NoItemMessage } from 'components/main/noItemMessage'; import { formatAttribute } from 'common/utils/attributeUtils'; import { StatusDropdown } from 'pages/inside/common/statusDropdown/statusDropdown'; -import { canWorkWithDefectTypes, canWorkWithTests } from 'common/utils/permissions/permissions'; +import { + canBulkEditItems, + canWorkWithDefectTypes, + canWorkWithTests, +} from 'common/utils/permissions/permissions'; import { userRolesSelector } from 'controllers/pages'; -import { userRolesType } from 'common/constants/projectRoles'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import { PredefinedFilterSwitcher } from './predefinedFilterSwitcher'; import { DefectType } from './defectType'; import { GroupHeader } from './groupHeader'; @@ -68,6 +71,7 @@ MethodTypeColumn.defaultProps = { const NameColumn = ({ className, customProps, ...rest }) => (
{ +const StatusColumn = ({ className, value, customProps: { onChange, fetchFunc, readOnly } }) => { const { id, status, attributes, description } = value; return (
@@ -96,7 +100,7 @@ const StatusColumn = ({ className, value, customProps: { onChange, fetchFunc, di description={description} onChange={onChange} fetchFunc={fetchFunc} - disabled={disabled} + readOnly={readOnly} />
); @@ -108,7 +112,7 @@ StatusColumn.propTypes = { formatMessage: PropTypes.func.isRequired, onChange: PropTypes.func, fetchFunc: PropTypes.func, - disabled: PropTypes.bool, + readOnly: PropTypes.bool, }).isRequired, }; StatusColumn.defaultProps = { @@ -175,82 +179,57 @@ PredefinedFilterSwitcherCell.defaultProps = { className: null, }; -@connect((state) => ({ - userRoles: userRolesSelector(state), -})) -@injectIntl -@track() -export class StepGrid extends Component { - static propTypes = { - data: PropTypes.array, - intl: PropTypes.object.isRequired, - selectedItems: PropTypes.array, - onItemSelect: PropTypes.func, - onItemsSelect: PropTypes.func, - onAllItemsSelect: PropTypes.func, - loading: PropTypes.bool, - listView: PropTypes.bool, - onFilterClick: PropTypes.func, - onEditDefect: PropTypes.func, - onUnlinkSingleTicket: PropTypes.func, - events: PropTypes.object, - tracking: PropTypes.shape({ - trackEvent: PropTypes.func, - getTrackingData: PropTypes.func, - }).isRequired, - onEditItem: PropTypes.func, - onChangeSorting: PropTypes.func, - sortingColumn: PropTypes.string, - sortingDirection: PropTypes.string, - rowHighlightingConfig: PropTypes.shape({ - onGridRowHighlighted: PropTypes.func, - isGridRowHighlighted: PropTypes.bool, - highlightedRowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - onStatusUpdate: PropTypes.func.isRequired, - modifyColumnsFunc: PropTypes.func, - userRoles: userRolesType, - }; +export const StepGrid = ({ + data, + selectedItems, + onItemSelect, + onItemsSelect, + onAllItemsSelect, + loading, + listView, + onFilterClick, + onEditDefect, + onUnlinkSingleTicket, + events, + onEditItem, + onChangeSorting, + sortingColumn, + sortingDirection, + rowHighlightingConfig, + onStatusUpdate, + modifyColumnsFunc, +}) => { + const { trackEvent } = useTracking(); + const { formatMessage } = useIntl(); + const userRoles = useSelector(userRolesSelector); + const canManageTests = canWorkWithTests(userRoles); - static defaultProps = { - data: [], - selectedItems: [], - onItemSelect: () => {}, - onItemsSelect: () => {}, - onAllItemsSelect: () => {}, - loading: false, - listView: false, - onFilterClick: () => {}, - onEditDefect: () => {}, - onUnlinkSingleTicket: () => {}, - onEditItem: () => {}, - events: {}, - onChangeSorting: () => {}, - sortingColumn: null, - sortingDirection: null, - rowHighlightingConfig: PropTypes.shape({ - onGridRowHighlighted: () => {}, - isGridRowHighlighted: false, - highlightedRowId: null, - }), - modifyColumnsFunc: null, - userRoles: {}, + const handleAttributeFilterClick = (attribute) => { + onFilterClick( + [ + { + id: ENTITY_ATTRIBUTE, + value: { + filteringField: ENTITY_ATTRIBUTE, + condition: CONDITION_HAS, + value: formatAttribute(attribute), + }, + }, + ], + true, + ); + + if (events.CLICK_ATTRIBUTES) { + trackEvent(events.CLICK_ATTRIBUTES); + } }; - constructor(props) { - super(props); - const { - intl: { formatMessage }, - tracking, - events, - onUnlinkSingleTicket, - onEditItem, - onEditDefect, - onStatusUpdate, - modifyColumnsFunc, - userRoles, - } = props; - this.columns = [ + const highlightFailedItems = (value) => ({ + failed: value.status === FAILED, + }); + + const columns = useMemo(() => { + const baseColumns = [ { id: 'predefinedFilterSwitcher', title: { @@ -260,30 +239,25 @@ export class StepGrid extends Component { }, { id: ENTITY_METHOD_TYPE, - title: { - full: 'method type', - }, + title: { full: 'method type' }, sortable: true, component: MethodTypeColumn, - customProps: { - formatMessage, - }, + customProps: { formatMessage }, withFilter: true, filterEventInfo: events.METHOD_TYPE_FILTER, sortingEventInfo: events.METHOD_TYPE_SORTING, }, { id: 'name', - title: { - full: 'name', - }, + title: { full: 'name' }, sortable: true, component: NameColumn, maxHeight: 170, customProps: { onEditItem, - onClickAttribute: this.handleAttributeFilterClick, + onClickAttribute: handleAttributeFilterClick, events, + hideEdit: !canBulkEditItems(userRoles), }, withFilter: true, filterEventInfo: events.NAME_FILTER, @@ -291,16 +265,14 @@ export class StepGrid extends Component { }, { id: ENTITY_STATUS, - title: { - full: 'status', - }, + title: { full: 'status' }, sortable: true, component: StatusColumn, customProps: { formatMessage, - onChange: (status) => tracking.trackEvent(events.getChangeItemStatusEvent(status)), + onChange: (status) => trackEvent(events.getChangeItemStatusEvent(status)), fetchFunc: onStatusUpdate, - disabled: !canWorkWithTests(userRoles), + readOnly: !canWorkWithTests(userRoles), }, withFilter: true, filterEventInfo: events.STATUS_FILTER, @@ -308,9 +280,7 @@ export class StepGrid extends Component { }, { id: ENTITY_START_TIME, - title: { - full: 'start time', - }, + title: { full: 'start time' }, sortable: true, component: StartTimeColumn, withFilter: true, @@ -319,15 +289,11 @@ export class StepGrid extends Component { }, { id: ENTITY_DEFECT_TYPE, - title: { - full: 'defect type', - }, + title: { full: 'defect type' }, sortable: false, component: DefectTypeColumn, customProps: { - onEdit: (data) => { - onEditDefect(data); - }, + onEdit: onEditDefect, onUnlinkSingleTicket, events: { onEditEvent: events.MAKE_DECISION_MODAL_EVENTS?.getOpenModalEvent, @@ -339,77 +305,94 @@ export class StepGrid extends Component { filterEventInfo: events.DEFECT_TYPE_FILTER, }, ]; - if (modifyColumnsFunc) { - this.columns = modifyColumnsFunc(this.columns); - } - } - - handleAttributeFilterClick = (attribute) => { - const { tracking, events, onFilterClick } = this.props; - - onFilterClick( - [ - { - id: ENTITY_ATTRIBUTE, - value: { - filteringField: ENTITY_ATTRIBUTE, - condition: CONDITION_HAS, - value: formatAttribute(attribute), - }, - }, - ], - true, - ); - events.CLICK_ATTRIBUTES && tracking.trackEvent(events.CLICK_ATTRIBUTES); - }; + return modifyColumnsFunc ? modifyColumnsFunc(baseColumns) : baseColumns; + }, [ + formatMessage, + events, + onEditItem, + onEditDefect, + onUnlinkSingleTicket, + onStatusUpdate, + modifyColumnsFunc, + userRoles, + trackEvent, + ]); - highlightFailedItems = (value) => ({ - [cx('failed')]: value.status === FAILED, - }); + return ( + <> + + {!data.length && !loading && ( + + )} + + ); +}; - render() { - const { - intl: { formatMessage }, - data, - onItemSelect, - onItemsSelect, - onAllItemsSelect, - selectedItems, - loading, - listView, - onFilterClick, - onChangeSorting, - sortingColumn, - sortingDirection, - rowHighlightingConfig, - } = this.props; +StepGrid.propTypes = { + data: PropTypes.array, + intl: PropTypes.object.isRequired, + selectedItems: PropTypes.array, + onItemSelect: PropTypes.func, + onItemsSelect: PropTypes.func, + onAllItemsSelect: PropTypes.func, + loading: PropTypes.bool, + listView: PropTypes.bool, + onFilterClick: PropTypes.func, + onEditDefect: PropTypes.func, + onUnlinkSingleTicket: PropTypes.func, + events: PropTypes.object, + onEditItem: PropTypes.func, + onChangeSorting: PropTypes.func, + sortingColumn: PropTypes.string, + sortingDirection: PropTypes.string, + rowHighlightingConfig: PropTypes.shape({ + onGridRowHighlighted: PropTypes.func, + isGridRowHighlighted: PropTypes.bool, + highlightedRowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }), + onStatusUpdate: PropTypes.func.isRequired, + modifyColumnsFunc: PropTypes.func, +}; - return ( - - - {!data.length && !loading && ( - - )} - - ); - } -} +StepGrid.defaultProps = { + data: [], + selectedItems: [], + onItemSelect: () => {}, + onItemsSelect: () => {}, + onAllItemsSelect: () => {}, + loading: false, + listView: false, + onFilterClick: () => {}, + onEditDefect: () => {}, + onUnlinkSingleTicket: () => {}, + onEditItem: () => {}, + events: {}, + onChangeSorting: () => {}, + sortingColumn: null, + sortingDirection: null, + rowHighlightingConfig: { + onGridRowHighlighted: () => {}, + isGridRowHighlighted: false, + highlightedRowId: null, + }, + modifyColumnsFunc: null, +}; diff --git a/app/src/pages/inside/suitesPage/suitesPage.jsx b/app/src/pages/inside/suitesPage/suitesPage.jsx index 0cb203334a..565abaacc6 100644 --- a/app/src/pages/inside/suitesPage/suitesPage.jsx +++ b/app/src/pages/inside/suitesPage/suitesPage.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2019 EPAM Systems + * Copyright 2024 EPAM Systems * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ * limitations under the License. */ -import { Component } from 'react'; -import track from 'react-tracking'; +import { useEffect, useState } from 'react'; +import { useTracking } from 'react-tracking'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { PageLayout, PageSection } from 'layouts/pageLayout'; import { PaginationToolbar } from 'components/main/paginationToolbar'; import { SORTING_ASC, withSortingURL } from 'controllers/sorting'; @@ -36,254 +36,214 @@ import { } from 'controllers/suite'; import { SUITE_PAGE, SUITES_PAGE_EVENTS } from 'components/main/analytics/events'; import { SuiteTestToolbar } from 'pages/inside/common/suiteTestToolbar'; -import { userIdSelector } from 'controllers/user'; import { namespaceSelector, fetchTestItemsAction, parentItemSelector, loadingSelector, } from 'controllers/testItem'; -import { prevTestItemSelector } from 'controllers/pages'; +import { prevTestItemSelector, userRolesSelector } from 'controllers/pages'; import { ENTITY_START_TIME } from 'components/filterEntities/constants'; +import { canWorkWithTests } from 'common/utils/permissions/permissions'; -@connect( - (state) => ({ - debugMode: debugModeSelector(state), - userId: userIdSelector(state), - suites: suitesSelector(state), - selectedSuites: selectedSuitesSelector(state), - parentItem: parentItemSelector(state), - loading: loadingSelector(state), - validationErrors: validationErrorsSelector(state), - highlightItemId: prevTestItemSelector(state), - }), - { - toggleSuiteSelectionAction, - unselectAllSuitesAction, - toggleAllSuitesAction, - selectSuitesAction, - fetchTestItemsAction, - }, -) -@withSortingURL({ - defaultFields: [ENTITY_START_TIME], - defaultDirection: SORTING_ASC, - namespaceSelector, -}) -@withPagination({ - paginationSelector: suitePaginationSelector, - namespaceSelector, -}) -@track({ page: SUITE_PAGE }) -export class SuitesPage extends Component { - static propTypes = { - debugMode: PropTypes.bool.isRequired, - deleteItems: PropTypes.func, - onEditItem: PropTypes.func, - onEditItems: PropTypes.func, - suites: PropTypes.arrayOf(PropTypes.object), - selectedSuites: PropTypes.arrayOf(PropTypes.object), - activePage: PropTypes.number, - itemCount: PropTypes.number, - pageCount: PropTypes.number, - pageSize: PropTypes.number, - sortingColumn: PropTypes.string, - sortingDirection: PropTypes.string, - fetchTestItemsAction: PropTypes.func, - onChangePage: PropTypes.func, - onChangePageSize: PropTypes.func, - onChangeSorting: PropTypes.func, - toggleSuiteSelectionAction: PropTypes.func, - unselectAllSuitesAction: PropTypes.func, - toggleAllSuitesAction: PropTypes.func, - selectSuitesAction: PropTypes.func, - parentItem: PropTypes.object, - loading: PropTypes.bool, - validationErrors: PropTypes.object, - onFilterAdd: PropTypes.func, - onFilterRemove: PropTypes.func, - onFilterValidate: PropTypes.func, - onFilterChange: PropTypes.func, - filterErrors: PropTypes.object, - filterEntities: PropTypes.array, - tracking: PropTypes.shape({ - trackEvent: PropTypes.func, - getTrackingData: PropTypes.func, - }).isRequired, - highlightItemId: PropTypes.number, - }; +// TODO: Refactor to avoid duplication +export const SuitesPageWrapped = ({ + deleteItems, + onEditItem, + onEditItems, + activePage, + itemCount, + pageCount, + pageSize, + sortingColumn, + sortingDirection, + onChangePage, + onChangePageSize, + onChangeSorting, + onFilterAdd, + onFilterRemove, + onFilterValidate, + onFilterChange, + filterErrors, + filterEntities, +}) => { + const { trackEvent } = useTracking({ page: SUITE_PAGE }); + + const [highlightedRowId, setHighlightedRowId] = useState(null); + const [isGridRowHighlighted, setIsGridRowHighlighted] = useState(false); + const debugMode = useSelector(debugModeSelector); + const suites = useSelector(suitesSelector); + const selectedSuites = useSelector(selectedSuitesSelector); + const parentItem = useSelector(parentItemSelector); + const loading = useSelector(loadingSelector); + const validationErrors = useSelector(validationErrorsSelector); + const highlightItemId = useSelector(prevTestItemSelector); + const userRoles = useSelector(userRolesSelector); + const canManageTests = canWorkWithTests(userRoles); + const dispatch = useDispatch(); - static defaultProps = { - deleteItems: () => {}, - onEditItem: () => {}, - onEditItems: () => {}, - suites: [], - selectedSuites: [], - activePage: DEFAULT_PAGINATION[PAGE_KEY], - itemCount: null, - pageCount: null, - pageSize: DEFAULT_PAGINATION[SIZE_KEY], - sortingColumn: null, - sortingDirection: null, - fetchTestItemsAction: () => {}, - onChangePage: () => {}, - onChangePageSize: () => {}, - onChangeSorting: () => {}, - toggleSuiteSelectionAction: () => {}, - unselectAllSuitesAction: () => {}, - toggleAllSuitesAction: () => {}, - selectSuitesAction: () => {}, - parentItem: null, - loading: false, - validationErrors: {}, - onFilterAdd: () => {}, - onFilterRemove: () => {}, - onFilterValidate: () => {}, - onFilterChange: () => {}, - filterErrors: {}, - filterEntities: [], - highlightItemId: null, + // TODO - extract highlighting into custom hook + const onHighlightRow = (rowId) => { + setHighlightedRowId(rowId); + setIsGridRowHighlighted(true); }; - state = { - highlightedRowId: null, - isGridRowHighlighted: false, - isSauceLabsIntegrationView: false, + const onGridRowHighlighted = () => { + setIsGridRowHighlighted(false); }; - componentDidMount() { - const { highlightItemId } = this.props; - if (highlightItemId) { - this.onHighlightRow(highlightItemId); + const handleAllSuitesSelection = () => { + if (suites.length !== selectedSuites.length) { + trackEvent(SUITES_PAGE_EVENTS.CLICK_SELECT_ALL_ITEMS); } - } + dispatch(toggleAllSuitesAction(suites)); + }; - componentWillUnmount() { - if (this.props.selectedSuites.length > 0) { - this.props.unselectAllSuitesAction(); + const handleOneItemSelection = (value) => { + if (!selectedSuites.includes(value)) { + trackEvent(SUITES_PAGE_EVENTS.CLICK_SELECT_ONE_ITEM); } - } - - onHighlightRow = (highlightedRowId) => { - this.setState({ - highlightedRowId, - isGridRowHighlighted: true, - }); + dispatch(toggleSuiteSelectionAction(value)); }; - onGridRowHighlighted = () => { - this.setState({ - isGridRowHighlighted: false, - }); + const handleItemsSelection = (items) => { + dispatch(selectSuitesAction(items)); }; - - handleAllSuitesSelection = () => { - if (this.props.suites.length !== this.props.selectedSuites.length) { - this.props.tracking.trackEvent(SUITES_PAGE_EVENTS.CLICK_SELECT_ALL_ITEMS); - } - this.props.toggleAllSuitesAction(this.props.suites); + const handleRefresh = () => { + dispatch(fetchTestItemsAction()); }; - - handleOneItemSelection = (value) => { - if (!this.props.selectedSuites.includes(value)) { - this.props.tracking.trackEvent(SUITES_PAGE_EVENTS.CLICK_SELECT_ONE_ITEM); - } - this.props.toggleSuiteSelectionAction(value); + const unselectAllItems = () => { + trackEvent(SUITES_PAGE_EVENTS.CLOSE_ICON_FOR_ALL_SELECTIONS); + dispatch(unselectAllSuitesAction()); }; - unselectAllItems = () => { - this.props.tracking.trackEvent(SUITES_PAGE_EVENTS.CLOSE_ICON_FOR_ALL_SELECTIONS); - this.props.unselectAllSuitesAction(); + const unselectItem = (item) => { + trackEvent(SUITES_PAGE_EVENTS.CLOSE_ICON_SELECTED_ITEM); + dispatch(toggleSuiteSelectionAction(item)); }; - unselectItem = (item) => { - this.props.tracking.trackEvent(SUITES_PAGE_EVENTS.CLOSE_ICON_SELECTED_ITEM); - this.props.toggleSuiteSelectionAction(item); + const rowHighlightingConfig = { + onGridRowHighlighted, + isGridRowHighlighted, + highlightedRowId, }; - render() { - const { - suites, - activePage, - itemCount, - pageCount, - pageSize, - onChangePage, - onChangePageSize, - sortingColumn, - sortingDirection, - onChangeSorting, - selectedSuites, - parentItem, - loading, - debugMode, - deleteItems, - onEditItem, - onEditItems, - onFilterAdd, - onFilterRemove, - onFilterValidate, - onFilterChange, - filterErrors, - filterEntities, - tracking, - } = this.props; + useEffect(() => { + if (highlightItemId) { + onHighlightRow(highlightItemId); + } + }, [highlightItemId]); - const rowHighlightingConfig = { - onGridRowHighlighted: this.onGridRowHighlighted, - isGridRowHighlighted: this.state.isGridRowHighlighted, - highlightedRowId: this.state.highlightedRowId, + useEffect(() => { + return () => { + if (selectedSuites.length > 0) { + dispatch(unselectAllSuitesAction()); + } }; + }, [selectedSuites, dispatch]); - return ( - - - deleteItems(selectedSuites)} - onEditItems={() => onEditItems(selectedSuites)} - errors={this.props.validationErrors} - selectedItems={selectedSuites} - onUnselect={this.unselectItem} - onUnselectAll={this.unselectAllItems} - onProceedValidItems={() => tracking.trackEvent(SUITES_PAGE_EVENTS.PROCEED_VALID_ITEMS)} - parentItem={parentItem} - onRefresh={this.props.fetchTestItemsAction} - debugMode={debugMode} - events={SUITES_PAGE_EVENTS} - filterErrors={filterErrors} - onFilterChange={onFilterChange} - onFilterValidate={onFilterValidate} - onFilterRemove={onFilterRemove} - onFilterAdd={onFilterAdd} - filterEntities={filterEntities} - /> - + + deleteItems(selectedSuites)} + onEditItems={() => onEditItems(selectedSuites)} + errors={validationErrors} + selectedItems={selectedSuites} + onUnselect={unselectItem} + onUnselectAll={unselectAllItems} + onProceedValidItems={() => trackEvent(SUITES_PAGE_EVENTS.PROCEED_VALID_ITEMS)} + parentItem={parentItem} + onRefresh={handleRefresh} + debugMode={debugMode} + events={SUITES_PAGE_EVENTS} + filterErrors={filterErrors} + onFilterChange={onFilterChange} + onFilterValidate={onFilterValidate} + onFilterRemove={onFilterRemove} + onFilterAdd={onFilterAdd} + filterEntities={filterEntities} + /> + + {!!pageCount && !loading && ( + - {!!pageCount && !loading && ( - - )} - - - ); - } -} + )} + + + ); +}; + +SuitesPageWrapped.propTypes = { + deleteItems: PropTypes.func, + onEditItem: PropTypes.func, + onEditItems: PropTypes.func, + activePage: PropTypes.number, + itemCount: PropTypes.number, + pageCount: PropTypes.number, + pageSize: PropTypes.number, + sortingColumn: PropTypes.string, + sortingDirection: PropTypes.string, + onChangePage: PropTypes.func, + onChangePageSize: PropTypes.func, + onChangeSorting: PropTypes.func, + onFilterAdd: PropTypes.func, + onFilterRemove: PropTypes.func, + onFilterValidate: PropTypes.func, + onFilterChange: PropTypes.func, + filterErrors: PropTypes.object, + filterEntities: PropTypes.array, +}; + +SuitesPageWrapped.defaultProps = { + deleteItems: () => {}, + onEditItem: () => {}, + onEditItems: () => {}, + activePage: DEFAULT_PAGINATION[PAGE_KEY], + itemCount: null, + pageCount: null, + pageSize: DEFAULT_PAGINATION[SIZE_KEY], + sortingColumn: null, + sortingDirection: null, + onChangePage: () => {}, + onChangePageSize: () => {}, + onChangeSorting: () => {}, + onFilterAdd: () => {}, + onFilterRemove: () => {}, + onFilterValidate: () => {}, + onFilterChange: () => {}, + filterErrors: {}, + filterEntities: [], +}; + +export const SuitesPage = withSortingURL({ + defaultFields: [ENTITY_START_TIME], + defaultDirection: SORTING_ASC, + namespaceSelector, +})( + withPagination({ + paginationSelector: suitePaginationSelector, + namespaceSelector, + })(SuitesPageWrapped), +); diff --git a/app/src/pages/inside/testsPage/testsPage.jsx b/app/src/pages/inside/testsPage/testsPage.jsx index 9608ed043f..338452b652 100644 --- a/app/src/pages/inside/testsPage/testsPage.jsx +++ b/app/src/pages/inside/testsPage/testsPage.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2019 EPAM Systems + * Copyright 2024 EPAM Systems * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ * limitations under the License. */ -import { Component } from 'react'; -import track from 'react-tracking'; +import { useEffect, useState } from 'react'; +import { useTracking } from 'react-tracking'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { SORTING_ASC, withSortingURL } from 'controllers/sorting'; import { PageLayout, PageSection } from 'layouts/pageLayout'; import { LaunchSuiteGrid } from 'pages/inside/common/launchSuiteGrid'; @@ -42,244 +42,210 @@ import { parentItemSelector, loadingSelector, } from 'controllers/testItem'; -import { prevTestItemSelector } from 'controllers/pages'; +import { prevTestItemSelector, userRolesSelector } from 'controllers/pages'; import { ENTITY_START_TIME } from 'components/filterEntities/constants'; +import { canWorkWithTests } from 'common/utils/permissions/permissions'; -@connect( - (state) => ({ - debugMode: debugModeSelector(state), - validationErrors: validationErrorsSelector(state), - tests: testsSelector(state), - selectedTests: selectedTestsSelector(state), - parentItem: parentItemSelector(state), - loading: loadingSelector(state), - highlightItemId: prevTestItemSelector(state), - }), - { - toggleTestSelectionAction, - unselectAllTestsAction, - toggleAllTestsAction, - selectTestsAction, - fetchTestItemsAction, - }, -) -@withSortingURL({ - defaultFields: [ENTITY_START_TIME], - defaultDirection: SORTING_ASC, - namespaceSelector, -}) -@withPagination({ - paginationSelector: testPaginationSelector, - namespaceSelector, -}) -@track() -export class TestsPage extends Component { - static propTypes = { - deleteItems: PropTypes.func, - onEditItem: PropTypes.func, - onEditItems: PropTypes.func, - validationErrors: PropTypes.object.isRequired, - debugMode: PropTypes.bool.isRequired, - tests: PropTypes.arrayOf(PropTypes.object), - selectedTests: PropTypes.arrayOf(PropTypes.object), - activePage: PropTypes.number, - itemCount: PropTypes.number, - pageCount: PropTypes.number, - pageSize: PropTypes.number, - sortingColumn: PropTypes.string, - sortingDirection: PropTypes.string, - fetchTestItemsAction: PropTypes.func, - onChangePage: PropTypes.func, - onChangePageSize: PropTypes.func, - onChangeSorting: PropTypes.func, - toggleTestSelectionAction: PropTypes.func, - unselectAllTestsAction: PropTypes.func, - toggleAllTestsAction: PropTypes.func, - selectTestsAction: PropTypes.func, - parentItem: PropTypes.object, - loading: PropTypes.bool, - onFilterAdd: PropTypes.func, - onFilterRemove: PropTypes.func, - onFilterValidate: PropTypes.func, - onFilterChange: PropTypes.func, - filterErrors: PropTypes.object, - filterEntities: PropTypes.array, - tracking: PropTypes.shape({ - trackEvent: PropTypes.func, - getTrackingData: PropTypes.func, - }).isRequired, - highlightItemId: PropTypes.number, - }; +// TODO: Refactor to avoid duplication +export const TestsPageWrapped = ({ + deleteItems, + onEditItem, + onEditItems, + activePage, + itemCount, + pageCount, + pageSize, + sortingColumn, + sortingDirection, + onChangePage, + onChangePageSize, + onChangeSorting, + onFilterAdd, + onFilterRemove, + onFilterValidate, + onFilterChange, + filterErrors, + filterEntities, +}) => { + const { trackEvent } = useTracking(); + + const [highlightedRowId, setHighlightedRowId] = useState(null); + const [isGridRowHighlighted, setIsGridRowHighlighted] = useState(false); + const debugMode = useSelector(debugModeSelector); + const validationErrors = useSelector(validationErrorsSelector); + const tests = useSelector(testsSelector); + const selectedTests = useSelector(selectedTestsSelector); + const parentItem = useSelector(parentItemSelector); + const loading = useSelector(loadingSelector); + const highlightItemId = useSelector(prevTestItemSelector); + const userRoles = useSelector(userRolesSelector); + const canManageTests = canWorkWithTests(userRoles); + const dispatch = useDispatch(); - static defaultProps = { - deleteItems: () => {}, - onEditItem: () => {}, - onEditItems: () => {}, - tests: [], - selectedTests: [], - activePage: DEFAULT_PAGINATION[PAGE_KEY], - itemCount: null, - pageCount: null, - pageSize: DEFAULT_PAGINATION[SIZE_KEY], - sortingColumn: null, - sortingDirection: null, - fetchTestItemsAction: () => {}, - onChangePage: () => {}, - onChangePageSize: () => {}, - onChangeSorting: () => {}, - toggleTestSelectionAction: () => {}, - unselectAllTestsAction: () => {}, - toggleAllTestsAction: () => {}, - selectTestsAction: () => {}, - parentItem: null, - loading: false, - onFilterAdd: () => {}, - onFilterRemove: () => {}, - onFilterValidate: () => {}, - onFilterChange: () => {}, - filterErrors: {}, - filterEntities: [], - highlightItemId: null, + // TODO - extract highlighting into custom hook + const onHighlightRow = (rowId) => { + setHighlightedRowId(rowId); + setIsGridRowHighlighted(true); }; - state = { - highlightedRowId: null, - isGridRowHighlighted: false, - isSauceLabsIntegrationView: false, + const onGridRowHighlighted = () => { + setIsGridRowHighlighted(false); }; - componentDidMount() { - const { highlightItemId } = this.props; - if (highlightItemId) { - this.onHighlightRow(highlightItemId); + const handleAllTestsSelection = () => { + if (tests.length !== selectedTests.length) { + trackEvent(TESTS_PAGE_EVENTS.CLICK_SELECT_ALL_ITEMS); } - } + dispatch(toggleAllTestsAction(tests)); + }; - componentWillUnmount() { - if (this.props.selectedTests.length > 0) { - this.props.unselectAllTestsAction(); + const handleOneItemSelection = (value) => { + if (!selectedTests.includes(value)) { + trackEvent(TESTS_PAGE_EVENTS.CLICK_SELECT_ONE_ITEM); } - } - - onHighlightRow = (highlightedRowId) => { - this.setState({ - highlightedRowId, - isGridRowHighlighted: true, - }); + dispatch(toggleTestSelectionAction(value)); }; - onGridRowHighlighted = () => { - this.setState({ - isGridRowHighlighted: false, - }); + const handleItemsSelection = (items) => { + dispatch(selectTestsAction(items)); }; - handleAllTestsSelection = () => { - if (this.props.tests.length !== this.props.selectedTests.length) { - this.props.tracking.trackEvent(TESTS_PAGE_EVENTS.CLICK_SELECT_ALL_ITEMS); - } - this.props.toggleAllTestsAction(this.props.tests); + const unselectAllItems = () => { + trackEvent(TESTS_PAGE_EVENTS.CLOSE_ICON_FOR_ALL_SELECTIONS); + dispatch(unselectAllTestsAction()); }; - handleOneItemSelection = (value) => { - if (!this.props.selectedTests.includes(value)) { - this.props.tracking.trackEvent(TESTS_PAGE_EVENTS.CLICK_SELECT_ONE_ITEM); - } - this.props.toggleTestSelectionAction(value); + const unselectItem = (item) => { + trackEvent(TESTS_PAGE_EVENTS.CLOSE_ICON_SELECTED_ITEM); + dispatch(toggleTestSelectionAction(item)); }; - unselectAllItems = () => { - this.props.tracking.trackEvent(TESTS_PAGE_EVENTS.CLOSE_ICON_FOR_ALL_SELECTIONS); - this.props.unselectAllTestsAction(); + const rowHighlightingConfig = { + onGridRowHighlighted, + isGridRowHighlighted, + highlightedRowId, }; - unselectItem = (item) => { - this.props.tracking.trackEvent(TESTS_PAGE_EVENTS.CLOSE_ICON_SELECTED_ITEM); - this.props.toggleTestSelectionAction(item); + const handleRefresh = () => { + dispatch(fetchTestItemsAction()); }; - render() { - const { - tests, - sortingColumn, - sortingDirection, - onChangeSorting, - selectedTests, - activePage, - itemCount, - pageCount, - pageSize, - onChangePage, - onChangePageSize, - parentItem, - loading, - debugMode, - deleteItems, - onEditItem, - onEditItems, - onFilterAdd, - onFilterRemove, - onFilterValidate, - onFilterChange, - filterErrors, - filterEntities, - tracking, - } = this.props; - const rowHighlightingConfig = { - onGridRowHighlighted: this.onGridRowHighlighted, - isGridRowHighlighted: this.state.isGridRowHighlighted, - highlightedRowId: this.state.highlightedRowId, + useEffect(() => { + if (highlightItemId) { + onHighlightRow(highlightItemId); + } + }, [highlightItemId]); + + useEffect(() => { + return () => { + if (selectedTests.length > 0) { + dispatch(unselectAllTestsAction()); + } }; + }, [selectedTests, dispatch]); - return ( - - - tracking.trackEvent(TESTS_PAGE_EVENTS.PROCEED_VALID_ITEMS)} - onRefresh={this.props.fetchTestItemsAction} - debugMode={debugMode} - errors={this.props.validationErrors} - onDelete={() => deleteItems(selectedTests)} - onEditItems={() => onEditItems(selectedTests)} - filterEntities={filterEntities} - filterErrors={filterErrors} - onFilterChange={onFilterChange} - onFilterValidate={onFilterValidate} - onFilterRemove={onFilterRemove} - onFilterAdd={onFilterAdd} - events={TESTS_PAGE_EVENTS} + return ( + + + trackEvent(TESTS_PAGE_EVENTS.PROCEED_VALID_ITEMS)} + onRefresh={handleRefresh} + debugMode={debugMode} + errors={validationErrors} + onDelete={() => deleteItems(selectedTests)} + onEditItems={() => onEditItems(selectedTests)} + filterEntities={filterEntities} + filterErrors={filterErrors} + onFilterChange={onFilterChange} + onFilterValidate={onFilterValidate} + onFilterRemove={onFilterRemove} + onFilterAdd={onFilterAdd} + events={TESTS_PAGE_EVENTS} + /> + + {!!pageCount && !loading && ( + - - {!!pageCount && !loading && ( - - )} - - - ); - } -} + )} + + + ); +}; + +TestsPageWrapped.propTypes = { + deleteItems: PropTypes.func, + onEditItem: PropTypes.func, + onEditItems: PropTypes.func, + activePage: PropTypes.number, + itemCount: PropTypes.number, + pageCount: PropTypes.number, + pageSize: PropTypes.number, + sortingColumn: PropTypes.string, + sortingDirection: PropTypes.string, + onChangePage: PropTypes.func, + onChangePageSize: PropTypes.func, + onChangeSorting: PropTypes.func, + onFilterAdd: PropTypes.func, + onFilterRemove: PropTypes.func, + onFilterValidate: PropTypes.func, + onFilterChange: PropTypes.func, + filterErrors: PropTypes.object, + filterEntities: PropTypes.arrayOf(PropTypes.object), +}; + +TestsPageWrapped.defaultProps = { + deleteItems: () => {}, + onEditItem: () => {}, + onEditItems: () => {}, + activePage: DEFAULT_PAGINATION[PAGE_KEY], + itemCount: null, + pageCount: null, + pageSize: DEFAULT_PAGINATION[SIZE_KEY], + sortingColumn: null, + sortingDirection: null, + onChangePage: () => {}, + onChangePageSize: () => {}, + onChangeSorting: () => {}, + onFilterAdd: () => {}, + onFilterRemove: () => {}, + onFilterValidate: () => {}, + onFilterChange: () => {}, + filterErrors: {}, + filterEntities: [], +}; + +export const TestsPage = withSortingURL({ + defaultFields: [ENTITY_START_TIME], + defaultDirection: SORTING_ASC, + namespaceSelector, +})( + withPagination({ + paginationSelector: testPaginationSelector, + namespaceSelector, + })(TestsPageWrapped), +);