mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +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;
|
legal: ILegalValue;
|
||||||
control: React.ReactElement;
|
control: React.ReactElement;
|
||||||
filter?: string;
|
filter?: string;
|
||||||
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LegalValueLabel = ({
|
export const LegalValueLabel = ({
|
||||||
legal,
|
legal,
|
||||||
control,
|
control,
|
||||||
filter,
|
filter,
|
||||||
|
value,
|
||||||
}: ILegalValueTextProps) => {
|
}: ILegalValueTextProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={legal.value}
|
value={value || legal.value}
|
||||||
control={control}
|
control={control}
|
||||||
sx={{ width: '100%' }}
|
sx={{ width: '100%' }}
|
||||||
label={
|
label={
|
||||||
|
@ -24,13 +24,7 @@ export const FeatureLifecycle: FC<{
|
|||||||
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
|
}> = ({ feature, onComplete, onUncomplete, onArchive }) => {
|
||||||
const currentStage = populateCurrentStage(feature);
|
const currentStage = populateCurrentStage(feature);
|
||||||
|
|
||||||
const { markFeatureCompleted, markFeatureUncompleted, loading } =
|
const { markFeatureUncompleted, loading } = useFeatureLifecycleApi();
|
||||||
useFeatureLifecycleApi();
|
|
||||||
|
|
||||||
const onCompleteHandler = async () => {
|
|
||||||
await markFeatureCompleted(feature.name, feature.project);
|
|
||||||
onComplete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUncompleteHandler = async () => {
|
const onUncompleteHandler = async () => {
|
||||||
await markFeatureUncompleted(feature.name, feature.project);
|
await markFeatureUncompleted(feature.name, feature.project);
|
||||||
@ -41,7 +35,7 @@ export const FeatureLifecycle: FC<{
|
|||||||
<FeatureLifecycleTooltip
|
<FeatureLifecycleTooltip
|
||||||
stage={currentStage!}
|
stage={currentStage!}
|
||||||
onArchive={onArchive}
|
onArchive={onArchive}
|
||||||
onComplete={onCompleteHandler}
|
onComplete={onComplete}
|
||||||
onUncomplete={onUncompleteHandler}
|
onUncomplete={onUncompleteHandler}
|
||||||
loading={loading}
|
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 { useShowDependentFeatures } from './useShowDependentFeatures';
|
||||||
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
|
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
|
||||||
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
|
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
|
||||||
|
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
@ -96,6 +97,9 @@ const FeatureOverviewMetaData = () => {
|
|||||||
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
|
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||||
|
const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
const showDependentFeatures = useShowDependentFeatures(feature.project);
|
const showDependentFeatures = useShowDependentFeatures(feature.project);
|
||||||
|
|
||||||
@ -141,7 +145,9 @@ const FeatureOverviewMetaData = () => {
|
|||||||
<FeatureLifecycle
|
<FeatureLifecycle
|
||||||
feature={feature}
|
feature={feature}
|
||||||
onArchive={() => setShowDelDialog(true)}
|
onArchive={() => setShowDelDialog(true)}
|
||||||
onComplete={refetchFeature}
|
onComplete={() =>
|
||||||
|
setShowMarkCompletedDialogue(true)
|
||||||
|
}
|
||||||
onUncomplete={refetchFeature}
|
onUncomplete={refetchFeature}
|
||||||
/>
|
/>
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
@ -237,6 +243,13 @@ const FeatureOverviewMetaData = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<MarkCompletedDialogue
|
||||||
|
isOpen={showMarkCompletedDialogue}
|
||||||
|
setIsOpen={setShowMarkCompletedDialogue}
|
||||||
|
projectId={feature.project}
|
||||||
|
featureId={feature.name}
|
||||||
|
onComplete={refetchFeature}
|
||||||
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
import type { FeatureLifecycleCompletedSchema } from 'openapi';
|
||||||
|
|
||||||
const useFeatureLifecycleApi = () => {
|
const useFeatureLifecycleApi = () => {
|
||||||
const { makeRequest, makeLightRequest, createRequest, errors, loading } =
|
const { makeRequest, makeLightRequest, createRequest, errors, loading } =
|
||||||
@ -6,11 +7,15 @@ const useFeatureLifecycleApi = () => {
|
|||||||
propagateErrors: true,
|
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 path = `api/admin/projects/${project}/features/${name}/lifecycle/complete`;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ status: 'kept' }),
|
body: JSON.stringify(status),
|
||||||
});
|
});
|
||||||
|
|
||||||
return makeRequest(req.caller, req.id);
|
return makeRequest(req.caller, req.id);
|
||||||
|
Loading…
Reference in New Issue
Block a user