From ab739eb6c33a423951f1aa46669d1a7ab37dfa75 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 9 Oct 2023 14:06:00 +0200 Subject: [PATCH] feat: Change request dependency UI (#4966) --- .../AddDependencyDialogue.test.tsx | 47 ++++++++ .../Dependencies/AddDependencyDialogue.tsx | 108 +++++++++++++++--- .../useChangeRequestApi.ts | 4 +- .../useDependentFeaturesApi.ts | 38 +----- 4 files changed, 148 insertions(+), 49 deletions(-) diff --git a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.test.tsx b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.test.tsx index 59435b099f1a..c7d10f733209 100644 --- a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.test.tsx +++ b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.test.tsx @@ -11,6 +11,9 @@ const setupApi = () => { flags: { dependentFeatures: true, }, + versionInfo: { + current: { oss: 'irrelevant', enterprise: 'some value' }, + }, }); 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 () => { let closed = false; setupApi(); @@ -95,3 +118,27 @@ test('Add dependency', async () => { expect(closed).toBe(true); }); }); + +test('Add change to draft', async () => { + let closed = false; + setupApi(); + setupChangeRequestApi(); + render( + { + closed = true; + }} + />, + ); + + const addChangeToDraft = await screen.findByText('Add change to draft'); + + userEvent.click(addChangeToDraft); + + await waitFor(() => { + expect(closed).toBe(true); + }); +}); diff --git a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx index e0a8b6f8382e..00a2ca840814 100644 --- a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx +++ b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx @@ -6,6 +6,12 @@ import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesA import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; 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 { 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 = ({ project, featureId, @@ -59,27 +139,27 @@ export const AddDependencyDialogue = ({ onClose, }: IAddDependencyDialogueProps) => { const [parent, setParent] = useState(REMOVE_DEPENDENCY_OPTION.key); - const { addDependency, removeDependencies } = - useDependentFeaturesApi(project); - - const { refetchFeature } = useFeature(project, featureId); + const handleClick = useManageDependency( + project, + featureId, + parent, + onClose, + ); + const { isChangeRequestConfiguredInAnyEnv } = + useChangeRequestsEnabled(project); return ( { - if (parent === REMOVE_DEPENDENCY_OPTION.key) { - await removeDependencies(featureId); - } else { - await addDependency(featureId, { feature: parent }); - } - await refetchFeature(); - onClose(); - }} + onClick={handleClick} primaryButtonText={ - parent === REMOVE_DEPENDENCY_OPTION.key ? 'Remove' : 'Add' + isChangeRequestConfiguredInAnyEnv() + ? 'Add change to draft' + : parent === REMOVE_DEPENDENCY_OPTION.key + ? 'Remove' + : 'Add' } secondaryButtonText='Cancel' > diff --git a/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts b/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts index 5a99b60ec6bf..58e352d20771 100644 --- a/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts +++ b/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts @@ -11,7 +11,9 @@ export interface IChangeSchema { | 'patchVariant' | 'reorderStrategy' | 'archiveFeature' - | 'updateSegment'; + | 'updateSegment' + | 'addDependency' + | 'deleteDependencies'; payload: string | boolean | object | number | undefined; } diff --git a/frontend/src/hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi.ts b/frontend/src/hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi.ts index e2e89f1301a6..a28f9e4c8ac7 100644 --- a/frontend/src/hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi.ts +++ b/frontend/src/hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi.ts @@ -1,6 +1,5 @@ import useAPI from '../useApi/useApi'; -import useToast from '../../../useToast'; -import { formatUnknownError } from '../../../../utils/formatUnknownError'; +import { formatUnknownError } from 'utils/formatUnknownError'; import { useCallback } from 'react'; import { DependentFeatureSchema } from '../../../../openapi'; @@ -8,7 +7,6 @@ export const useDependentFeaturesApi = (project: string) => { const { makeRequest, createRequest, errors, loading } = useAPI({ propagateErrors: true, }); - const { setToastData, setToastApiError } = useToast(); const addDependency = async ( childFeature: string, @@ -21,16 +19,7 @@ export const useDependentFeaturesApi = (project: string) => { body: JSON.stringify(parentFeaturePayload), }, ); - try { - await makeRequest(req.caller, req.id); - - setToastData({ - title: 'Dependency added', - type: 'success', - }); - } catch (error) { - setToastApiError(formatUnknownError(error)); - } + await makeRequest(req.caller, req.id); }; const removeDependency = async ( @@ -43,16 +32,7 @@ export const useDependentFeaturesApi = (project: string) => { method: 'DELETE', }, ); - try { - await makeRequest(req.caller, req.id); - - setToastData({ - title: 'Dependency removed', - type: 'success', - }); - } catch (error) { - setToastApiError(formatUnknownError(error)); - } + await makeRequest(req.caller, req.id); }; const removeDependencies = async (childFeature: string) => { @@ -62,22 +42,12 @@ export const useDependentFeaturesApi = (project: string) => { method: 'DELETE', }, ); - try { - await makeRequest(req.caller, req.id); - - setToastData({ - title: 'Dependencies removed', - type: 'success', - }); - } catch (error) { - setToastApiError(formatUnknownError(error)); - } + await makeRequest(req.caller, req.id); }; const callbackDeps = [ createRequest, makeRequest, - setToastData, formatUnknownError, project, ];