= ({
onChange={e => setName(e.target.value)}
error={Boolean(errors.name)}
errorText={errors.name}
- onFocus={() => clearErrors()}
autoFocus
required
+ data-test={SEGMENT_NAME_ID}
/>
What is the segment description?
@@ -55,7 +60,7 @@ export const SegmentFormStepOne: React.FC = ({
onChange={e => setDescription(e.target.value)}
error={Boolean(errors.description)}
errorText={errors.description}
- onFocus={() => clearErrors()}
+ data-test={SEGMENT_DESC_ID}
/>
@@ -64,7 +69,8 @@ export const SegmentFormStepOne: React.FC
= ({
variant="contained"
color="primary"
onClick={() => setCurrentStep(2)}
- disabled={name.length === 0}
+ disabled={name.length === 0 || Boolean(errors.name)}
+ data-test={SEGMENT_NEXT_BTN_ID}
>
Next
diff --git a/frontend/src/component/segments/SegmentList/SegmentList.tsx b/frontend/src/component/segments/SegmentList/SegmentList.tsx
index 2f3e763c91..281dea24f3 100644
--- a/frontend/src/component/segments/SegmentList/SegmentList.tsx
+++ b/frontend/src/component/segments/SegmentList/SegmentList.tsx
@@ -28,6 +28,7 @@ import PageContent from 'component/common/PageContent';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { SegmentDelete } from '../SegmentDelete/SegmentDelete';
import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs';
+import { NAVIGATE_TO_CREATE_SEGMENT } from 'utils/testIds';
export const SegmentsList = () => {
const history = useHistory();
@@ -101,6 +102,7 @@ export const SegmentsList = () => {
history.push('/segments/create')}
permission={CREATE_SEGMENT}
+ data-test={NAVIGATE_TO_CREATE_SEGMENT}
>
New Segment
diff --git a/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.tsx b/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.tsx
index 1d0289ab0c..f948cf851e 100644
--- a/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.tsx
+++ b/frontend/src/component/segments/SegmentList/SegmentListItem/SegmentListItem.tsx
@@ -9,6 +9,7 @@ import PermissionIconButton from 'component/common/PermissionIconButton/Permissi
import TimeAgo from 'react-timeago';
import { ISegment } from 'interfaces/segment';
import { useHistory } from 'react-router-dom';
+import { SEGMENT_DELETE_BTN_ID } from 'utils/testIds';
interface ISegmentListItemProps {
id: number;
@@ -82,6 +83,7 @@ export const SegmentListItem = ({
setDelDialog(true);
}}
permission={ADMIN}
+ data-test={`${SEGMENT_DELETE_BTN_ID}_${name}`}
>
diff --git a/frontend/src/component/segments/hooks/useSegmentForm.ts b/frontend/src/component/segments/hooks/useSegmentForm.ts
index 2e81eb0c32..c01caa26e9 100644
--- a/frontend/src/component/segments/hooks/useSegmentForm.ts
+++ b/frontend/src/component/segments/hooks/useSegmentForm.ts
@@ -1,5 +1,6 @@
import { IConstraint } from 'interfaces/strategy';
import { useEffect, useState } from 'react';
+import { useSegmentValidation } from 'hooks/api/getters/useSegmentValidation/useSegmentValidation';
export const useSegmentForm = (
initialName = '',
@@ -11,6 +12,7 @@ export const useSegmentForm = (
const [constraints, setConstraints] =
useState(initialConstraints);
const [errors, setErrors] = useState({});
+ const nameError = useSegmentValidation(name, initialName);
useEffect(() => {
setName(initialName);
@@ -25,6 +27,13 @@ export const useSegmentForm = (
// eslint-disable-next-line
}, [JSON.stringify(initialConstraints)]);
+ useEffect(() => {
+ setErrors(errors => ({
+ ...errors,
+ name: nameError,
+ }));
+ }, [nameError]);
+
const getSegmentPayload = () => {
return {
name,
diff --git a/frontend/src/hooks/api/getters/useSegmentValidation/useSegmentValidation.ts b/frontend/src/hooks/api/getters/useSegmentValidation/useSegmentValidation.ts
new file mode 100644
index 0000000000..9b865a5f4d
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useSegmentValidation/useSegmentValidation.ts
@@ -0,0 +1,40 @@
+import { useEffect, useState } from 'react';
+import { formatApiPath } from 'utils/formatPath';
+
+export const useSegmentValidation = (
+ name: string,
+ initialName: string
+): string | undefined => {
+ const [error, setError] = useState();
+ const nameHasChanged = name !== initialName;
+
+ useEffect(() => {
+ setError(undefined);
+ if (name && nameHasChanged) {
+ fetchNewNameValidation(name)
+ .then(parseValidationResponse)
+ .then(setError)
+ .catch(() => setError(undefined));
+ }
+ }, [name, nameHasChanged]);
+
+ return error;
+};
+
+const fetchNewNameValidation = (name: string): Promise =>
+ fetch(formatApiPath('api/admin/segments/validate'), {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name }),
+ });
+
+const parseValidationResponse = async (
+ res: Response
+): Promise => {
+ if (res.ok) {
+ return;
+ }
+
+ const json = await res.json();
+ return json.details[0].message;
+};
diff --git a/frontend/src/utils/testIds.ts b/frontend/src/utils/testIds.ts
index ec8b5f8515..7d0e9c539d 100644
--- a/frontend/src/utils/testIds.ts
+++ b/frontend/src/utils/testIds.ts
@@ -1,5 +1,6 @@
/* NAVIGATION */
export const NAVIGATE_TO_CREATE_FEATURE = 'NAVIGATE_TO_CREATE_FEATURE';
+export const NAVIGATE_TO_CREATE_SEGMENT = 'NAVIGATE_TO_CREATE_SEGMENT';
export const CREATE_API_TOKEN_BUTTON = 'CREATE_API_TOKEN_BUTTON';
/* CREATE FEATURE */
@@ -8,6 +9,15 @@ export const CF_TYPE_ID = 'CF_TYPE_ID';
export const CF_DESC_ID = 'CF_DESC_ID';
export const CF_CREATE_BTN_ID = 'CF_CREATE_BTN_ID';
+/* SEGMENT */
+export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID';
+export const SEGMENT_DESC_ID = 'SEGMENT_DESC_ID';
+export const SEGMENT_NEXT_BTN_ID = 'SEGMENT_NEXT_BTN_ID';
+export const SEGMENT_CREATE_BTN_ID = 'SEGMENT_CREATE_BTN_ID';
+export const SEGMENT_SAVE_BTN_ID = 'SEGMENT_SAVE_BTN_ID';
+export const SEGMENT_DELETE_BTN_ID = 'SEGMENT_DELETE_BTN_ID';
+export const SEGMENT_DIALOG_NAME_ID = 'SEGMENT_DIALOG_NAME_ID';
+
/* LOGIN */
export const LOGIN_EMAIL_ID = 'LOGIN_EMAIL_ID';
export const LOGIN_BUTTON = 'LOGIN_BUTTON';
diff --git a/frontend/vercel.json b/frontend/vercel.json
index d924b1a1e0..e2941effb6 100644
--- a/frontend/vercel.json
+++ b/frontend/vercel.json
@@ -1,7 +1,16 @@
{
"rewrites": [
- { "source": "/api/:match*", "destination": "https://unleash.herokuapp.com/api/:match*" },
- { "source": "/logout", "destination": "https://unleash.herokuapp.com/logout" },
- { "source": "/auth/:match*", "destination": "https://unleash.herokuapp.com/auth/:match*" }
+ {
+ "source": "/api/:match*",
+ "destination": "https://unleash4.herokuapp.com/api/:match*"
+ },
+ {
+ "source": "/logout",
+ "destination": "https://unleash4.herokuapp.com/logout"
+ },
+ {
+ "source": "/auth/:match*",
+ "destination": "https://unleash4.herokuapp.com/auth/:match*"
+ }
]
}