mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
hide discard for nonauthors (#2634)
This commit is contained in:
parent
1be2483e6a
commit
2107834768
@ -1,30 +1,9 @@
|
||||
import React, { FC, VFC } from 'react';
|
||||
import { Alert, Box, styled } from '@mui/material';
|
||||
import { ChangeRequestFeatureToggleChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ChangeRequestFeatureToggleChange';
|
||||
import { objectId } from 'utils/objectId';
|
||||
import { ToggleStatusChange } from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/ToggleStatusChange';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import useToast from 'hooks/useToast';
|
||||
import type {
|
||||
IChange,
|
||||
IChangeRequest,
|
||||
IChangeRequestFeature,
|
||||
} from '../changeRequest.types';
|
||||
import { hasNameField } from '../changeRequest.types';
|
||||
import {
|
||||
Discard,
|
||||
StrategyAddedChange,
|
||||
StrategyDeletedChange,
|
||||
StrategyEditedChange,
|
||||
} from '../ChangeRequestOverview/ChangeRequestFeatureToggleChange/StrategyChange';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { StrategyExecution } from '../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
||||
import {
|
||||
CodeSnippetPopover,
|
||||
PopoverDiff,
|
||||
} from './CodeSnippetPopover/CodeSnippetPopover';
|
||||
import React, { VFC } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import type { IChangeRequest } from '../changeRequest.types';
|
||||
import { FeatureToggleChanges } from './Changes/FeatureToggleChanges';
|
||||
import { Change } from './Changes/Change/Change';
|
||||
import { DiscardContainer } from './Changes/Change/Discard';
|
||||
|
||||
interface IChangeRequestProps {
|
||||
changeRequest: IChangeRequest;
|
||||
@ -32,190 +11,15 @@ interface IChangeRequestProps {
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
const StyledSingleChangeBox = styled(Box, {
|
||||
shouldForwardProp: (prop: string) => !prop.startsWith('$'),
|
||||
})<{
|
||||
$hasConflict: boolean;
|
||||
$isAfterWarning: boolean;
|
||||
$isLast: boolean;
|
||||
$isInConflictFeature: boolean;
|
||||
}>(
|
||||
({
|
||||
theme,
|
||||
$hasConflict,
|
||||
$isInConflictFeature,
|
||||
$isAfterWarning,
|
||||
$isLast,
|
||||
}) => ({
|
||||
borderLeft: '1px solid',
|
||||
borderRight: '1px solid',
|
||||
borderTop: '1px solid',
|
||||
borderBottom: $isLast ? '1px solid' : 'none',
|
||||
borderRadius: $isLast
|
||||
? `0 0
|
||||
${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`
|
||||
: 0,
|
||||
borderColor:
|
||||
$hasConflict || $isInConflictFeature
|
||||
? theme.palette.warning.border
|
||||
: theme.palette.dividerAlternative,
|
||||
borderTopColor:
|
||||
($hasConflict || $isAfterWarning) && !$isInConflictFeature
|
||||
? theme.palette.warning.border
|
||||
: theme.palette.dividerAlternative,
|
||||
})
|
||||
);
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
borderRadius: 0,
|
||||
padding: theme.spacing(0, 2),
|
||||
'&.MuiAlert-standardWarning': {
|
||||
borderStyle: 'none none solid none',
|
||||
},
|
||||
}));
|
||||
|
||||
const Change: FC<{
|
||||
onDiscard: () => Promise<void>;
|
||||
index: number;
|
||||
changeRequest: IChangeRequest;
|
||||
change: IChange;
|
||||
feature: IChangeRequestFeature;
|
||||
}> = ({ index, change, feature, changeRequest, onDiscard }) => {
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(
|
||||
changeRequest.project
|
||||
);
|
||||
const allowChangeRequestActions = isChangeRequestConfigured(
|
||||
changeRequest.environment
|
||||
);
|
||||
|
||||
const showDiscard =
|
||||
allowChangeRequestActions &&
|
||||
!['Cancelled', 'Applied'].includes(changeRequest.state) &&
|
||||
changeRequest.features.flatMap(feature => feature.changes).length > 1;
|
||||
|
||||
return (
|
||||
<StyledSingleChangeBox
|
||||
key={objectId(change)}
|
||||
$hasConflict={Boolean(change.conflict)}
|
||||
$isInConflictFeature={Boolean(feature.conflict)}
|
||||
$isAfterWarning={Boolean(feature.changes[index - 1]?.conflict)}
|
||||
$isLast={index + 1 === feature.changes.length}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(change.conflict) && !feature.conflict}
|
||||
show={
|
||||
<StyledAlert severity="warning">
|
||||
<strong>Conflict!</strong> This change can’t be applied.{' '}
|
||||
{change.conflict}.
|
||||
</StyledAlert>
|
||||
}
|
||||
/>
|
||||
<Box sx={{ p: 2 }}>
|
||||
{change.action === 'updateEnabled' && (
|
||||
<ToggleStatusChange
|
||||
enabled={change.payload.enabled}
|
||||
discard={
|
||||
<ConditionallyRender
|
||||
condition={showDiscard}
|
||||
show={<Discard onDiscard={onDiscard} />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{change.action === 'addStrategy' && (
|
||||
<>
|
||||
<StrategyAddedChange
|
||||
discard={
|
||||
<ConditionallyRender
|
||||
condition={showDiscard}
|
||||
show={<Discard onDiscard={onDiscard} />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
</StrategyAddedChange>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
</>
|
||||
)}
|
||||
{change.action === 'deleteStrategy' && (
|
||||
<StrategyDeletedChange
|
||||
discard={
|
||||
<ConditionallyRender
|
||||
condition={showDiscard}
|
||||
show={<Discard onDiscard={onDiscard} />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{hasNameField(change.payload) && (
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
)}
|
||||
</StrategyDeletedChange>
|
||||
)}
|
||||
{change.action === 'updateStrategy' && (
|
||||
<>
|
||||
<StrategyEditedChange
|
||||
discard={
|
||||
<ConditionallyRender
|
||||
condition={showDiscard}
|
||||
show={<Discard onDiscard={onDiscard} />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
</StrategyEditedChange>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</StyledSingleChangeBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChangeRequest: VFC<IChangeRequestProps> = ({
|
||||
changeRequest,
|
||||
onRefetch,
|
||||
onNavigate,
|
||||
}) => {
|
||||
const { discardChange } = useChangeRequestApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const onDiscard = (id: number) => async () => {
|
||||
try {
|
||||
await discardChange(changeRequest.project, changeRequest.id, id);
|
||||
setToastData({
|
||||
title: 'Change discarded from change request draft.',
|
||||
type: 'success',
|
||||
});
|
||||
onRefetch?.();
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{changeRequest.features?.map(feature => (
|
||||
<ChangeRequestFeatureToggleChange
|
||||
<FeatureToggleChanges
|
||||
key={feature.name}
|
||||
featureName={feature.name}
|
||||
projectId={changeRequest.project}
|
||||
@ -225,14 +29,20 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
|
||||
{feature.changes.map((change, index) => (
|
||||
<Change
|
||||
key={index}
|
||||
onDiscard={onDiscard(change.id)}
|
||||
discard={
|
||||
<DiscardContainer
|
||||
changeRequest={changeRequest}
|
||||
changeId={change.id}
|
||||
onPostDiscard={onRefetch}
|
||||
/>
|
||||
}
|
||||
index={index}
|
||||
changeRequest={changeRequest}
|
||||
change={change}
|
||||
feature={feature}
|
||||
/>
|
||||
))}
|
||||
</ChangeRequestFeatureToggleChange>
|
||||
</FeatureToggleChanges>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
@ -0,0 +1,144 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import {
|
||||
hasNameField,
|
||||
IChange,
|
||||
IChangeRequest,
|
||||
IChangeRequestFeature,
|
||||
} from '../../../changeRequest.types';
|
||||
import { objectId } from 'utils/objectId';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Alert, Box, styled } from '@mui/material';
|
||||
|
||||
import {
|
||||
CodeSnippetPopover,
|
||||
PopoverDiff,
|
||||
} from '../../CodeSnippetPopover/CodeSnippetPopover';
|
||||
import { StrategyExecution } from '../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
||||
import { ToggleStatusChange } from './ToggleStatusChange';
|
||||
import {
|
||||
StrategyAddedChange,
|
||||
StrategyDeletedChange,
|
||||
StrategyEditedChange,
|
||||
} from './StrategyChange';
|
||||
|
||||
const StyledSingleChangeBox = styled(Box, {
|
||||
shouldForwardProp: (prop: string) => !prop.startsWith('$'),
|
||||
})<{
|
||||
$hasConflict: boolean;
|
||||
$isAfterWarning: boolean;
|
||||
$isLast: boolean;
|
||||
$isInConflictFeature: boolean;
|
||||
}>(
|
||||
({
|
||||
theme,
|
||||
$hasConflict,
|
||||
$isInConflictFeature,
|
||||
$isAfterWarning,
|
||||
$isLast,
|
||||
}) => ({
|
||||
borderLeft: '1px solid',
|
||||
borderRight: '1px solid',
|
||||
borderTop: '1px solid',
|
||||
borderBottom: $isLast ? '1px solid' : 'none',
|
||||
borderRadius: $isLast
|
||||
? `0 0
|
||||
${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`
|
||||
: 0,
|
||||
borderColor:
|
||||
$hasConflict || $isInConflictFeature
|
||||
? theme.palette.warning.border
|
||||
: theme.palette.dividerAlternative,
|
||||
borderTopColor:
|
||||
($hasConflict || $isAfterWarning) && !$isInConflictFeature
|
||||
? theme.palette.warning.border
|
||||
: theme.palette.dividerAlternative,
|
||||
})
|
||||
);
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
borderRadius: 0,
|
||||
padding: theme.spacing(0, 2),
|
||||
'&.MuiAlert-standardWarning': {
|
||||
borderStyle: 'none none solid none',
|
||||
},
|
||||
}));
|
||||
|
||||
export const Change: FC<{
|
||||
discard: ReactNode;
|
||||
index: number;
|
||||
changeRequest: IChangeRequest;
|
||||
change: IChange;
|
||||
feature: IChangeRequestFeature;
|
||||
}> = ({ index, change, feature, changeRequest, discard }) => {
|
||||
return (
|
||||
<StyledSingleChangeBox
|
||||
key={objectId(change)}
|
||||
$hasConflict={Boolean(change.conflict)}
|
||||
$isInConflictFeature={Boolean(feature.conflict)}
|
||||
$isAfterWarning={Boolean(feature.changes[index - 1]?.conflict)}
|
||||
$isLast={index + 1 === feature.changes.length}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(change.conflict) && !feature.conflict}
|
||||
show={
|
||||
<StyledAlert severity="warning">
|
||||
<strong>Conflict!</strong> This change can’t be applied.{' '}
|
||||
{change.conflict}.
|
||||
</StyledAlert>
|
||||
}
|
||||
/>
|
||||
<Box sx={{ p: 2 }}>
|
||||
{change.action === 'updateEnabled' && (
|
||||
<ToggleStatusChange
|
||||
enabled={change.payload.enabled}
|
||||
discard={discard}
|
||||
/>
|
||||
)}
|
||||
{change.action === 'addStrategy' && (
|
||||
<>
|
||||
<StrategyAddedChange discard={discard}>
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
</StrategyAddedChange>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
</>
|
||||
)}
|
||||
{change.action === 'deleteStrategy' && (
|
||||
<StrategyDeletedChange discard={discard}>
|
||||
{hasNameField(change.payload) && (
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
)}
|
||||
</StrategyDeletedChange>
|
||||
)}
|
||||
{change.action === 'updateStrategy' && (
|
||||
<>
|
||||
<StrategyEditedChange discard={discard}>
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
</StrategyEditedChange>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</StyledSingleChangeBox>
|
||||
);
|
||||
};
|
@ -0,0 +1,74 @@
|
||||
import React, { FC } from 'react';
|
||||
import { IChangeRequest } from '../../../changeRequest.types';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||
import { changesCount } from '../../../changesCount';
|
||||
import { Box, Link, styled } from '@mui/material';
|
||||
|
||||
const useShowDiscard = (changeRequest: IChangeRequest) => {
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(
|
||||
changeRequest.project
|
||||
);
|
||||
const allowChangeRequestActions = isChangeRequestConfigured(
|
||||
changeRequest.environment
|
||||
);
|
||||
const isPending = !['Cancelled', 'Applied'].includes(changeRequest.state);
|
||||
|
||||
const { user } = useAuthUser();
|
||||
const isAuthor = user?.id === changeRequest.createdBy.id;
|
||||
|
||||
const showDiscard =
|
||||
allowChangeRequestActions &&
|
||||
isPending &&
|
||||
isAuthor &&
|
||||
changesCount(changeRequest) > 1;
|
||||
|
||||
return showDiscard;
|
||||
};
|
||||
|
||||
const StyledLink = styled(Link)(() => ({
|
||||
textDecoration: 'none',
|
||||
'&:hover, &:focus': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
const Discard: FC<{ onDiscard: () => void }> = ({ onDiscard }) => (
|
||||
<Box>
|
||||
<StyledLink onClick={onDiscard}>Discard</StyledLink>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const DiscardContainer: FC<{
|
||||
changeRequest: IChangeRequest;
|
||||
changeId: number;
|
||||
onPostDiscard?: () => void;
|
||||
}> = ({ changeRequest, changeId, onPostDiscard }) => {
|
||||
const showDiscard = useShowDiscard(changeRequest);
|
||||
const { discardChange } = useChangeRequestApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const onDiscard = (id: number) => async () => {
|
||||
try {
|
||||
await discardChange(changeRequest.project, changeRequest.id, id);
|
||||
setToastData({
|
||||
title: 'Change discarded from change request draft.',
|
||||
type: 'success',
|
||||
});
|
||||
onPostDiscard?.();
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={showDiscard}
|
||||
show={<Discard onDiscard={onDiscard(changeId)} />}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,10 +1,6 @@
|
||||
import { Box, Link, styled, Typography } from '@mui/material';
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
interface IStrategyChangeProps {
|
||||
onDiscard: () => void;
|
||||
}
|
||||
|
||||
export const ChangeItemWrapper = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
@ -16,19 +12,6 @@ const ChangeItemInfo: FC = styled(Box)(({ theme }) => ({
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link)(() => ({
|
||||
textDecoration: 'none',
|
||||
'&:hover, &:focus': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
export const Discard: FC<IStrategyChangeProps> = ({ onDiscard }) => (
|
||||
<Box>
|
||||
<StyledLink onClick={onDiscard}>Discard</StyledLink>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const StrategyAddedChange: FC<{ discard?: ReactNode }> = ({
|
||||
children,
|
||||
discard,
|
@ -3,16 +3,20 @@ import { Link } from 'react-router-dom';
|
||||
import { Alert, Box, Card, Typography } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface IChangeRequestToggleChange {
|
||||
interface IFeatureToggleChanges {
|
||||
featureName: string;
|
||||
projectId: string;
|
||||
conflict?: string;
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
export const ChangeRequestFeatureToggleChange: FC<
|
||||
IChangeRequestToggleChange
|
||||
> = ({ featureName, projectId, conflict, onNavigate, children }) => (
|
||||
export const FeatureToggleChanges: FC<IFeatureToggleChanges> = ({
|
||||
featureName,
|
||||
projectId,
|
||||
conflict,
|
||||
onNavigate,
|
||||
children,
|
||||
}) => (
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={theme => ({
|
Loading…
Reference in New Issue
Block a user