diff --git a/frontend/common/hooks/useFeatureStates.ts b/frontend/common/hooks/useFeatureStates.ts
index e87e86681ff5..5d1a6a593775 100644
--- a/frontend/common/hooks/useFeatureStates.ts
+++ b/frontend/common/hooks/useFeatureStates.ts
@@ -9,6 +9,7 @@ type UseFeatureStatesArgs = {
// The api key, as everywhere else in the UI.
environmentId: string
projectId: number
+ skip?: boolean
}
// Whether each of a project's features is on in one environment, from the
@@ -16,6 +17,7 @@ type UseFeatureStatesArgs = {
export const useFeatureStates = ({
environmentId,
projectId,
+ skip,
}: UseFeatureStatesArgs) => {
const {
error: environmentsError,
@@ -35,7 +37,7 @@ export const useFeatureStates = ({
page_size: 999,
projectId,
},
- { skip: !numericEnvId },
+ { skip: skip || !numericEnvId },
)
const features = data?.results ?? NO_FEATURES
diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts
index 9c6096351ed8..3ef3f6f99efd 100644
--- a/frontend/common/types/responses.ts
+++ b/frontend/common/types/responses.ts
@@ -187,7 +187,6 @@ export type DependencyEdge = {
export type StagedAction = 'add' | 'remove'
-// A dependency held in a change request, not live until it is published.
export type StagedDependencyChange = {
action: StagedAction
prerequisite: DependencyFeature
@@ -247,7 +246,10 @@ export type Segment = {
metadata: Metadata[] | []
membership_counts?: SegmentMembership[]
cohort?: SegmentCohort | null
+ managed_by?: SegmentManagedBy
}
+// What creates and edits the segment, empty where users do.
+export type SegmentManagedBy = '' | 'cohort' | 'dependency' | 'experiment'
export type ProjectChangeRequest = Omit<
ChangeRequest,
| 'environment_feature_versions'
diff --git a/frontend/common/utils/__tests__/diffDependencies.test.ts b/frontend/common/utils/__tests__/diffDependencies.test.ts
new file mode 100644
index 000000000000..2202a11777f7
--- /dev/null
+++ b/frontend/common/utils/__tests__/diffDependencies.test.ts
@@ -0,0 +1,40 @@
+import { DependencyEdge } from 'common/types/responses'
+import { diffDependencies } from 'common/utils/diffDependencies'
+
+const edge = (prerequisiteId: number): DependencyEdge => ({
+ feature: { id: 1, name: 'checkout' },
+ prerequisite: { id: prerequisiteId, name: `prereq_${prerequisiteId}` },
+ segment: {
+ condition_json_path: '',
+ id: 1,
+ is_system: true,
+ name: 'checkout',
+ rules: [],
+ },
+})
+
+describe('diffDependencies', () => {
+ it('reads a prerequisite only in the change request as an add', () => {
+ expect(diffDependencies([edge(1)], [edge(1), edge(2)])).toEqual([
+ { action: 'add', prerequisite: { id: 2, name: 'prereq_2' } },
+ ])
+ })
+
+ it('reads a live prerequisite missing from the change request as a removal', () => {
+ expect(diffDependencies([edge(1), edge(2)], [edge(1)])).toEqual([
+ { action: 'remove', prerequisite: { id: 2, name: 'prereq_2' } },
+ ])
+ })
+
+ it('lists adds before removals', () => {
+ expect(
+ diffDependencies([edge(1), edge(2)], [edge(1), edge(3)]).map(
+ (change) => change.action,
+ ),
+ ).toEqual(['add', 'remove'])
+ })
+
+ it('finds no change when the lists match', () => {
+ expect(diffDependencies([edge(1)], [edge(1)])).toEqual([])
+ })
+})
diff --git a/frontend/common/utils/diffDependencies.ts b/frontend/common/utils/diffDependencies.ts
new file mode 100644
index 000000000000..ea19e63feeac
--- /dev/null
+++ b/frontend/common/utils/diffDependencies.ts
@@ -0,0 +1,29 @@
+import { DependencyEdge, StagedDependencyChange } from 'common/types/responses'
+
+// The API lists a change request's dependencies as if it were live, so what
+// it changes is that list against today's.
+export const diffDependencies = (
+ live: DependencyEdge[],
+ scoped: DependencyEdge[],
+): StagedDependencyChange[] => {
+ const liveIds = new Set(live.map((edge) => edge.prerequisite.id))
+ const scopedIds = new Set(scoped.map((edge) => edge.prerequisite.id))
+ return [
+ ...scoped
+ .filter((edge) => !liveIds.has(edge.prerequisite.id))
+ .map(
+ ({ prerequisite }): StagedDependencyChange => ({
+ action: 'add',
+ prerequisite,
+ }),
+ ),
+ ...live
+ .filter((edge) => !scopedIds.has(edge.prerequisite.id))
+ .map(
+ ({ prerequisite }): StagedDependencyChange => ({
+ action: 'remove',
+ prerequisite,
+ }),
+ ),
+ ]
+}
diff --git a/frontend/documentation/components/Chip.stories.tsx b/frontend/documentation/components/Chip.stories.tsx
index c88fe9860935..068a95042d1b 100644
--- a/frontend/documentation/components/Chip.stories.tsx
+++ b/frontend/documentation/components/Chip.stories.tsx
@@ -28,6 +28,10 @@ export const Accent: Story = {
args: { children: '"hello"', variant: 'accent' },
}
+export const Danger: Story = {
+ args: { children: 'Rejected', variant: 'danger' },
+}
+
export const Sizes: Story = {
render: () => (
diff --git a/frontend/web/components/base/Chip/Chip.scss b/frontend/web/components/base/Chip/Chip.scss
index 854427ce3f98..a55ba57d0212 100644
--- a/frontend/web/components/base/Chip/Chip.scss
+++ b/frontend/web/components/base/Chip/Chip.scss
@@ -13,6 +13,10 @@
border-color: var(--color-border-action);
}
+ &--danger {
+ border-color: var(--color-border-danger);
+ }
+
// Sizes (default is the base above).
&--sm {
padding: 3px 8px;
diff --git a/frontend/web/components/base/Chip/Chip.tsx b/frontend/web/components/base/Chip/Chip.tsx
index 296219372dda..2aae4d2edc89 100644
--- a/frontend/web/components/base/Chip/Chip.tsx
+++ b/frontend/web/components/base/Chip/Chip.tsx
@@ -5,7 +5,7 @@ import { colorIconSecondary } from 'common/theme/tokens'
import './Chip.scss'
export type ChipSize = 'default' | 'sm' | 'xs'
-export type ChipVariant = 'neutral' | 'accent'
+export type ChipVariant = 'neutral' | 'accent' | 'danger'
export type ChipProps = {
children: ReactNode
@@ -30,6 +30,7 @@ export type ChipProps = {
// bg + text come from token utilities; the variant border lives in Chip.scss.
const VARIANT_UTILITIES: Record = {
accent: 'bg-surface-action-subtle text-action',
+ danger: 'bg-transparent text-danger',
neutral: 'bg-surface-subtle text-default',
}
@@ -63,6 +64,7 @@ const Chip = ({
{
'ds-chip--accent': variant === 'accent',
'ds-chip--clickable': interactive,
+ 'ds-chip--danger': variant === 'danger',
[`ds-chip--${size}`]: size !== 'default',
'ds-chip--truncate': truncate,
},
diff --git a/frontend/web/components/diff/DiffChangeRequest.tsx b/frontend/web/components/diff/DiffChangeRequest.tsx
index fbf1bf526e49..09b443c01113 100644
--- a/frontend/web/components/diff/DiffChangeRequest.tsx
+++ b/frontend/web/components/diff/DiffChangeRequest.tsx
@@ -3,6 +3,10 @@ import { ChangeRequest } from 'common/types/responses'
import { useGetFeatureStatesQuery } from 'common/services/useFeatureState'
import DiffFeature from './DiffFeature'
import { mergeChangeSets } from 'common/services/useChangeRequest'
+import { useFeatureStates } from 'common/hooks/useFeatureStates'
+import { useDependencyChanges } from 'components/feature-dependencies/useDependencyChanges'
+import { useHasPermission } from 'common/providers/Permission'
+import { EnvironmentPermission } from 'common/types/permissions.types'
type DiffChangeRequestType = {
changeRequest: ChangeRequest | null
@@ -38,6 +42,34 @@ const DiffChangeRequest: FC = ({
return mergeChangeSets(changeSets, data?.results, changeRequest?.conflicts)
}, [changeRequest, feature, data])
+ // The API lists a change request's dependencies only to those who can
+ // create change requests.
+ const { permission: canCreateChangeRequest } = useHasPermission({
+ id: environmentId,
+ level: 'environment',
+ permission: EnvironmentPermission.CREATE_CHANGE_REQUEST,
+ })
+ const skipDependencies =
+ !changeRequest ||
+ // The API rejects these.
+ changeRequest.is_committed ||
+ !isVersioned ||
+ !canCreateChangeRequest ||
+ !Utils.getFlagsmithHasFeature('flag_dependencies') ||
+ !Utils.getFlagsmithHasFeature('flag_dependencies_change_requests')
+ const dependencies = useDependencyChanges({
+ changeRequestId: changeRequest?.id ?? 0,
+ environmentId,
+ featureId: feature,
+ skip: skipDependencies,
+ })
+ // Shows if an added prerequisite is off.
+ const states = useFeatureStates({
+ environmentId,
+ projectId: Number(projectId),
+ skip: skipDependencies,
+ })
+
if (!changeRequest) {
return null
}
@@ -52,6 +84,12 @@ const DiffChangeRequest: FC = ({
return (
boolean
+ }
}
type ViewMode = 'combined' | 'new' | 'old'
const DiffFeature: FC = ({
conflicts,
+ dependencies,
disableSegments,
environmentId,
featureId,
@@ -71,17 +80,36 @@ const DiffFeature: FC = ({
projectId,
})
+ // Dependency segments get their own tab once loaded.
+ const overrideSegments = dependencies?.changes
+ ? segments?.results.filter((segment) => segment.managed_by !== 'dependency')
+ : segments?.results
const segmentDiffs = disableSegments
? { diffs: [], totalChanges: 0 }
- : getSegmentOverrideDiff(oldState, newState, segments?.results, conflicts)
+ : getSegmentOverrideDiff(oldState, newState, overrideSegments, conflicts)
const variationDiffs = getVariationDiff(oldEnv, newEnv)
const totalSegmentChanges = segmentDiffs?.totalChanges
const totalVariationChanges = variationDiffs?.totalChanges
+ const totalDependencyChanges = dependencies?.changes?.length ?? 0
+ const showDependencies = !!totalDependencyChanges || !!dependencies?.isError
+ // Each tab's change count, in the order the tabs render.
+ const tabChanges = [
+ totalChanges,
+ ...(variationDiffs?.diffs?.length ? [totalVariationChanges] : []),
+ ...(segmentDiffs?.diffs.length ? [totalSegmentChanges] : []),
+ ...(showDependencies ? [totalDependencyChanges] : []),
+ ]
+ const changedTab = tabChanges.findIndex((changes) => !!changes)
+ // With no other changes, open Dependencies (the last tab) to show its error.
+ const firstChangedTab =
+ changedTab === -1 && dependencies?.isError
+ ? tabChanges.length - 1
+ : changedTab
useEffect(() => {
- if (!totalChanges && (totalSegmentChanges || totalVariationChanges)) {
- setValue(1)
+ if (firstChangedTab > 0) {
+ setValue(firstChangedTab)
}
- }, [totalSegmentChanges, totalVariationChanges, totalChanges])
+ }, [firstChangedTab])
const hideValue =
!totalChanges && (diff.newValue === null || diff.newValue === undefined)
const viewOptions = [
@@ -245,6 +273,31 @@ const DiffFeature: FC = ({
/>
)}
+ {showDependencies && (
+
+ Dependencies
+ {!!totalDependencyChanges && (
+ {totalDependencyChanges}
+ )}
+
+ }
+ >
+ {dependencies?.isError ? (
+
+ Could not load the dependency changes in this change
+ request.
+
+ ) : (
+
+ )}
+
+ )}
>
)}
diff --git a/frontend/web/components/feature-dependencies/DependencyChangesTable/DependencyChangesTable.scss b/frontend/web/components/feature-dependencies/DependencyChangesTable/DependencyChangesTable.scss
new file mode 100644
index 000000000000..7602c4c7140c
--- /dev/null
+++ b/frontend/web/components/feature-dependencies/DependencyChangesTable/DependencyChangesTable.scss
@@ -0,0 +1,8 @@
+// Fixed widths so the names line up across tables.
+.dependency-changes__change {
+ width: 48px;
+}
+
+.dependency-changes__state {
+ width: 120px;
+}
diff --git a/frontend/web/components/feature-dependencies/DependencyChangesTable/DependencyChangesTable.tsx b/frontend/web/components/feature-dependencies/DependencyChangesTable/DependencyChangesTable.tsx
new file mode 100644
index 000000000000..f34968b447db
--- /dev/null
+++ b/frontend/web/components/feature-dependencies/DependencyChangesTable/DependencyChangesTable.tsx
@@ -0,0 +1,83 @@
+import { FC } from 'react'
+import { StagedDependencyChange } from 'common/types/responses'
+import cn from 'classnames'
+import StateToggle from 'components/feature-dependencies/StateToggle'
+import Satisfies from 'components/feature-dependencies/Satisfies'
+import 'components/diff/DiffRow/DiffRow.scss'
+import './DependencyChangesTable.scss'
+import Table, {
+ TableBody,
+ TableCell,
+ TableColumnHeader,
+ TableHeader,
+ TableRow,
+} from 'components/base/Table'
+
+type DependencyChangesTableProps = {
+ changes: StagedDependencyChange[]
+ // Adds the live table's Status and Satisfies columns for this environment.
+ isEnabled?: (prerequisiteId: number) => boolean
+}
+
+const DependencyChangesTable: FC = ({
+ changes,
+ isEnabled,
+}) => (
+
+
+
+
+ Prerequisite
+ {!!isEnabled && (
+ <>
+
+ Status
+
+
+ Satisfies
+
+ >
+ )}
+
+
+
+ {changes.map(({ action, prerequisite }) => {
+ const isAdd = action === 'add'
+ return (
+
+
+
+ {isAdd ? '+' : '-'}
+
+
+
+ {prerequisite.name}
+
+ {!!isEnabled && (
+ <>
+
+
+
+
+ {/* Only system edges can be staged, so on means met. */}
+
+
+ >
+ )}
+
+ )
+ })}
+
+
+)
+
+export default DependencyChangesTable
diff --git a/frontend/web/components/feature-dependencies/DependencyChangesTable/index.ts b/frontend/web/components/feature-dependencies/DependencyChangesTable/index.ts
new file mode 100644
index 000000000000..4564a17ff442
--- /dev/null
+++ b/frontend/web/components/feature-dependencies/DependencyChangesTable/index.ts
@@ -0,0 +1 @@
+export { default } from './DependencyChangesTable'
diff --git a/frontend/web/components/feature-dependencies/useDependencyChanges.ts b/frontend/web/components/feature-dependencies/useDependencyChanges.ts
new file mode 100644
index 000000000000..3682df154dfb
--- /dev/null
+++ b/frontend/web/components/feature-dependencies/useDependencyChanges.ts
@@ -0,0 +1,30 @@
+import { skipToken } from '@reduxjs/toolkit/query'
+import { useGetFeatureDependenciesQuery } from 'common/services/useFeatureDependency'
+import { diffDependencies } from 'common/utils/diffDependencies'
+
+type UseDependencyChangesArgs = {
+ environmentId: string
+ featureId: number
+ changeRequestId: number
+ skip?: boolean
+}
+
+export const useDependencyChanges = ({
+ changeRequestId,
+ environmentId,
+ featureId,
+ skip,
+}: UseDependencyChangesArgs) => {
+ const live = useGetFeatureDependenciesQuery(
+ skip ? skipToken : { environmentId, featureId },
+ )
+ const scoped = useGetFeatureDependenciesQuery(
+ skip ? skipToken : { changeRequestId, environmentId, featureId },
+ )
+ const isError = live.isError || scoped.isError
+ const changes =
+ live.data && scoped.data
+ ? diffDependencies(live.data.results, scoped.data.results)
+ : undefined
+ return { changes, isError }
+}
diff --git a/frontend/web/components/modals/create-feature/hoc/FeatureProvider.tsx b/frontend/web/components/modals/create-feature/hoc/FeatureProvider.tsx
index 254fec52a67f..790384afbcac 100644
--- a/frontend/web/components/modals/create-feature/hoc/FeatureProvider.tsx
+++ b/frontend/web/components/modals/create-feature/hoc/FeatureProvider.tsx
@@ -23,13 +23,7 @@ export default function withFeatureProvider(
this.listenTo(
FeatureListStore,
'saved',
- ({
- changeRequest,
- createdFlag,
- error,
- isCreate,
- updatedChangeRequest,
- }: any = {}) => {
+ ({ changeRequest, createdFlag, error, isCreate }: any = {}) => {
if (error?.data?.metadata) {
error.data.metadata?.forEach((m: any) => {
if (Object.keys(m).length > 0) {
@@ -39,28 +33,10 @@ export default function withFeatureProvider(
} else if (error?.data) {
toast('Error updating the Flag', 'danger')
return
- } else {
- const isEditingChangeRequest =
- this.props.changeRequest && changeRequest
+ } else if (!changeRequest) {
+ // The modal toasts a change request once its dependencies are staged.
const operation = createdFlag || isCreate ? 'Created' : 'Updated'
- const type = changeRequest ? 'Change Request' : 'Feature'
-
- const toastText = isEditingChangeRequest
- ? `Updated ${type}`
- : `${operation} ${type}`
- const toastAction = changeRequest
- ? {
- buttonText: 'Open',
- onClick: () => {
- closeModal()
- this.props.history.push(
- `/project/${this.props.projectId}/environment/${this.props.environmentId}/change-requests/${updatedChangeRequest?.id}`,
- )
- },
- }
- : undefined
-
- toast(toastText, 'success', undefined, toastAction)
+ toast(`${operation} Feature`, 'success')
}
const envFlags = FeatureListStore.getEnvironmentFlags()
diff --git a/frontend/web/components/modals/create-feature/index.tsx b/frontend/web/components/modals/create-feature/index.tsx
index 18a35671864b..e7ab3f699719 100644
--- a/frontend/web/components/modals/create-feature/index.tsx
+++ b/frontend/web/components/modals/create-feature/index.tsx
@@ -156,8 +156,7 @@ const CreateFeatureModal: FC = (props) => {
const [, setTabKey] = useState(0)
const isEdit = !!props.projectFlag
- // Held here rather than in the tab, so the change request any tab creates
- // takes them along.
+ // In the modal, so a change request from any tab includes them.
const dependencyChanges = useStagedPrerequisites({
environmentId,
projectFlag,
@@ -497,6 +496,22 @@ const CreateFeatureModal: FC = (props) => {
}
const Provider = identity ? IdentityProvider : FeatureListProvider
+
+ const toastChangeRequestSaved = (changeRequestId: number) =>
+ toast(
+ `${existingChangeRequest ? 'Updated' : 'Created'} Change Request`,
+ 'success',
+ undefined,
+ {
+ buttonText: 'Open',
+ onClick: () => {
+ closeModal()
+ props.history?.push(
+ `/project/${projectId}/environment/${environmentId}/change-requests/${changeRequestId}`,
+ )
+ },
+ },
+ )
const environmentName = environment.name
return (
@@ -508,10 +523,22 @@ const CreateFeatureModal: FC = (props) => {
setValueChanged(true)
}
}}
- onSave={(data?: { updatedChangeRequest?: { id: number } }) => {
- const changeRequestId = data?.updatedChangeRequest?.id
- if (changeRequestId && dependenciesChanged) {
- dependencyChanges.stageInto(changeRequestId)
+ onSave={(data?: {
+ changeRequest?: boolean
+ updatedChangeRequest?: { id: number }
+ }) => {
+ const changeRequestId = data?.changeRequest
+ ? data.updatedChangeRequest?.id
+ : undefined
+ if (changeRequestId) {
+ const staging = dependenciesChanged
+ ? dependencyChanges.stageInto(changeRequestId)
+ : Promise.resolve(null)
+ // A change request missing some dependency changes gets the banner,
+ // not a success toast.
+ staging.then((failure) => {
+ if (!failure) toastChangeRequestSaved(changeRequestId)
+ })
}
if (identity) {
close()
diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/FeatureDependenciesTab.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/FeatureDependenciesTab.tsx
index 27927f57fb60..ad4e39a60d95 100644
--- a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/FeatureDependenciesTab.tsx
+++ b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/FeatureDependenciesTab.tsx
@@ -28,7 +28,6 @@ type FeatureDependenciesTabProps = {
projectFlag: ProjectFlag
requiresChangeRequests: boolean
isVersioned: boolean
- // Held by the modal, so any tab's change request takes them along.
staging: StagedPrerequisites
// The modal is creating the change request the held changes go into.
isSaving?: boolean
@@ -95,7 +94,6 @@ const FeatureDependenciesTab: FC = ({
return
}
- // Where change requests are required, edits are held instead of written.
const editing = isChangeRequest
? {
onAdd: (feature: ProjectFlag) => {
diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/PrerequisitesTable.scss b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/PrerequisitesTable.scss
index 54d52a48d425..56b6491c76fc 100644
--- a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/PrerequisitesTable.scss
+++ b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/PrerequisitesTable.scss
@@ -15,11 +15,6 @@
background: var(--color-surface-muted);
}
-// On the cell, as with the input row, so hover does not replace it.
-.prerequisite-row--refused td {
- background: var(--color-surface-danger);
-}
-
// The select needs permission to shrink or it pushes the row past the panel.
.prerequisite-add-control > :first-child {
flex: 1 1 auto;
diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/PrerequisitesTable.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/PrerequisitesTable.tsx
index 08475d434dd8..f2d5881f0755 100644
--- a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/PrerequisitesTable.tsx
+++ b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/PrerequisitesTable.tsx
@@ -2,7 +2,6 @@ import { FC, MouseEvent, ReactNode } from 'react'
import cn from 'classnames'
import { DependencyEdge } from 'common/types/responses'
import Button from 'components/base/forms/Button'
-import Chip from 'components/base/Chip'
import Table, {
TableBody,
TableCell,
@@ -15,6 +14,7 @@ import Icon from 'components/icons/Icon'
import { PrerequisiteRow } from 'components/modals/create-feature/tabs/FeatureDependenciesTab/prerequisiteState'
import StateToggle from 'components/feature-dependencies/StateToggle'
import Satisfies from 'components/feature-dependencies/Satisfies'
+import StagedChip from './StagedChip'
import './PrerequisitesTable.scss'
// The actions column only exists where the user can manage dependencies, so the
@@ -27,9 +27,8 @@ type PrerequisitesTableProps = {
flashedId?: number
isRemoving?: number
onRemove: (edge: DependencyEdge) => void
- // Takes back a held removal.
onUndo?: (edge: DependencyEdge) => void
- // The row a change request refused, marked until the user acts.
+ // The held change the API rejected.
refusedId?: number
onSelect: (edge: DependencyEdge) => void
// The prerequisite being added, shown as a row of its own until the POST
@@ -72,7 +71,6 @@ const PrerequisitesTable: FC = ({
key={edge.prerequisite.id}
className={cn('cursor-pointer', {
'prerequisite-row--flash': flashedId === edge.prerequisite.id,
- 'prerequisite-row--refused': refusedId === edge.prerequisite.id,
})}
onClick={() => onSelect(edge)}
pending={isRemoving === edge.prerequisite.id}
@@ -99,13 +97,10 @@ const PrerequisitesTable: FC = ({
{!!staged && (
-
- {staged === 'add' ? 'Staged: add' : 'Staged: remove'}
-
+
)}
diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/StagedChip.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/StagedChip.tsx
new file mode 100644
index 000000000000..adb0f4ec7152
--- /dev/null
+++ b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/PrerequisitesTable/StagedChip.tsx
@@ -0,0 +1,25 @@
+import { FC } from 'react'
+import { StagedAction } from 'common/types/responses'
+import Chip, { ChipVariant } from 'components/base/Chip'
+
+type StagedChipProps = {
+ action: StagedAction
+ isRefused?: boolean
+}
+
+const variant = (action: StagedAction, isRefused?: boolean): ChipVariant => {
+ if (isRefused) return 'danger'
+ return action === 'add' ? 'accent' : 'neutral'
+}
+
+const StagedChip: FC = ({ action, isRefused }) => (
+
+ {isRefused ? 'Rejected' : 'Staged'}: {action}
+
+)
+
+export default StagedChip
diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/dependenciesMode.ts b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/dependenciesMode.ts
index 822e49634d5a..0531d21e2530 100644
--- a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/dependenciesMode.ts
+++ b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/dependenciesMode.ts
@@ -1,6 +1,6 @@
// How dependency changes reach an environment: written directly, held for a
// change request, or not at all where change requests run on v1 versioning,
-// which #8449 does not support.
+// which isn't supported.
export type DependenciesMode = 'direct' | 'changeRequest' | 'readOnly'
export const getDependenciesMode = (
diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/hooks/useStagedPrerequisites.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/hooks/useStagedPrerequisites.tsx
index 65aba2ec38fb..a1e2fdd7b942 100644
--- a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/hooks/useStagedPrerequisites.tsx
+++ b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/hooks/useStagedPrerequisites.tsx
@@ -9,7 +9,6 @@ import {
useDeleteFeatureDependencyMutation,
} from 'common/services/useFeatureDependency'
-// Why a change could not be staged, and the row it belongs to.
export type StagingFailure = { message: string; prerequisiteId?: number }
// Dependency refusals answer { message }, other errors { detail }.
@@ -87,9 +86,7 @@ export const useStagedPrerequisites = ({
return null
}
- // Into the change request the modal created with the value and segment
- // overrides. Those that made it in leave the held list, so the rest can be
- // tried again.
+ // Staged changes leave the list, so a retry sends only the rest.
const stageInto = async (changeRequestId: number) => {
setIsSubmitting(true)
setError(null)
@@ -107,6 +104,7 @@ export const useStagedPrerequisites = ({
setStaged([])
}
setIsSubmitting(false)
+ return failure
}
return {
@@ -120,7 +118,7 @@ export const useStagedPrerequisites = ({
remove,
stageInto,
staged,
- // The refusal was about the held changes, so it goes with them.
+ // Undoing clears the error too.
undo: (prerequisiteId: number) => {
setError(null)
drop(prerequisiteId)
diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/prerequisiteState.ts b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/prerequisiteState.ts
index c81fe9ea672f..4545aaf77d67 100644
--- a/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/prerequisiteState.ts
+++ b/frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/prerequisiteState.ts
@@ -11,7 +11,6 @@ export type PrerequisiteRow = {
isEnabled: boolean
// Undefined where the rule cannot be read. See toPrerequisiteRow.
isMet?: boolean
- // Held for a change request, not live yet.
staged?: StagedAction
}