mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
feat: mark completed ui selector (#7025)

This commit is contained in:
parent
3fc7714e78
commit
3f983d061a
@ -12,17 +12,19 @@ interface ILegalValueTextProps {
|
||||
legal: ILegalValue;
|
||||
control: React.ReactElement;
|
||||
filter?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const LegalValueLabel = ({
|
||||
legal,
|
||||
control,
|
||||
filter,
|
||||
value,
|
||||
}: ILegalValueTextProps) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<FormControlLabel
|
||||
value={legal.value}
|
||||
value={value || legal.value}
|
||||
control={control}
|
||||
sx={{ width: '100%' }}
|
||||
label={
|
||||
|
@ -24,13 +24,7 @@ export const FeatureLifecycle: FC<{
|
||||
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
|
||||
const currentStage = populateCurrentStage(feature);
|
||||
|
||||
const { markFeatureCompleted, markFeatureUncompleted, loading } =
|
||||
useFeatureLifecycleApi();
|
||||
|
||||
const onCompleteHandler = async () => {
|
||||
await markFeatureCompleted(feature.name, feature.project);
|
||||
onComplete();
|
||||
};
|
||||
const { markFeatureUncompleted, loading } = useFeatureLifecycleApi();
|
||||
|
||||
const onUncompleteHandler = async () => {
|
||||
await markFeatureUncompleted(feature.name, feature.project);
|
||||
@ -41,7 +35,7 @@ export const FeatureLifecycle: FC<{
|
||||
<FeatureLifecycleTooltip
|
||||
stage={currentStage!}
|
||||
onArchive={onArchive}
|
||||
onComplete={onCompleteHandler}
|
||||
onComplete={onComplete}
|
||||
onUncomplete={onUncompleteHandler}
|
||||
loading={loading}
|
||||
>
|
||||
|
@ -0,0 +1,125 @@
|
||||
import { Box, Radio, RadioGroup, Typography } from '@mui/material';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import { LegalValueLabel } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel';
|
||||
import { useState } from 'react';
|
||||
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { SingleVariantOptions } from './SingleVariantOptions';
|
||||
|
||||
interface IMarkCompletedDialogueProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
onComplete: () => void;
|
||||
projectId: string;
|
||||
featureId: string;
|
||||
}
|
||||
|
||||
type Status = 'kept' | 'discarded' | 'kept-with-variant';
|
||||
|
||||
export const MarkCompletedDialogue = ({
|
||||
projectId,
|
||||
featureId,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
onComplete,
|
||||
}: IMarkCompletedDialogueProps) => {
|
||||
const { markFeatureCompleted } = useFeatureLifecycleApi();
|
||||
const [status, setStatus] = useState<Status>('kept');
|
||||
const [variant, setVariant] = useState<string | undefined>(undefined);
|
||||
const onClick = async () => {
|
||||
const sentStatus = status === 'kept-with-variant' ? 'kept' : status;
|
||||
await markFeatureCompleted(featureId, projectId, {
|
||||
status: sentStatus,
|
||||
statusValue: variant,
|
||||
});
|
||||
setIsOpen(false);
|
||||
onComplete();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
open={isOpen}
|
||||
title='Mark completed'
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
disabledPrimaryButton={
|
||||
status === 'kept-with-variant' && variant === null
|
||||
}
|
||||
onClick={onClick}
|
||||
primaryButtonText={'Mark completed'}
|
||||
secondaryButtonText='Cancel'
|
||||
>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
mb: 4,
|
||||
}}
|
||||
>
|
||||
Marking the feature toggle as complete does not affect any
|
||||
configuration, but it moves the feature toggle into it’s
|
||||
next life cycle stage and is an indication that you have
|
||||
learned what you needed in order to progress with the
|
||||
feature. It serves as a reminder to start cleaning up the
|
||||
feature toggle and removing it from the code.
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<b>What was the outcome of this feature?</b>
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
aria-label='selected-value'
|
||||
name='selected'
|
||||
sx={{ gap: (theme) => theme.spacing(0.5) }}
|
||||
onChange={(e, value) => {
|
||||
setStatus(value as Status);
|
||||
}}
|
||||
>
|
||||
<LegalValueLabel
|
||||
key={'kept'}
|
||||
value={'kept'}
|
||||
legal={{
|
||||
value: 'We decided to keep the feature',
|
||||
}}
|
||||
control={<Radio />}
|
||||
/>
|
||||
<LegalValueLabel
|
||||
key={'discarded'}
|
||||
value={'discarded'}
|
||||
legal={{
|
||||
value: 'We decided to discard the feature',
|
||||
}}
|
||||
control={<Radio />}
|
||||
/>
|
||||
<LegalValueLabel
|
||||
key={'kept-with-variant'}
|
||||
value={'kept-with-variant'}
|
||||
legal={{
|
||||
value: 'We decided to keep the feature variant',
|
||||
description:
|
||||
'Choose to specify which feature variant will be kept',
|
||||
}}
|
||||
control={<Radio />}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={status === 'kept-with-variant'}
|
||||
show={
|
||||
<SingleVariantOptions
|
||||
parent={featureId}
|
||||
project={projectId}
|
||||
onSelect={(variant) => {
|
||||
setVariant(variant);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import { Autocomplete, Checkbox, styled, TextField } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
import { useParentVariantOptions } from 'hooks/api/getters/useFeatureDependencyOptions/useFeatureDependencyOptions';
|
||||
|
||||
const StyledAutocomplete = styled(Autocomplete)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
export const SingleVariantOptions: FC<{
|
||||
project: string;
|
||||
parent: string;
|
||||
onSelect: (value: string) => void;
|
||||
}> = ({ project, parent, onSelect }) => {
|
||||
const { parentVariantOptions: variantOptions } = useParentVariantOptions(
|
||||
project,
|
||||
parent,
|
||||
);
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize='small' />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize='small' />;
|
||||
return (
|
||||
<StyledAutocomplete
|
||||
id='single-variant-options'
|
||||
options={variantOptions}
|
||||
renderOption={(props, option, { selected }) => (
|
||||
<li {...props}>
|
||||
<Checkbox
|
||||
icon={icon}
|
||||
checkedIcon={checkedIcon}
|
||||
style={{ marginRight: 8 }}
|
||||
checked={selected}
|
||||
/>
|
||||
{option}
|
||||
</li>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} placeholder='Select variant' />
|
||||
)}
|
||||
fullWidth
|
||||
onChange={(_, selectedValue) => {
|
||||
onSelect(String(selectedValue));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -20,6 +20,7 @@ import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { useShowDependentFeatures } from './useShowDependentFeatures';
|
||||
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
|
||||
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
|
||||
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
@ -96,6 +97,9 @@ const FeatureOverviewMetaData = () => {
|
||||
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
|
||||
const navigate = useNavigate();
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] =
|
||||
useState(false);
|
||||
|
||||
const { locationSettings } = useLocationSettings();
|
||||
const showDependentFeatures = useShowDependentFeatures(feature.project);
|
||||
|
||||
@ -141,7 +145,9 @@ const FeatureOverviewMetaData = () => {
|
||||
<FeatureLifecycle
|
||||
feature={feature}
|
||||
onArchive={() => setShowDelDialog(true)}
|
||||
onComplete={refetchFeature}
|
||||
onComplete={() =>
|
||||
setShowMarkCompletedDialogue(true)
|
||||
}
|
||||
onUncomplete={refetchFeature}
|
||||
/>
|
||||
</StyledRow>
|
||||
@ -237,6 +243,13 @@ const FeatureOverviewMetaData = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<MarkCompletedDialogue
|
||||
isOpen={showMarkCompletedDialogue}
|
||||
setIsOpen={setShowMarkCompletedDialogue}
|
||||
projectId={feature.project}
|
||||
featureId={feature.name}
|
||||
onComplete={refetchFeature}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import useAPI from '../useApi/useApi';
|
||||
import type { FeatureLifecycleCompletedSchema } from 'openapi';
|
||||
|
||||
const useFeatureLifecycleApi = () => {
|
||||
const { makeRequest, makeLightRequest, createRequest, errors, loading } =
|
||||
@ -6,11 +7,15 @@ const useFeatureLifecycleApi = () => {
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const markFeatureCompleted = async (name: string, project: string) => {
|
||||
const markFeatureCompleted = async (
|
||||
name: string,
|
||||
project: string,
|
||||
status: FeatureLifecycleCompletedSchema,
|
||||
) => {
|
||||
const path = `api/admin/projects/${project}/features/${name}/lifecycle/complete`;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ status: 'kept' }),
|
||||
body: JSON.stringify(status),
|
||||
});
|
||||
|
||||
return makeRequest(req.caller, req.id);
|
||||
|
Loading…
Reference in New Issue
Block a user