Skip to content

Commit

Permalink
feat: variant dependencies ui (#6739)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Mar 29, 2024
1 parent 9eee654 commit 7f043c7
Show file tree
Hide file tree
Showing 15 changed files with 437 additions and 238 deletions.
Expand Up @@ -42,8 +42,9 @@ export const DependencyChange: VFC<{
>
{change.payload.feature}
</StyledLink>
{change.payload.enabled === false
? ' (disabled)'
{!change.payload.enabled ? ' (disabled)' : null}
{change.payload.variants?.length
? `(${change.payload.variants?.join(', ')})`
: null}
</AddDependencyWrapper>
{actions}
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/component/changeRequest/changeRequest.types.ts
Expand Up @@ -223,7 +223,11 @@ type ChangeRequestVariantPatch = {

type ChangeRequestEnabled = { enabled: boolean };

type ChangeRequestAddDependency = { feature: string; enabled: boolean };
type ChangeRequestAddDependency = {
feature: string;
enabled: boolean;
variants?: string[];
};

export type ChangeRequestAddStrategy = Pick<
IFeatureStrategy,
Expand Down
Expand Up @@ -91,7 +91,7 @@ test('Edit dependency', async () => {
<AddDependencyDialogue
project='default'
featureId='child'
parentFeatureId='parentB'
parentDependency={{ feature: 'parentB' }}
showDependencyDialogue={true}
onClose={() => {
closed = true;
Expand Down
278 changes: 84 additions & 194 deletions frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx
@@ -1,205 +1,60 @@
import { type FC, useState } from 'react';
import { Box, styled, Typography } from '@mui/material';
import { useEffect, useState } from 'react';
import { Box, Typography } from '@mui/material';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
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';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { DependenciesUpgradeAlert } from './DependenciesUpgradeAlert';
import { useUiFlag } from 'hooks/useUiFlag';
import type { IDependency } from '../../../interfaces/featureToggle';
import { ParentVariantOptions } from './ParentVariantOptions';
import { type ParentValue, REMOVE_DEPENDENCY_OPTION } from './constants';
import { FeatureStatusOptions } from './FeatureStatusOptions';
import { useManageDependency } from './useManageDependency';
import { LazyParentOptions } from './LazyParentOptions';

interface IAddDependencyDialogueProps {
project: string;
featureId: string;
parentFeatureId?: string;
parentFeatureValue?: ParentValue;
parentDependency?: IDependency;
showDependencyDialogue: boolean;
onClose: () => void;
}

const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
marginTop: theme.spacing(2),
marginBottom: theme.spacing(1.5),
}));

const REMOVE_DEPENDENCY_OPTION = {
key: 'none (remove dependency)',
label: 'none (remove dependency)',
};

// Project can have 100s of parents. We want to read them only when the modal for dependencies opens.
const LazyOptions: FC<{
project: string;
featureId: string;
parent: string;
onSelect: (parent: string) => void;
}> = ({ project, featureId, parent, onSelect }) => {
const { parentOptions } = useParentOptions(project, featureId);

const options = parentOptions
? [
REMOVE_DEPENDENCY_OPTION,
...parentOptions.map((parent) => ({
key: parent,
label: parent,
})),
]
: [REMOVE_DEPENDENCY_OPTION];
return (
<StyledSelect
fullWidth
options={options}
value={parent}
onChange={onSelect}
/>
);
};

const FeatureValueOptions: FC<{
parentValue: ParentValue;
onSelect: (parent: string) => void;
}> = ({ onSelect, parentValue }) => {
return (
<StyledSelect
fullWidth
options={[
{ key: 'enabled', label: 'enabled' },
{ key: 'disabled', label: 'disabled' },
]}
value={parentValue.status}
onChange={onSelect}
/>
);
};

type ParentValue = { status: 'enabled' } | { status: 'disabled' };

const useManageDependency = (
project: string,
featureId: string,
parent: string,
parentValue: ParentValue,
onClose: () => void,
) => {
const { trackEvent } = usePlausibleTracker();
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' | 'deleteDependency',
) => {
if (!environment) {
console.error('No change request environment');
return;
}
if (actionType === 'addDependency') {
await addChange(project, environment, [
{
action: actionType,
feature: featureId,
payload: {
feature: parent,
enabled: parentValue.status !== 'disabled',
},
},
]);
trackEvent('dependent_features', {
props: {
eventType: 'dependency added',
},
});
}
if (actionType === 'deleteDependency') {
await addChange(project, environment, [
{ action: actionType, feature: featureId, payload: undefined },
]);
}
void refetchChangeRequests();
setToastData({
text:
actionType === 'addDependency'
? `${featureId} will depend on ${parent}`
: `${featureId} dependency will be removed`,
type: 'success',
title: 'Change added to a draft',
});
};

return async () => {
try {
if (isChangeRequestConfiguredInAnyEnv()) {
const actionType =
parent === REMOVE_DEPENDENCY_OPTION.key
? 'deleteDependency'
: 'addDependency';
await handleAddChange(actionType);
trackEvent('dependent_features', {
props: {
eventType:
actionType === 'addDependency'
? 'add dependency added to change request'
: 'delete dependency added to change request',
},
});
} else if (parent === REMOVE_DEPENDENCY_OPTION.key) {
await removeDependencies(featureId);
trackEvent('dependent_features', {
props: {
eventType: 'dependency removed',
},
});
setToastData({ title: 'Dependency removed', type: 'success' });
} else {
await addDependency(featureId, {
feature: parent,
enabled: parentValue.status !== 'disabled',
});
trackEvent('dependent_features', {
props: {
eventType: 'dependency added',
},
});
setToastData({ title: 'Dependency added', type: 'success' });
}
} catch (error) {
setToastApiError(formatUnknownError(error));
}
void refetchFeature();
onClose();
};
};

export const AddDependencyDialogue = ({
project,
featureId,
parentFeatureId,
parentFeatureValue,
parentDependency,
showDependencyDialogue,
onClose,
}: IAddDependencyDialogueProps) => {
const [parent, setParent] = useState(
parentFeatureId || REMOVE_DEPENDENCY_OPTION.key,
parentDependency?.feature || REMOVE_DEPENDENCY_OPTION.key,
);

const getInitialParentValue = (): ParentValue => {
if (!parentDependency) return { status: 'enabled' };
if (parentDependency.variants?.length)
return {
status: 'enabled_with_variants',
variants: parentDependency.variants,
};
if (parentDependency.enabled === false) return { status: 'disabled' };
return { status: 'enabled' };
};
const [parentValue, setParentValue] = useState<ParentValue>(
parentFeatureValue || { status: 'enabled' },
getInitialParentValue,
);
const handleClick = useManageDependency(

const resetState = () => {
setParent(parentDependency?.feature || REMOVE_DEPENDENCY_OPTION.key);
setParentValue(getInitialParentValue());
};

useEffect(() => {
resetState();
}, [JSON.stringify(parentDependency)]);

const manageDependency = useManageDependency(
project,
featureId,
parent,
Expand All @@ -210,13 +65,38 @@ export const AddDependencyDialogue = ({
useChangeRequestsEnabled(project);

const variantDependenciesEnabled = useUiFlag('variantDependencies');
const showStatus =
parent !== REMOVE_DEPENDENCY_OPTION.key && variantDependenciesEnabled;
const showVariants =
parent !== REMOVE_DEPENDENCY_OPTION.key &&
variantDependenciesEnabled &&
parentValue.status === 'enabled_with_variants';

const selectStatus = (value: string) => {
if (value === 'enabled' || value === 'disabled') {
setParentValue({ status: value });
}
if (value === 'enabled_with_variants') {
setParentValue({
status: value,
variants: [],
});
}
};

const selectVariants = (variants: string[]) => {
setParentValue({
status: 'enabled_with_variants',
variants,
});
};

return (
<Dialogue
open={showDependencyDialogue}
title='Add parent feature dependency'
onClose={onClose}
onClick={handleClick}
onClick={manageDependency}
primaryButtonText={
isChangeRequestConfiguredInAnyEnv()
? 'Add change to draft'
Expand Down Expand Up @@ -245,7 +125,7 @@ export const AddDependencyDialogue = ({
<ConditionallyRender
condition={showDependencyDialogue}
show={
<LazyOptions
<LazyParentOptions
project={project}
featureId={featureId}
parent={parent}
Expand All @@ -258,30 +138,40 @@ export const AddDependencyDialogue = ({
/>

<ConditionallyRender
condition={
parent !== REMOVE_DEPENDENCY_OPTION.key &&
variantDependenciesEnabled
}
condition={showStatus}
show={
<Box sx={{ mt: 2 }}>
<Typography>
What <b>feature status</b> do you want to depend
on?
</Typography>
<FeatureValueOptions
<FeatureStatusOptions
parentValue={parentValue}
onSelect={(value) =>
setParentValue({
status:
value === 'disabled'
? 'disabled'
: 'enabled',
})
}
onSelect={selectStatus}
/>
</Box>
}
/>

<ConditionallyRender
condition={showVariants}
show={
parentValue.status === 'enabled_with_variants' && (
<Box sx={{ mt: 2 }}>
<Typography>
What <b>variant</b> do you want to depend
on?
</Typography>
<ParentVariantOptions
parent={parent}
project={project}
selectedValues={parentValue.variants}
onSelect={selectVariants}
/>
</Box>
)
}
/>
</Box>
</Dialogue>
);
Expand Down

0 comments on commit 7f043c7

Please sign in to comment.