mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-23 01:16:27 +02:00
feat: Change request dependency UI (#4966)
This commit is contained in:
parent
7f61438095
commit
ab739eb6c3
@ -11,6 +11,9 @@ const setupApi = () => {
|
|||||||
flags: {
|
flags: {
|
||||||
dependentFeatures: true,
|
dependentFeatures: true,
|
||||||
},
|
},
|
||||||
|
versionInfo: {
|
||||||
|
current: { oss: 'irrelevant', enterprise: 'some value' },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
testServerRoute(
|
testServerRoute(
|
||||||
@ -34,6 +37,26 @@ const setupApi = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setupChangeRequestApi = () => {
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/projects/default/change-requests/config',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
environment: 'development',
|
||||||
|
type: 'development',
|
||||||
|
requiredApprovals: null,
|
||||||
|
changeRequestEnabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'api/admin/projects/default/change-requests/pending',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
test('Delete dependency', async () => {
|
test('Delete dependency', async () => {
|
||||||
let closed = false;
|
let closed = false;
|
||||||
setupApi();
|
setupApi();
|
||||||
@ -95,3 +118,27 @@ test('Add dependency', async () => {
|
|||||||
expect(closed).toBe(true);
|
expect(closed).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Add change to draft', async () => {
|
||||||
|
let closed = false;
|
||||||
|
setupApi();
|
||||||
|
setupChangeRequestApi();
|
||||||
|
render(
|
||||||
|
<AddDependencyDialogue
|
||||||
|
project='default'
|
||||||
|
featureId='child'
|
||||||
|
showDependencyDialogue={true}
|
||||||
|
onClose={() => {
|
||||||
|
closed = true;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const addChangeToDraft = await screen.findByText('Add change to draft');
|
||||||
|
|
||||||
|
userEvent.click(addChangeToDraft);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(closed).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -6,6 +6,12 @@ import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesA
|
|||||||
import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions';
|
import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
|
||||||
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
interface IAddDependencyDialogueProps {
|
interface IAddDependencyDialogueProps {
|
||||||
project: string;
|
project: string;
|
||||||
@ -52,6 +58,80 @@ const LazyOptions: FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useManageDependency = (
|
||||||
|
project: string,
|
||||||
|
featureId: string,
|
||||||
|
parent: string,
|
||||||
|
onClose: () => void,
|
||||||
|
) => {
|
||||||
|
const { addChange } = useChangeRequestApi();
|
||||||
|
const { refetch: refetchChangeRequests } =
|
||||||
|
usePendingChangeRequests(project);
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { refetchFeature } = useFeature(project, featureId);
|
||||||
|
const environment = useHighestPermissionChangeRequestEnvironment(project)();
|
||||||
|
const { isChangeRequestConfiguredInAnyEnv } =
|
||||||
|
useChangeRequestsEnabled(project);
|
||||||
|
const { addDependency, removeDependencies } =
|
||||||
|
useDependentFeaturesApi(project);
|
||||||
|
|
||||||
|
const handleAddChange = async (
|
||||||
|
actionType: 'addDependency' | 'deleteDependencies',
|
||||||
|
) => {
|
||||||
|
if (!environment) {
|
||||||
|
console.error('No change request environment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actionType === 'addDependency') {
|
||||||
|
await addChange(project, environment, [
|
||||||
|
{
|
||||||
|
action: actionType,
|
||||||
|
feature: featureId,
|
||||||
|
payload: { feature: parent },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (actionType === 'deleteDependencies') {
|
||||||
|
await addChange(project, environment, [
|
||||||
|
{ action: actionType, feature: featureId, payload: undefined },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
refetchChangeRequests();
|
||||||
|
setToastData({
|
||||||
|
text:
|
||||||
|
actionType === 'addDependency'
|
||||||
|
? `${featureId} will depend on ${parent}`
|
||||||
|
: `${featureId} dependency will be removed`,
|
||||||
|
type: 'success',
|
||||||
|
title: 'Change added to a draft',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const manageDependency = async () => {
|
||||||
|
try {
|
||||||
|
if (isChangeRequestConfiguredInAnyEnv()) {
|
||||||
|
await handleAddChange(
|
||||||
|
parent === REMOVE_DEPENDENCY_OPTION.key
|
||||||
|
? 'deleteDependencies'
|
||||||
|
: 'addDependency',
|
||||||
|
);
|
||||||
|
} else if (parent === REMOVE_DEPENDENCY_OPTION.key) {
|
||||||
|
await removeDependencies(featureId);
|
||||||
|
setToastData({ title: 'Dependency removed', type: 'success' });
|
||||||
|
} else {
|
||||||
|
await addDependency(featureId, { feature: parent });
|
||||||
|
setToastData({ title: 'Dependency added', type: 'success' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
await refetchFeature();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return manageDependency;
|
||||||
|
};
|
||||||
|
|
||||||
export const AddDependencyDialogue = ({
|
export const AddDependencyDialogue = ({
|
||||||
project,
|
project,
|
||||||
featureId,
|
featureId,
|
||||||
@ -59,27 +139,27 @@ export const AddDependencyDialogue = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}: IAddDependencyDialogueProps) => {
|
}: IAddDependencyDialogueProps) => {
|
||||||
const [parent, setParent] = useState(REMOVE_DEPENDENCY_OPTION.key);
|
const [parent, setParent] = useState(REMOVE_DEPENDENCY_OPTION.key);
|
||||||
const { addDependency, removeDependencies } =
|
const handleClick = useManageDependency(
|
||||||
useDependentFeaturesApi(project);
|
project,
|
||||||
|
featureId,
|
||||||
const { refetchFeature } = useFeature(project, featureId);
|
parent,
|
||||||
|
onClose,
|
||||||
|
);
|
||||||
|
const { isChangeRequestConfiguredInAnyEnv } =
|
||||||
|
useChangeRequestsEnabled(project);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialogue
|
<Dialogue
|
||||||
open={showDependencyDialogue}
|
open={showDependencyDialogue}
|
||||||
title='Add parent feature dependency'
|
title='Add parent feature dependency'
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onClick={async () => {
|
onClick={handleClick}
|
||||||
if (parent === REMOVE_DEPENDENCY_OPTION.key) {
|
|
||||||
await removeDependencies(featureId);
|
|
||||||
} else {
|
|
||||||
await addDependency(featureId, { feature: parent });
|
|
||||||
}
|
|
||||||
await refetchFeature();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
primaryButtonText={
|
primaryButtonText={
|
||||||
parent === REMOVE_DEPENDENCY_OPTION.key ? 'Remove' : 'Add'
|
isChangeRequestConfiguredInAnyEnv()
|
||||||
|
? 'Add change to draft'
|
||||||
|
: parent === REMOVE_DEPENDENCY_OPTION.key
|
||||||
|
? 'Remove'
|
||||||
|
: 'Add'
|
||||||
}
|
}
|
||||||
secondaryButtonText='Cancel'
|
secondaryButtonText='Cancel'
|
||||||
>
|
>
|
||||||
|
@ -11,7 +11,9 @@ export interface IChangeSchema {
|
|||||||
| 'patchVariant'
|
| 'patchVariant'
|
||||||
| 'reorderStrategy'
|
| 'reorderStrategy'
|
||||||
| 'archiveFeature'
|
| 'archiveFeature'
|
||||||
| 'updateSegment';
|
| 'updateSegment'
|
||||||
|
| 'addDependency'
|
||||||
|
| 'deleteDependencies';
|
||||||
payload: string | boolean | object | number | undefined;
|
payload: string | boolean | object | number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
import useToast from '../../../useToast';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { formatUnknownError } from '../../../../utils/formatUnknownError';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { DependentFeatureSchema } from '../../../../openapi';
|
import { DependentFeatureSchema } from '../../../../openapi';
|
||||||
|
|
||||||
@ -8,7 +7,6 @@ export const useDependentFeaturesApi = (project: string) => {
|
|||||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
});
|
});
|
||||||
const { setToastData, setToastApiError } = useToast();
|
|
||||||
|
|
||||||
const addDependency = async (
|
const addDependency = async (
|
||||||
childFeature: string,
|
childFeature: string,
|
||||||
@ -21,16 +19,7 @@ export const useDependentFeaturesApi = (project: string) => {
|
|||||||
body: JSON.stringify(parentFeaturePayload),
|
body: JSON.stringify(parentFeaturePayload),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
try {
|
await makeRequest(req.caller, req.id);
|
||||||
await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
setToastData({
|
|
||||||
title: 'Dependency added',
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeDependency = async (
|
const removeDependency = async (
|
||||||
@ -43,16 +32,7 @@ export const useDependentFeaturesApi = (project: string) => {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
try {
|
await makeRequest(req.caller, req.id);
|
||||||
await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
setToastData({
|
|
||||||
title: 'Dependency removed',
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeDependencies = async (childFeature: string) => {
|
const removeDependencies = async (childFeature: string) => {
|
||||||
@ -62,22 +42,12 @@ export const useDependentFeaturesApi = (project: string) => {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
try {
|
await makeRequest(req.caller, req.id);
|
||||||
await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
setToastData({
|
|
||||||
title: 'Dependencies removed',
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const callbackDeps = [
|
const callbackDeps = [
|
||||||
createRequest,
|
createRequest,
|
||||||
makeRequest,
|
makeRequest,
|
||||||
setToastData,
|
|
||||||
formatUnknownError,
|
formatUnknownError,
|
||||||
project,
|
project,
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user