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.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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.