From b85b3261040b1f91d2be05550f9a9e62488ad296 Mon Sep 17 00:00:00 2001
From: sighphyre <liquidwicked64@gmail.com>
Date: Fri, 26 Nov 2021 17:07:05 +0200
Subject: [PATCH] task: Add buttons for deleting/editing a constraint (#522)

* task: Add buttons for deleting/editing a constraint

* task: Improve look and feel of constraints buttons

- Make constraints fill their container
- Move constraint buttons to material ui buttons
- Move constraint buttons to top right of their container

* fix: adjust positioning

* fix: added project id to permissin button

* fix: add correct permission

* fix: update create feature path

Co-authored-by: Simon Hornby <simon@getunleash.ai>
Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
---
 .../feature-toggle/feature.spec.js            |  4 +-
 frontend/package.json                         |  1 +
 .../common/Constraint/Constraint.styles.ts    | 14 ++++-
 .../common/Constraint/Constraint.tsx          | 60 ++++++++++++++++---
 .../PermissionIconButton.tsx                  |  2 +-
 .../FeatureStrategyAccordionBody.styles.ts    |  6 +-
 .../FeatureStrategyAccordionBody.tsx          | 42 ++++++++-----
 7 files changed, 99 insertions(+), 30 deletions(-)

diff --git a/frontend/cypress/integration/feature-toggle/feature.spec.js b/frontend/cypress/integration/feature-toggle/feature.spec.js
index 404951534c..ea4d779eb6 100644
--- a/frontend/cypress/integration/feature-toggle/feature.spec.js
+++ b/frontend/cypress/integration/feature-toggle/feature.spec.js
@@ -82,7 +82,9 @@ describe('feature toggle', () => {
 
         cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click();
 
-        cy.intercept('POST', '/api/admin/features').as('createFeature');
+        cy.intercept('POST', '/api/admin/projects/default/features').as(
+            'createFeature'
+        );
 
         cy.get("[data-test='CF_NAME_ID'").type(featureToggleName);
         cy.get("[data-test='CF_DESC_ID'").type('hellowrdada');
diff --git a/frontend/package.json b/frontend/package.json
index c88647cee9..ecaf782a48 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -33,6 +33,7 @@
     "test": "react-scripts test",
     "prepare": "yarn run build",
     "e2e": "yarn run cypress open --config baseUrl='http://localhost:3000' --env PASSWORD_AUTH=true,AUTH_TOKEN=$AUTH_TOKEN",
+    "e2e:heroku": "yarn run cypress open --config baseUrl='http://localhost:3000' --env PASSWORD_AUTH=false,AUTH_TOKEN=$AUTH_TOKEN",
     "e2e:enterprise": "yarn run cypress open --config baseUrl='http://localhost:3000' --env PASSWORD_AUTH=true,ENTERPRISE=true,AUTH_TOKEN=$AUTH_TOKEN"
   },
   "devDependencies": {
diff --git a/frontend/src/component/common/Constraint/Constraint.styles.ts b/frontend/src/component/common/Constraint/Constraint.styles.ts
index d9b3834573..2541965ff9 100644
--- a/frontend/src/component/common/Constraint/Constraint.styles.ts
+++ b/frontend/src/component/common/Constraint/Constraint.styles.ts
@@ -11,11 +11,21 @@ export const useStyles = makeStyles(theme => ({
         alignItems: 'center',
         justifyContent: 'center',
         padding: '0.1rem 0.5rem',
-        border: `1px solid ${theme.palette.grey[300]}`,
-        borderRadius: '5px',
         fontSize: theme.fontSizes.smallBody,
         backgroundColor: theme.palette.grey[200],
         margin: '0.5rem 0',
+        position: 'relative',
+        borderRadius: '5px',
+    },
+    constraintBtn: {
+        color: theme.palette.primary.main,
+        fontWeight: 'normal',
+        marginBottom: '0.5rem',
+    },
+    btnContainer: {
+        position: 'absolute',
+        top: '6px',
+        right: 0,
     },
     column: {
         flexDirection: 'column',
diff --git a/frontend/src/component/common/Constraint/Constraint.tsx b/frontend/src/component/common/Constraint/Constraint.tsx
index de1c96c1cc..8b960fac12 100644
--- a/frontend/src/component/common/Constraint/Constraint.tsx
+++ b/frontend/src/component/common/Constraint/Constraint.tsx
@@ -1,31 +1,75 @@
+import { Delete, Edit } from '@material-ui/icons';
 import classnames from 'classnames';
+import { useParams } from 'react-router';
+import { IFeatureViewParams } from '../../../interfaces/params';
 import { IConstraint } from '../../../interfaces/strategy';
 import FeatureStrategiesSeparator from '../../feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesSeparator/FeatureStrategiesSeparator';
+import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
+import ConditionallyRender from '../ConditionallyRender';
+import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
 import StringTruncator from '../StringTruncator/StringTruncator';
 import { useStyles } from './Constraint.styles';
 
 interface IConstraintProps {
     constraint: IConstraint;
     className?: string;
+    deleteCallback?: () => void;
+    editCallback?: () => void;
 }
 
-const Constraint = ({ constraint, className, ...rest }: IConstraintProps) => {
+const Constraint = ({
+    constraint,
+    deleteCallback,
+    editCallback,
+    className,
+    ...rest
+}: IConstraintProps) => {
     const styles = useStyles();
+    const { projectId } = useParams<IFeatureViewParams>();
 
     const classes = classnames(styles.constraint, {
         [styles.column]: constraint.values.length > 2,
     });
 
+    const editable = !!(deleteCallback && editCallback);
+
     return (
         <div className={classes + ' ' + className} {...rest}>
-            <StringTruncator text={constraint.contextName} maxWidth="125" />
-            <FeatureStrategiesSeparator
-                text={constraint.operator}
-                maxWidth="none"
+            <div className={classes + ' ' + className} {...rest}>
+                <StringTruncator text={constraint.contextName} maxWidth="125" />
+                <FeatureStrategiesSeparator
+                    text={constraint.operator}
+                    maxWidth="none"
+                />
+                <span className={styles.values}>
+                    {constraint.values.join(', ')}
+                </span>
+            </div>
+
+            <ConditionallyRender
+                condition={editable}
+                show={
+                    <div className={styles.btnContainer}>
+                        <PermissionIconButton
+                            onClick={editCallback}
+                            tooltip="Edit strategy"
+                            permission={UPDATE_FEATURE}
+                            projectId={projectId}
+                        >
+                            <Edit />
+                        </PermissionIconButton>
+
+                        <PermissionIconButton
+                            onClick={deleteCallback}
+                            tooltip="Delete strategy"
+                            permission={UPDATE_FEATURE}
+                            projectId={projectId}
+                        >
+                            <Delete />
+                        </PermissionIconButton>
+                    </div>
+                }
             />
-            <span className={styles.values}>
-                {constraint.values.join(', ')}
-            </span>
         </div>
     );
 };
diff --git a/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx b/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx
index ac743785ef..3c02b8b654 100644
--- a/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx
+++ b/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx
@@ -5,7 +5,7 @@ import AccessContext from '../../../contexts/AccessContext';
 
 interface IPermissionIconButtonProps extends OverridableComponent<any> {
     permission: string;
-    Icon: React.ElementType;
+    Icon?: React.ElementType;
     tooltip: string;
     onClick?: (e: any) => void;
     projectId?: string;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategyAccordion/FeatureStrategyAccordionBody/FeatureStrategyAccordionBody.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategyAccordion/FeatureStrategyAccordionBody/FeatureStrategyAccordionBody.styles.ts
index 0dc423ddbf..399f20e3fa 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategyAccordion/FeatureStrategyAccordionBody/FeatureStrategyAccordionBody.styles.ts
+++ b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategyAccordion/FeatureStrategyAccordionBody/FeatureStrategyAccordionBody.styles.ts
@@ -7,7 +7,8 @@ export const useStyles = makeStyles(theme => ({
         marginBottom: '0.5rem',
     },
     accordionContainer: {
-        width: '80%',
+        width: '100%',
+        paddingRight: '37px',
         [theme.breakpoints.down(800)]: {
             width: '100%',
         },
@@ -20,7 +21,4 @@ export const useStyles = makeStyles(theme => ({
         marginTop: '0.5rem',
         fontSize: theme.fontSizes.smallBody,
     },
-    constraintBody: {
-        maxWidth: '350px',
-    },
 }));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategyAccordion/FeatureStrategyAccordionBody/FeatureStrategyAccordionBody.tsx b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategyAccordion/FeatureStrategyAccordionBody/FeatureStrategyAccordionBody.tsx
index 30266bfbc4..dc47742308 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategyAccordion/FeatureStrategyAccordionBody/FeatureStrategyAccordionBody.tsx
+++ b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategyAccordion/FeatureStrategyAccordionBody/FeatureStrategyAccordionBody.tsx
@@ -12,7 +12,6 @@ import { useContext, useState } from 'react';
 import ConditionallyRender from '../../../../../common/ConditionallyRender';
 import useUiConfig from '../../../../../../hooks/api/getters/useUiConfig/useUiConfig';
 import { C } from '../../../../../common/flags';
-import { Button } from '@material-ui/core';
 import { useStyles } from './FeatureStrategyAccordionBody.styles';
 import Dialogue from '../../../../../common/Dialogue';
 import DefaultStrategy from '../../common/DefaultStrategy/DefaultStrategy';
@@ -20,6 +19,9 @@ import { ADD_CONSTRAINT_ID } from '../../../../../../testIds';
 import AccessContext from '../../../../../../contexts/AccessContext';
 import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
 import Constraint from '../../../../../common/Constraint/Constraint';
+import PermissionButton from '../../../../../common/PermissionButton/PermissionButton';
+import { useParams } from 'react-router';
+import { IFeatureViewParams } from '../../../../../../interfaces/params';
 
 interface IFeatureStrategyAccordionBodyProps {
     strategy: IFeatureStrategy;
@@ -40,6 +42,7 @@ const FeatureStrategyAccordionBody: React.FC<IFeatureStrategyAccordionBodyProps>
         setStrategyConstraints,
     }) => {
         const styles = useStyles();
+        const { projectId } = useParams<IFeatureViewParams>();
         const [constraintError, setConstraintError] = useState({});
         const { strategies } = useStrategies();
         const { uiConfig } = useUiConfig();
@@ -106,13 +109,25 @@ const FeatureStrategyAccordionBody: React.FC<IFeatureStrategyAccordionBodyProps>
                 return (
                     <Constraint
                         constraint={constraint}
+                        editCallback={() => {
+                            setShowConstraints(true);
+                        }}
+                        deleteCallback={() => {
+                            removeConstraint(index);
+                        }}
                         key={`${constraint.contextName}-${index}`}
-                        className={styles.constraintBody}
                     />
                 );
             });
         };
 
+        const removeConstraint = (index: number) => {
+            const updatedConstraints = [...constraints];
+            updatedConstraints.splice(index, 1);
+
+            updateConstraints(updatedConstraints);
+        };
+
         const closeConstraintDialog = () => {
             setShowConstraints(false);
             const filteredConstraints = constraints.filter(constraint => {
@@ -137,18 +152,17 @@ const FeatureStrategyAccordionBody: React.FC<IFeatureStrategyAccordionBodyProps>
                                 Constraints
                             </p>
                             {renderConstraints()}
-                            <ConditionallyRender
-                                condition={hasAccess(UPDATE_FEATURE)}
-                                show={
-                                    <Button
-                                        className={styles.addConstraintBtn}
-                                        onClick={toggleConstraints}
-                                        data-test={ADD_CONSTRAINT_ID}
-                                    >
-                                        + Edit constraints
-                                    </Button>
-                                }
-                            />
+
+                            <PermissionButton
+                                className={styles.addConstraintBtn}
+                                onClick={toggleConstraints}
+                                variant={'text'}
+                                data-test={ADD_CONSTRAINT_ID}
+                                permission={UPDATE_FEATURE}
+                                projectId={projectId}
+                            >
+                                + Add constraints
+                            </PermissionButton>
                         </>
                     }
                 />