diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx
index 4cca8ddd9f..8c51528caf 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx
@@ -14,11 +14,13 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
interface IEnvironmentAccordionBodyProps {
isDisabled: boolean;
featureEnvironment?: IFeatureEnvironment;
+ otherEnvironments?: IFeatureEnvironment['name'][];
}
const EnvironmentAccordionBody = ({
featureEnvironment,
isDisabled,
+ otherEnvironments,
}: IEnvironmentAccordionBodyProps) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@@ -93,6 +95,7 @@ const EnvironmentAccordionBody = ({
strategy={strategy}
index={index}
environmentName={featureEnvironment.name}
+ otherEnvironments={otherEnvironments}
/>
))}
>
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx
index 7465b6b3f6..080e1731e2 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx
@@ -1,6 +1,7 @@
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { MoveListItem, useDragItem } from 'hooks/useDragItem';
+import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { IFeatureStrategy } from 'interfaces/strategy';
import { StrategyItem } from './StrategyItem/StrategyItem';
@@ -8,6 +9,7 @@ interface IStrategyDraggableItemProps {
strategy: IFeatureStrategy;
environmentName: string;
index: number;
+ otherEnvironments?: IFeatureEnvironment['name'][];
onDragAndDrop: MoveListItem;
}
@@ -15,6 +17,7 @@ export const StrategyDraggableItem = ({
strategy,
index,
environmentName,
+ otherEnvironments,
onDragAndDrop,
}: IStrategyDraggableItemProps) => {
const ref = useDragItem(index, onDragAndDrop);
@@ -28,6 +31,7 @@ export const StrategyDraggableItem = ({
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/CopyStrategyIconMenu/CopyStrategyIconMenu.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/CopyStrategyIconMenu/CopyStrategyIconMenu.tsx
new file mode 100644
index 0000000000..e2950df9cd
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/CopyStrategyIconMenu/CopyStrategyIconMenu.tsx
@@ -0,0 +1,148 @@
+import { MouseEvent, useContext, useState, VFC } from 'react';
+import {
+ IconButton,
+ ListItemIcon,
+ ListItemText,
+ Menu,
+ MenuItem,
+ Tooltip,
+} from '@mui/material';
+import { AddToPhotos as CopyIcon, Lock } from '@mui/icons-material';
+import { IFeatureStrategy } from 'interfaces/strategy';
+import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { IFeatureEnvironment } from 'interfaces/featureToggle';
+import AccessContext from 'contexts/AccessContext';
+import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
+import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
+import useToast from 'hooks/useToast';
+import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
+import { formatUnknownError } from 'utils/formatUnknownError';
+
+interface ICopyStrategyIconMenuProps {
+ environments: IFeatureEnvironment['name'][];
+ strategy: IFeatureStrategy;
+}
+
+export const CopyStrategyIconMenu: VFC = ({
+ environments,
+ strategy,
+}) => {
+ const projectId = useRequiredPathParam('projectId');
+ const featureId = useRequiredPathParam('featureId');
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+ const { addStrategyToFeature } = useFeatureStrategyApi();
+ const { setToastData, setToastApiError } = useToast();
+ const { refetchFeature } = useFeature(projectId, featureId);
+ const { refetchFeature: refetchFeatureImmutable } = useFeatureImmutable(
+ projectId,
+ featureId
+ );
+ const handleClick = (event: MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+ const { hasAccess } = useContext(AccessContext);
+ const onClick = async (environmentId: string) => {
+ const { id, ...strategyCopy } = {
+ ...strategy,
+ environment: environmentId,
+ };
+
+ try {
+ await addStrategyToFeature(
+ projectId,
+ featureId,
+ environmentId,
+ strategyCopy
+ );
+ refetchFeature();
+ refetchFeatureImmutable();
+ setToastData({
+ title: `Strategy created`,
+ text: `Successfully copied a strategy to ${environmentId}`,
+ type: 'success',
+ });
+ } catch (error) {
+ setToastApiError(formatUnknownError(error));
+ }
+ handleClose();
+ };
+
+ const enabled = environments.some(environment =>
+ hasAccess(CREATE_FEATURE_STRATEGY, projectId, environment)
+ );
+
+ return (
+
+ );
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx
index f44f82ce6a..be80809709 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx
@@ -1,6 +1,7 @@
import { DragIndicator, Edit } from '@mui/icons-material';
import { styled, useTheme, IconButton } from '@mui/material';
import { Link } from 'react-router-dom';
+import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { IFeatureStrategy } from 'interfaces/strategy';
import {
getFeatureStrategyIcon,
@@ -13,13 +14,15 @@ import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/Feature
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
-import { useStyles } from './StrategyItem.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
+import { useStyles } from './StrategyItem.styles';
interface IStrategyItemProps {
environmentId: string;
strategy: IFeatureStrategy;
isDraggable?: boolean;
+ otherEnvironments?: IFeatureEnvironment['name'][];
}
const DragIcon = styled(IconButton)(({ theme }) => ({
@@ -32,6 +35,7 @@ export const StrategyItem = ({
environmentId,
strategy,
isDraggable,
+ otherEnvironments,
}: IStrategyItemProps) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@@ -67,6 +71,17 @@ export const StrategyItem = ({
text={formatStrategyName(strategy.name)}
/>
+
0
+ )}
+ show={() => (
+
+ )}
+ />
name)
+ .filter(name => name !== env.name)}
/>