Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions frontend/web/components/modals/create-feature/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -143,6 +145,8 @@ const CreateFeatureModal: FC<CreateFeatureModalProps> = (props) => {
const [ownerIds, setOwnerIds] = useState<number[]>([])
const [groupOwnerIds, setGroupOwnerIds] = useState<number[]>([])
const [, setTabKey] = useState(0)
const [removeFeature, { isLoading: isRemovingFeature }] =
useRemoveFeatureWithToast()

const isEdit = !!props.projectFlag
const focusTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
Expand Down Expand Up @@ -211,6 +215,27 @@ const CreateFeatureModal: FC<CreateFeatureModalProps> = (props) => {
[environmentId, props.projectFlag?.id],
)

const confirmRemoveFeature = useCallback(() => {
const featureToRemove = props.projectFlag || projectFlag

if (!featureToRemove?.id) {
return
}

openModal2(
'Remove Feature',
<ConfirmRemoveFeature
projectFlag={featureToRemove}
cb={() => {
removeFeature(featureToRemove, projectId, {
onSuccess: close,
}).catch(() => undefined)
}}
/>,
'p-0',
)
}, [close, projectFlag, projectId, props.projectFlag, removeFeature])

// Mount effects
useEffect(() => {
setInterceptClose(onClosing)
Expand Down Expand Up @@ -806,6 +831,7 @@ const CreateFeatureModal: FC<CreateFeatureModalProps> = (props) => {
projectFlag={projectFlag}
isSaving={isSaving}
invalid={invalid}
isRemoving={isRemovingFeature}
hasMetadataRequired={hasMetadataRequired}
onChange={(changes: any) => {
setProjectFlag((prev: any) => ({
Expand All @@ -817,6 +843,9 @@ const CreateFeatureModal: FC<CreateFeatureModalProps> = (props) => {
}
}}
onHasMetadataRequiredChange={setHasMetadataRequired}
onRemoveFeature={
!identity ? confirmRemoveFeature : undefined
}
onSaveSettings={saveSettings}
/>
</TabItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -44,6 +47,7 @@ type FeatureSettingsTabProps = {
onGroupOwnerIdsChange?: (ids: number[]) => void
onChange: (projectFlag: ProjectFlag) => void
onHasMetadataRequiredChange: (hasMetadataRequired: boolean) => void
onRemoveFeature?: () => void
onSaveSettings?: () => void
}

Expand All @@ -52,11 +56,13 @@ const FeatureSettingsTab: FC<FeatureSettingsTabProps> = ({
hasMetadataRequired,
identity,
invalid,
isRemoving = false,
isSaving,
onChange,
onGroupOwnerIdsChange,
onHasMetadataRequiredChange,
onOwnerIdsChange,
onRemoveFeature,
onSaveSettings,
ownerIds,
projectFlag,
Expand All @@ -79,6 +85,10 @@ const FeatureSettingsTab: FC<FeatureSettingsTabProps> = ({
{ 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()
Expand All @@ -100,6 +110,32 @@ const FeatureSettingsTab: FC<FeatureSettingsTabProps> = ({
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 && (
<Button
data-test='remove-feature-settings-btn'
disabled={!!removeFeatureDisabledReason}
id='remove-feature-settings-btn'
onClick={onRemoveFeature}
theme='danger'
>
{isRemoving ? 'Removing' : 'Remove Feature'}
</Button>
)

if (!createFeature) {
return (
Expand Down Expand Up @@ -175,10 +211,7 @@ const FeatureSettingsTab: FC<FeatureSettingsTabProps> = ({
})
.unwrap()
.catch((e) =>
toast(
e?.data?.[0] || 'Failed to remove owner.',
'danger',
),
toast(e?.data?.[0] || 'Failed to remove owner.', 'danger'),
)
}
/>
Expand Down Expand Up @@ -334,9 +367,19 @@ const FeatureSettingsTab: FC<FeatureSettingsTabProps> = ({
/>
<ModalHR className='mt-4' />
{isEdit && (
<div className='text-right mt-3'>
<div className='d-flex align-items-start justify-content-between mt-3'>
<div>
{removeFeatureButton &&
(removeFeatureDisabledReason ? (
<Tooltip title={removeFeatureButton}>
{removeFeatureDisabledReason}
</Tooltip>
) : (
removeFeatureButton
))}
</div>
{createFeature && (
<>
<div className='text-right'>
<p className='text-right modal-caption fs-small lh-sm'>
This will save the above settings{' '}
<strong>all environments</strong>.
Expand All @@ -354,7 +397,7 @@ const FeatureSettingsTab: FC<FeatureSettingsTabProps> = ({
>
{isSaving ? 'Updating' : 'Update Settings'}
</Button>
</>
</div>
)}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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()
})
})
Loading