From 5ba548835f21c7ece8591554d7cff02475c0745b Mon Sep 17 00:00:00 2001 From: Deep Santoshwar Date: Sat, 1 Aug 2026 23:40:19 +0530 Subject: [PATCH 1/2] fix(Features): add remove option to feature settings panel The Settings tab only exposed an Archived toggle, which looks like the way to delete a feature but isn't. Add a "Remove Feature" action that reuses the existing ConfirmRemoveFeature flow already used from the features list. --- .../tabs/FeatureSettingsTab.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx index ea8535c37087..525d5045ea35 100644 --- a/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx +++ b/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx @@ -32,6 +32,8 @@ 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 ConfirmRemoveFeature from 'components/modals/ConfirmRemoveFeature' +import { useRemoveFeatureWithToast } from 'components/pages/features/hooks/useRemoveFeatureWithToast' type FeatureSettingsTabProps = { identity?: string @@ -106,6 +108,30 @@ const FeatureSettingsTab: FC = ({ level: 'project', permission: ProjectPermission.CREATE_FEATURE, }) + const { permission: deleteFeature } = useHasPermission({ + id: projectId, + level: 'project', + permission: ProjectPermission.DELETE_FEATURE, + }) + const [removeFeature] = useRemoveFeatureWithToast() + + const handleRemove = () => { + if (!projectFlag) return + openModal2( + 'Remove Feature', + { + removeFeature(projectFlag, numericProjectId, { + onSuccess: closeModal, + }).catch(() => { + // toast already shown by useRemoveFeatureWithToast + }) + }} + />, + 'p-0', + ) + } if (!createFeature) { return ( @@ -336,6 +362,29 @@ const FeatureSettingsTab: FC = ({ )} + {!identity && isEdit && ( + + +
+ +

+ This will remove the feature for all environments. This action + cannot be undone. +

+
+ +
+
+ )} + {onSaveSettings && ( <> Date: Sun, 2 Aug 2026 00:11:01 +0530 Subject: [PATCH 2/2] fix(Features): make Remove Feature reachable for delete-only permission The tab returned early behind the CREATE_FEATURE permission check before ever rendering the Remove Feature button added in the previous commit, so a user granted only DELETE_FEATURE (a distinct, independently grantable project permission) could never reach it. Broaden the gate to render for either permission, keeping the editable settings themselves scoped to CREATE_FEATURE as before. --- .../tabs/FeatureSettingsTab.tsx | 403 +++++++++--------- 1 file changed, 212 insertions(+), 191 deletions(-) diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx index 525d5045ea35..535cd6a41bca 100644 --- a/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx +++ b/frontend/web/components/modals/create-feature/tabs/FeatureSettingsTab.tsx @@ -133,7 +133,7 @@ const FeatureSettingsTab: FC = ({ ) } - if (!createFeature) { + if (!createFeature && !deleteFeature) { return (
= ({ return (
- {freeze?.isFrozen && freeze.experiment && environmentId && ( - - )} - {!identity && projectFlag?.tags && ( - - onChange({ ...projectFlag, tags })} - /> - } + {!createFeature && ( + +
- - )} - {metadataEnable && featureContentType?.id && !identity && ( - <> - - onChange({ ...projectFlag, metadata })} - /> - + )} - {!identity && projectFlag?.id && createFeature && ( + {createFeature && ( <> - - o.id)} - onAdd={(id) => - addOwners({ - feature_id: projectFlag.id, - project_id: numericProjectId, - user_ids: [id], - }) - } - onRemove={(id) => - removeOwners({ - feature_id: projectFlag.id, - project_id: numericProjectId, - user_ids: [id], - }) - .unwrap() - .catch((e) => - toast(e?.data?.[0] || 'Failed to remove owner.', 'danger'), - ) - } + {freeze?.isFrozen && freeze.experiment && environmentId && ( + - - - g.id)} - onAdd={(id) => - addGroupOwners({ - feature_id: projectFlag.id, - group_ids: [id], - project_id: numericProjectId, - }) - } - onRemove={(id) => - removeGroupOwners({ - feature_id: projectFlag.id, - group_ids: [id], - project_id: numericProjectId, - }) - .unwrap() - .catch((e) => - toast( - e?.data?.[0] || 'Failed to remove group owner.', - 'danger', - ), - ) - } - /> - - - - )} - {!identity && - !projectFlag?.id && - enforceFeatureOwners && - onOwnerIdsChange && - onGroupOwnerIdsChange && ( - <> + )} + {!identity && projectFlag?.tags && ( - - onOwnerIdsChange([...(ownerIds ?? []), id]) - } - onRemove={(id: number) => - onOwnerIdsChange((ownerIds ?? []).filter((uid) => uid !== id)) + onChange({ ...projectFlag, tags })} + /> } /> - - - onGroupOwnerIdsChange([...(groupOwnerIds ?? []), id]) - } - onRemove={(id: number) => - onGroupOwnerIdsChange( - (groupOwnerIds ?? []).filter((gid) => gid !== id), - ) + )} + {metadataEnable && featureContentType?.id && !identity && ( + <> + + onChange({ ...projectFlag, metadata })} /> - - - - )} - - - onChange({ - ...projectFlag, - description: Utils.safeParseEventValue(e), - }) - } - type='text' - title={identity ? 'Description' : 'Description (optional)'} - placeholder="e.g. 'This determines what size the header is' " - /> - - - {!identity && ( - - - - onChange({ ...projectFlag, is_server_key_only }) + + )} + {!identity && projectFlag?.id && createFeature && ( + <> + + o.id)} + onAdd={(id) => + addOwners({ + feature_id: projectFlag.id, + project_id: numericProjectId, + user_ids: [id], + }) + } + onRemove={(id) => + removeOwners({ + feature_id: projectFlag.id, + project_id: numericProjectId, + user_ids: [id], + }) + .unwrap() + .catch((e) => + toast( + e?.data?.[0] || 'Failed to remove owner.', + 'danger', + ), + ) + } + /> + + + g.id)} + onAdd={(id) => + addGroupOwners({ + feature_id: projectFlag.id, + group_ids: [id], + project_id: numericProjectId, + }) + } + onRemove={(id) => + removeGroupOwners({ + feature_id: projectFlag.id, + group_ids: [id], + project_id: numericProjectId, + }) + .unwrap() + .catch((e) => + toast( + e?.data?.[0] || 'Failed to remove group owner.', + 'danger', + ), + ) + } + /> + + + + )} + {!identity && + !projectFlag?.id && + enforceFeatureOwners && + onOwnerIdsChange && + onGroupOwnerIdsChange && ( + <> + + + onOwnerIdsChange([...(ownerIds ?? []), id]) + } + onRemove={(id: number) => + onOwnerIdsChange( + (ownerIds ?? []).filter((uid) => uid !== id), + ) + } + /> + + + + onGroupOwnerIdsChange([...(groupOwnerIds ?? []), id]) + } + onRemove={(id: number) => + onGroupOwnerIdsChange( + (groupOwnerIds ?? []).filter((gid) => gid !== id), + ) + } + /> + + + + )} + + + onChange({ + ...projectFlag, + description: Utils.safeParseEventValue(e), + }) } - disabled={!!freeze?.isFrozen} - className='ml-0' + type='text' + title={identity ? 'Description' : 'Description (optional)'} + placeholder="e.g. 'This determines what size the header is' " /> - - Server-side only - - } - > - Prevent this feature from being accessed with client-side SDKs. - - - - )} + - {!identity && isEdit && ( - - - - onChange({ ...projectFlag, is_archived }) - } - disabled={!!freeze?.isFrozen} - className='ml-0' - /> - - Archived - - } - > - {`Archiving a flag allows you to filter out flags from the + {!identity && ( + + + + onChange({ ...projectFlag, is_server_key_only }) + } + disabled={!!freeze?.isFrozen} + className='ml-0' + /> + + Server-side only + + } + > + Prevent this feature from being accessed with client-side + SDKs. + + + + )} + + {!identity && isEdit && ( + + + + onChange({ ...projectFlag, is_archived }) + } + disabled={!!freeze?.isFrozen} + className='ml-0' + /> + + Archived + + } + > + {`Archiving a flag allows you to filter out flags from the Flagsmith dashboard that are no longer relevant.
An archived flag will still return as normal in all SDK endpoints.`} -
-
-
+
+
+
+ )} + )} {!identity && isEdit && (