diff --git a/app/src/common/constants/permissions.js b/app/src/common/constants/permissions.js index 13a5017fad..c9e5a4ffc4 100644 --- a/app/src/common/constants/permissions.js +++ b/app/src/common/constants/permissions.js @@ -42,6 +42,9 @@ export const ACTIONS = { FORCE_FINISH_RERUN_LAUNCH: 'FORCE_FINISH_RERUN_LAUNCH', START_ANALYSIS: 'START_ANALYSIS', DELETE_TEST_ITEM: 'DELETE_TEST_ITEM', + MAKE_DECISION: 'MAKE_DECISION', + CHANGE_TEST_ITEM_STATUS: 'CHANGE_TEST_ITEM_STATUS', + MANAGE_BTS_ISSUES: 'MANAGE_BTS_ISSUES', MOVE_TO_DEBUG: 'MOVE_TO_DEBUG', MERGE_LAUNCHES: 'MERGE_LAUNCHES', WORK_WITH_FILTERS: 'WORK_WITH_FILTERS', @@ -80,7 +83,10 @@ export const PERMISSIONS_MAP = { [ACTIONS.START_ANALYSIS]: true, [ACTIONS.BULK_EDIT_ITEMS]: true, [ACTIONS.FORCE_FINISH_RERUN_LAUNCH]: true, + [ACTIONS.CHANGE_TEST_ITEM_STATUS]: true, [ACTIONS.DELETE_TEST_ITEM]: true, + [ACTIONS.MAKE_DECISION]: true, + [ACTIONS.MANAGE_BTS_ISSUES]: true, [ACTIONS.WORK_WITH_FILTERS]: true, [ACTIONS.WORK_WITH_WIDGETS]: true, [ACTIONS.SEE_ORGANIZATION_SETTINGS]: true, @@ -120,6 +126,9 @@ export const PERMISSIONS_MAP = { [ACTIONS.FORCE_FINISH_RERUN_LAUNCH]: true, [ACTIONS.BULK_EDIT_ITEMS]: true, [ACTIONS.DELETE_TEST_ITEM]: true, + [ACTIONS.CHANGE_TEST_ITEM_STATUS]: true, + [ACTIONS.MAKE_DECISION]: true, + [ACTIONS.MANAGE_BTS_ISSUES]: true, [ACTIONS.WORK_WITH_FILTERS]: true, [ACTIONS.WORK_WITH_WIDGETS]: true, [ACTIONS.RENAME_PROJECT]: true, diff --git a/app/src/common/utils/permissions/permissions.js b/app/src/common/utils/permissions/permissions.js index 6acf8efe89..9cc74386c3 100644 --- a/app/src/common/utils/permissions/permissions.js +++ b/app/src/common/utils/permissions/permissions.js @@ -63,6 +63,9 @@ export const canForceFinishRerunLaunch = checkPermission(ACTIONS.FORCE_FINISH_RE export const canStartAnalysis = checkPermission(ACTIONS.START_ANALYSIS); export const canDeleteTestItem = checkPermission(ACTIONS.DELETE_TEST_ITEM); export const canMoveToDebug = checkPermission(ACTIONS.MOVE_TO_DEBUG); +export const canChangeTestItemStatus = checkPermission(ACTIONS.CHANGE_TEST_ITEM_STATUS); +export const canMakeDecision = checkPermission(ACTIONS.MAKE_DECISION); +export const canManageBTSIssues = checkPermission(ACTIONS.MANAGE_BTS_ISSUES); export const canMergeLaunches = checkPermission(ACTIONS.MERGE_LAUNCHES); export const canWorkWithFilters = checkPermission(ACTIONS.WORK_WITH_FILTERS); export const canReadData = checkPermission(ACTIONS.READ_DATA); diff --git a/app/src/pages/inside/common/statusDropdown/statusDropdown.jsx b/app/src/pages/inside/common/statusDropdown/statusDropdown.jsx index d8f28bdc3d..a8d2df981b 100644 --- a/app/src/pages/inside/common/statusDropdown/statusDropdown.jsx +++ b/app/src/pages/inside/common/statusDropdown/statusDropdown.jsx @@ -65,6 +65,7 @@ export class StatusDropdown extends Component { onChange: PropTypes.func, withIndicator: PropTypes.bool, disabled: PropTypes.bool, + readOnly: PropTypes.bool, }; static defaultProps = { @@ -75,6 +76,7 @@ export class StatusDropdown extends Component { onChange: () => {}, withIndicator: false, disabled: false, + readOnly: false, }; updateItem = (newStatus) => { @@ -142,25 +144,33 @@ export class StatusDropdown extends Component { }; render() { - const { status, withIndicator, disabled } = this.props; + const { status, withIndicator, disabled, readOnly } = this.props; return (
- + {readOnly ? ( + + ) : ( + + )}
); } diff --git a/app/src/pages/inside/common/statusDropdown/statusDropdown.scss b/app/src/pages/inside/common/statusDropdown/statusDropdown.scss index f4e3d98e86..6e7c8f08b9 100644 --- a/app/src/pages/inside/common/statusDropdown/statusDropdown.scss +++ b/app/src/pages/inside/common/statusDropdown/statusDropdown.scss @@ -17,42 +17,55 @@ .status-dropdown { text-transform: capitalize; } + .dropdown { height: auto; } + .opened { .select-block { box-shadow: none; } } + .select-block { min-width: 140px; padding: 0 10px 0 0; border: none; background-color: transparent; + &.select-block-with-indicator { min-width: auto; padding: 0 16px; } } + .value { display: inline; padding-right: 7px; line-height: 18px; } + .arrow { position: relative; right: auto; top: -2px; margin-top: 0; } + .select-list { margin-top: 0; min-width: fit-content; } + .status-container { display: inline-flex; } + .dropdown-option { padding: 0 16px 0 15px; } + +.defined-status { + padding: 0 16px; +} \ No newline at end of file diff --git a/app/src/pages/inside/common/testItemStatus/testItemStatus.jsx b/app/src/pages/inside/common/testItemStatus/testItemStatus.jsx index c031cf7954..6260b2ad63 100644 --- a/app/src/pages/inside/common/testItemStatus/testItemStatus.jsx +++ b/app/src/pages/inside/common/testItemStatus/testItemStatus.jsx @@ -20,8 +20,8 @@ import styles from './testItemStatus.scss'; const cx = classNames.bind(styles); -export const TestItemStatus = ({ status }) => ( -
+export const TestItemStatus = ({ status, className }) => ( +
{status}
@@ -29,8 +29,10 @@ export const TestItemStatus = ({ status }) => ( TestItemStatus.propTypes = { status: PropTypes.string, + className: PropTypes.string, }; TestItemStatus.defaultProps = { status: '', + className: '', }; diff --git a/app/src/pages/inside/logsPage/logItemInfo/defectDetails/defectDetails.jsx b/app/src/pages/inside/logsPage/logItemInfo/defectDetails/defectDetails.jsx index fb3531527c..b094d9c91f 100644 --- a/app/src/pages/inside/logsPage/logItemInfo/defectDetails/defectDetails.jsx +++ b/app/src/pages/inside/logsPage/logItemInfo/defectDetails/defectDetails.jsx @@ -14,12 +14,12 @@ * limitations under the License. */ -import React, { Component, Fragment } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import track from 'react-tracking'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTracking } from 'react-tracking'; import classNames from 'classnames/bind'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import Parser from 'html-react-parser'; import { GhostButton } from 'components/buttons/ghostButton'; import { getIssueTitle } from 'pages/inside/common/utils'; @@ -52,6 +52,12 @@ import { PALabel, } from 'pages/inside/stepPage/stepGrid/defectType/defectType'; import { TO_INVESTIGATE_LOCATOR_PREFIX } from 'common/constants/defectTypes'; +import { + canChangeTestItemStatus, + canMakeDecision, + canManageBTSIssues, +} from 'common/utils/permissions/permissions'; +import { userRolesSelector } from 'controllers/pages'; import styles from './defectDetails.scss'; const cx = classNames.bind(styles); @@ -110,62 +116,22 @@ const getUnlinkIssueEventsInfo = (place) => ({ closeIcon: LOG_PAGE_EVENTS.UNLINK_ISSUE_MODAL_EVENTS.CLOSE_ICON_UNLINK_ISSUE_MODAL, }); -@connect( - (state) => ({ - btsIntegrations: availableBtsIntegrationsSelector(state), - isBtsPluginsExist: isBtsPluginsExistSelector(state), - enabledBtsPlugins: enabledBtsPluginsSelector(state), - }), - { - linkIssueAction, - unlinkIssueAction, - postIssueAction, - editDefectsAction, - updateHistoryItemIssues: updateHistoryItemIssuesAction, - fetchHistoryItemsWithLoading: fetchHistoryItemsWithLoadingAction, - }, -) -@track() -@injectIntl -export class DefectDetails extends Component { - static propTypes = { - intl: PropTypes.object.isRequired, - editDefectsAction: PropTypes.func.isRequired, - linkIssueAction: PropTypes.func.isRequired, - unlinkIssueAction: PropTypes.func.isRequired, - postIssueAction: PropTypes.func.isRequired, - btsIntegrations: PropTypes.array.isRequired, - fetchFunc: PropTypes.func.isRequired, - updateHistoryItemIssues: PropTypes.func.isRequired, - debugMode: PropTypes.bool.isRequired, - fetchHistoryItemsWithLoading: PropTypes.func.isRequired, - tracking: PropTypes.shape({ - trackEvent: PropTypes.func, - getTrackingData: PropTypes.func, - }).isRequired, - logItem: PropTypes.object, - isBtsPluginsExist: PropTypes.bool, - enabledBtsPlugins: PropTypes.array, - }; - static defaultProps = { - logItem: null, - isBtsPluginsExist: false, - enabledBtsPlugins: [], - }; +export const DefectDetails = ({ fetchFunc, debugMode, logItem }) => { + const { formatMessage } = useIntl(); + const { trackEvent } = useTracking(); + const dispatch = useDispatch(); + const btsIntegrations = useSelector(availableBtsIntegrationsSelector); + const isBtsPluginsExist = useSelector(isBtsPluginsExistSelector); + const enabledBtsPlugins = useSelector(enabledBtsPluginsSelector); + const [expanded, setExpanded] = useState(false); + const userRoles = useSelector(userRolesSelector); - state = { - expanded: false, - }; - - getIssueActionTitle = (noIssueMessage, isPostIssueUnavailable) => { - const { - logItem, - intl: { formatMessage }, - btsIntegrations, - isBtsPluginsExist, - enabledBtsPlugins, - } = this.props; + const canChangeStatus = canChangeTestItemStatus(userRoles); + const canSeeMakeDecision = canMakeDecision(userRoles); + const canWorkWithBtsIssues = canManageBTSIssues(userRoles); + const isDefectTypeVisible = logItem.issue?.issueType; + const getIssueActionTitle = (noIssueMessage, isPostIssueUnavailable) => { if (!logItem.issue) { return formatMessage(noIssueMessage); } @@ -179,21 +145,17 @@ export class DefectDetails extends Component { ); }; - isDefectTypeVisible = () => { - const { logItem } = this.props; - return logItem.issue?.issueType; - }; - - handleLinkIssue = () => { - this.props.tracking.trackEvent(LOG_PAGE_EVENTS.LINK_ISSUE_ACTION); - this.props.linkIssueAction([this.props.logItem], { - fetchFunc: this.props.fetchFunc, - eventsInfo: getLinkIssueEventsInfo(), - }); + const handleLinkIssue = () => { + trackEvent(LOG_PAGE_EVENTS.LINK_ISSUE_ACTION); + dispatch( + linkIssueAction([logItem], { + fetchFunc, + eventsInfo: getLinkIssueEventsInfo(), + }), + ); }; - handleUnlinkTicket = (ticketId) => { - const { logItem, fetchFunc, tracking } = this.props; + const handleUnlinkTicket = (ticketId) => { const items = [ { ...logItem, @@ -206,228 +168,241 @@ export class DefectDetails extends Component { }, ]; - tracking.trackEvent(LOG_PAGE_EVENTS.UNLINK_ISSUES_ACTION); + trackEvent(LOG_PAGE_EVENTS.UNLINK_ISSUES_ACTION); - this.props.unlinkIssueAction(items, { - fetchFunc, - eventsInfo: getUnlinkIssueEventsInfo(), - }); + dispatch( + unlinkIssueAction(items, { + fetchFunc, + eventsInfo: getUnlinkIssueEventsInfo(), + }), + ); }; - handlePostIssue = () => { - this.props.tracking.trackEvent(LOG_PAGE_EVENTS.POST_ISSUE_ACTION); - this.props.postIssueAction([this.props.logItem], { - fetchFunc: this.props.fetchFunc, - eventsInfo: getPostIssueEventsInfo(), - }); + const handlePostIssue = () => { + trackEvent(LOG_PAGE_EVENTS.POST_ISSUE_ACTION); + dispatch( + postIssueAction([logItem], { + fetchFunc, + eventsInfo: getPostIssueEventsInfo(), + }), + ); }; - onDefectEdited = (issues) => { - const { fetchFunc, updateHistoryItemIssues } = this.props; - + const onDefectEdited = (issues) => { if (issues) { - updateHistoryItemIssues(issues); + dispatch(updateHistoryItemIssuesAction(issues)); } else { fetchFunc(); } }; - handleEditDefect = () => { - const { logItem } = this.props; + const handleEditDefect = () => { const MAKE_DECISION = 'make_decision'; - this.props.tracking.trackEvent( + trackEvent( LOG_PAGE_EVENTS.MAKE_DECISION_MODAL_EVENTS.getOpenModalEvent( logItem.issue.issueType.startsWith(TO_INVESTIGATE_LOCATOR_PREFIX), ), ); - this.props.editDefectsAction([logItem], { - fetchFunc: this.onDefectEdited, - eventsInfo: { - changeSearchMode: LOG_PAGE_EVENTS.CHANGE_SEARCH_MODE_EDIT_DEFECT_MODAL, - selectAllSimilarItems: LOG_PAGE_EVENTS.SELECT_ALL_SIMILAR_ITEMS_EDIT_DEFECT_MODAL, - selectSpecificSimilarItem: LOG_PAGE_EVENTS.SELECT_SPECIFIC_SIMILAR_ITEM_EDIT_DEFECT_MODAL, - editDefectsEvents: LOG_PAGE_EVENTS.MAKE_DECISION_MODAL_EVENTS, - unlinkIssueEvents: getUnlinkIssueEventsInfo(MAKE_DECISION), - postIssueEvents: getPostIssueEventsInfo(MAKE_DECISION), - linkIssueEvents: getLinkIssueEventsInfo(MAKE_DECISION), - }, - }); - }; - - toggleExpanded = () => { - this.setState( - (state) => ({ - expanded: !state.expanded, + dispatch( + editDefectsAction([logItem], { + fetchFunc: onDefectEdited, + eventsInfo: { + changeSearchMode: LOG_PAGE_EVENTS.CHANGE_SEARCH_MODE_EDIT_DEFECT_MODAL, + selectAllSimilarItems: LOG_PAGE_EVENTS.SELECT_ALL_SIMILAR_ITEMS_EDIT_DEFECT_MODAL, + selectSpecificSimilarItem: LOG_PAGE_EVENTS.SELECT_SPECIFIC_SIMILAR_ITEM_EDIT_DEFECT_MODAL, + editDefectsEvents: LOG_PAGE_EVENTS.MAKE_DECISION_MODAL_EVENTS, + unlinkIssueEvents: getUnlinkIssueEventsInfo(MAKE_DECISION), + postIssueEvents: getPostIssueEventsInfo(MAKE_DECISION), + linkIssueEvents: getLinkIssueEventsInfo(MAKE_DECISION), + }, }), - () => { - this.props.tracking.trackEvent( - LOG_PAGE_EVENTS.getClickOnDefectDetailsTogglerEvent(this.state.expanded), - ); - }, ); }; - onChangeStatus = (status) => - this.props.tracking.trackEvent(LOG_PAGE_EVENTS.getChangeItemStatusEvent(status)); + const toggleExpanded = () => { + setExpanded(!expanded); + trackEvent(LOG_PAGE_EVENTS.getClickOnDefectDetailsTogglerEvent(expanded)); + }; + + const onChangeStatus = (status) => trackEvent(LOG_PAGE_EVENTS.getChangeItemStatusEvent(status)); - onClickIssue = (pluginName) => { - this.props.tracking.trackEvent(LOG_PAGE_EVENTS.onClickIssueTicketEvent(pluginName)); + const onClickIssue = (pluginName) => { + trackEvent(LOG_PAGE_EVENTS.onClickIssueTicketEvent(pluginName)); }; - render() { - const { - logItem, - btsIntegrations, - debugMode, - intl: { formatMessage }, - fetchHistoryItemsWithLoading, - } = this.props; - const { expanded } = this.state; - const isPostIssueUnavailable = !isPostIssueActionAvailable(this.props.btsIntegrations); + const isPostIssueUnavailable = !isPostIssueActionAvailable(btsIntegrations); - return ( -
- {this.isDefectTypeVisible() && ( -
-
- - {Parser(CommentIcon)} - {formatMessage(messages.comment)} - -
- {expanded ? ( - - - - ) : ( + return ( +
+ {isDefectTypeVisible && ( +
+
+ + {Parser(CommentIcon)} + {formatMessage(messages.comment)} + +
+ {expanded ? ( + - )} -
-
- -
- {expanded && ( - - - {Parser(BugIcon)} - {formatMessage(messages.btsLink)} - -
- -
- {!debugMode && ( - <> - - {formatMessage(messages.postIssue)} - - - {formatMessage(messages.linkIssue)} - - - )} -
+ + ) : ( + )}
- )} -
- {this.isDefectTypeVisible() && ( - - {expanded ? null : ( -
- - {Parser(ArrowDownIcon)} - {formatMessage(messages.more)} - - - {Parser(BugIcon)} - {logItem.issue.externalSystemIssues.length} - +
+ {expanded && ( + <> + + {Parser(BugIcon)} + {formatMessage(messages.btsLink)} + +
+
- )} - {logItem.issue.ignoreAnalyzer && } - {logItem.issue.autoAnalyzed && } - {!!logItem.patternTemplates.length && ( - - )} - - )} - - + + {formatMessage(messages.postIssue)} + + + {formatMessage(messages.linkIssue)} + + + )} + + )} +
+
+ )} +
+ {isDefectTypeVisible && ( + <> + {expanded ? null : ( +
+ + + {Parser(BugIcon)} + {logItem.issue.externalSystemIssues.length} + +
+ )} + {logItem.issue.ignoreAnalyzer && } + {logItem.issue.autoAnalyzed && } + {!!logItem.patternTemplates.length && ( + + )} + + )} + + dispatch(fetchHistoryItemsWithLoadingAction())} + onChange={onChangeStatus} + withIndicator + readOnly={!canChangeStatus} + /> + + {isDefectTypeVisible && ( + + - {this.isDefectTypeVisible() && ( - - - - )} - {!debugMode && ( -
- - {formatMessage(messages.makeDecision)} - -
- )} - {expanded && logItem.issue && ( - - {Parser(ArrowDownIcon)} - {formatMessage(messages.showLess)} - - )} -
+ )} + {!debugMode && canSeeMakeDecision && ( +
+ + {formatMessage(messages.makeDecision)} + +
+ )} + {expanded && logItem.issue && ( + + )}
- ); - } -} +
+ ); +}; + +DefectDetails.propTypes = { + fetchFunc: PropTypes.func.isRequired, + debugMode: PropTypes.bool.isRequired, + logItem: PropTypes.object, +}; + +DefectDetails.defaultProps = { + logItem: null, +}; diff --git a/app/src/pages/inside/logsPage/logItemInfo/defectDetails/defectDetails.scss b/app/src/pages/inside/logsPage/logItemInfo/defectDetails/defectDetails.scss index 6701e857c0..9e32cfcf69 100644 --- a/app/src/pages/inside/logsPage/logItemInfo/defectDetails/defectDetails.scss +++ b/app/src/pages/inside/logsPage/logItemInfo/defectDetails/defectDetails.scss @@ -118,10 +118,16 @@ line-height: 16px; padding-right: 16px; color: $COLOR--topaz; + background: none; + border: none; font-size: 13px; font-family: $FONT-SEMIBOLD; cursor: pointer; + &:focus-visible { + outline-color: $COLOR--topaz; + } + .icon { width: 13px; height: 13px; @@ -146,6 +152,7 @@ .defect-item { border: none; cursor: default; + &:hover { border: none; }