diff --git a/frontend/web/components/modals/create-feature/index.tsx b/frontend/web/components/modals/create-feature/index.tsx index 92c05e5fc20d..33b1b10957f0 100644 --- a/frontend/web/components/modals/create-feature/index.tsx +++ b/frontend/web/components/modals/create-feature/index.tsx @@ -45,6 +45,8 @@ import FeatureUpdateSummary from './components/FeatureUpdateSummary' import FeatureNameInput from './components/FeatureNameInput' import IdentitySaveFooter from './components/IdentitySaveFooter' import { ProjectPermission } from 'common/types/permissions.types' +import ConfirmRemoveFeature from 'components/modals/ConfirmRemoveFeature' +import { useRemoveFeatureWithToast } from 'components/pages/features/hooks/useRemoveFeatureWithToast' import type { ChangeRequest, FeatureState, @@ -143,6 +145,8 @@ const CreateFeatureModal: FC = (props) => { const [ownerIds, setOwnerIds] = useState([]) const [groupOwnerIds, setGroupOwnerIds] = useState([]) const [, setTabKey] = useState(0) + const [removeFeature, { isLoading: isRemovingFeature }] = + useRemoveFeatureWithToast() const isEdit = !!props.projectFlag const focusTimeoutRef = useRef | null>(null) @@ -211,6 +215,27 @@ const CreateFeatureModal: FC = (props) => { [environmentId, props.projectFlag?.id], ) + const confirmRemoveFeature = useCallback(() => { + const featureToRemove = props.projectFlag || projectFlag + + if (!featureToRemove?.id) { + return + } + + openModal2( + 'Remove Feature', + { + removeFeature(featureToRemove, projectId, { + onSuccess: close, + }).catch(() => undefined) + }} + />, + 'p-0', + ) + }, [close, projectFlag, projectId, props.projectFlag, removeFeature]) + // Mount effects useEffect(() => { setInterceptClose(onClosing) @@ -806,6 +831,7 @@ const CreateFeatureModal: FC = (props) => { projectFlag={projectFlag} isSaving={isSaving} invalid={invalid} + isRemoving={isRemovingFeature} hasMetadataRequired={hasMetadataRequired} onChange={(changes: any) => { setProjectFlag((prev: any) => ({ @@ -817,6 +843,9 @@ const CreateFeatureModal: FC = (props) => { } }} onHasMetadataRequiredChange={setHasMetadataRequired} + onRemoveFeature={ + !identity ? confirmRemoveFeature : undefined + } onSaveSettings={saveSettings} /> diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx index cdb382fc6345..c1ed488c9f30 100644 --- a/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx +++ b/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx @@ -30,12 +30,15 @@ import AccountStore from 'common/stores/account-store' import { ProjectPermission } from 'common/types/permissions.types' import { getStore } from 'common/store' import { getSupportedContentType } from 'common/services/useSupportedContentType' +import { useGetTagsQuery } from 'common/services/useTag' +import { getRemoveFeatureDisabledReason } from './FeatureSettingsTab.utils' type FeatureSettingsTabProps = { identity?: string projectId: number | string projectFlag: ProjectFlag | null isSaving?: boolean + isRemoving?: boolean invalid?: boolean hasMetadataRequired?: boolean ownerIds?: number[] @@ -44,6 +47,7 @@ type FeatureSettingsTabProps = { onGroupOwnerIdsChange?: (ids: number[]) => void onChange: (projectFlag: ProjectFlag) => void onHasMetadataRequiredChange: (hasMetadataRequired: boolean) => void + onRemoveFeature?: () => void onSaveSettings?: () => void } @@ -52,11 +56,13 @@ const FeatureSettingsTab: FC = ({ hasMetadataRequired, identity, invalid, + isRemoving = false, isSaving, onChange, onGroupOwnerIdsChange, onHasMetadataRequiredChange, onOwnerIdsChange, + onRemoveFeature, onSaveSettings, ownerIds, projectFlag, @@ -79,6 +85,10 @@ const FeatureSettingsTab: FC = ({ { id: projectFlag?.id ?? 0, project: numericProjectId }, { skip: !projectFlag?.id }, ) + const { data: tags } = useGetTagsQuery( + { projectId: numericProjectId }, + { skip: !projectFlag?.id || !!identity }, + ) const [addOwners] = useAddFlagOwnersMutation() const [removeOwners] = useRemoveFlagOwnersMutation() const [addGroupOwners] = useAddFlagGroupOwnersMutation() @@ -100,6 +110,32 @@ const FeatureSettingsTab: FC = ({ level: 'project', permission: ProjectPermission.CREATE_FEATURE, }) + const { permission: deleteFeature } = useHasPermission({ + id: projectId, + level: 'project', + permission: ProjectPermission.DELETE_FEATURE, + tags: flagData?.tags ?? projectFlag?.tags, + }) + const featureTags = flagData?.tags ?? projectFlag?.tags ?? [] + const hasProtectedTags = !!tags?.some( + (tag) => tag.is_permanent && featureTags.includes(tag.id), + ) + const removeFeatureDisabledReason = getRemoveFeatureDisabledReason({ + isProtected: hasProtectedTags, + isRemoving, + isSaving: !!isSaving, + }) + const removeFeatureButton = onRemoveFeature && deleteFeature && isEdit && ( + + ) if (!createFeature) { return ( @@ -175,10 +211,7 @@ const FeatureSettingsTab: FC = ({ }) .unwrap() .catch((e) => - toast( - e?.data?.[0] || 'Failed to remove owner.', - 'danger', - ), + toast(e?.data?.[0] || 'Failed to remove owner.', 'danger'), ) } /> @@ -334,9 +367,19 @@ const FeatureSettingsTab: FC = ({ /> {isEdit && ( -
+
+
+ {removeFeatureButton && + (removeFeatureDisabledReason ? ( + + {removeFeatureDisabledReason} + + ) : ( + removeFeatureButton + ))} +
{createFeature && ( - <> +

This will save the above settings{' '} all environments. @@ -354,7 +397,7 @@ const FeatureSettingsTab: FC = ({ > {isSaving ? 'Updating' : 'Update Settings'} - +

)}
)} diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.utils.ts b/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.utils.ts new file mode 100644 index 000000000000..b65fc5e49d0b --- /dev/null +++ b/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.utils.ts @@ -0,0 +1,25 @@ +type GetRemoveFeatureDisabledReasonParams = { + isProtected: boolean + isRemoving: boolean + isSaving: boolean +} + +export const getRemoveFeatureDisabledReason = ({ + isProtected, + isRemoving, + isSaving, +}: GetRemoveFeatureDisabledReasonParams): string | null => { + if (isProtected) { + return 'This feature has a permanent tag. Remove it before deleting the feature.' + } + + if (isRemoving) { + return 'The feature is being removed.' + } + + if (isSaving) { + return 'Wait for the current feature update to finish.' + } + + return null +} diff --git a/frontend/web/components/modals/create-feature/tabs/__tests__/FeatureSettingsTab.utils.test.ts b/frontend/web/components/modals/create-feature/tabs/__tests__/FeatureSettingsTab.utils.test.ts new file mode 100644 index 000000000000..085d0a7c361a --- /dev/null +++ b/frontend/web/components/modals/create-feature/tabs/__tests__/FeatureSettingsTab.utils.test.ts @@ -0,0 +1,43 @@ +import { getRemoveFeatureDisabledReason } from 'components/modals/create-feature/tabs/FeatureSettingsTab.utils' + +describe('getRemoveFeatureDisabledReason', () => { + it('disables removal when the feature has permanent tags', () => { + expect( + getRemoveFeatureDisabledReason({ + isProtected: true, + isRemoving: false, + isSaving: false, + }), + ).toBe( + 'This feature has a permanent tag. Remove it before deleting the feature.', + ) + }) + + it('disables removal while the feature is saving or being removed', () => { + expect( + getRemoveFeatureDisabledReason({ + isProtected: false, + isRemoving: false, + isSaving: true, + }), + ).toBe('Wait for the current feature update to finish.') + + expect( + getRemoveFeatureDisabledReason({ + isProtected: false, + isRemoving: true, + isSaving: false, + }), + ).toBe('The feature is being removed.') + }) + + it('allows removal when no blocking condition applies', () => { + expect( + getRemoveFeatureDisabledReason({ + isProtected: false, + isRemoving: false, + isSaving: false, + }), + ).toBeNull() + }) +})