Skip to content

Commit 3a10762

Browse files
talissoncostaclaude
andcommitted
feat(dependencies): show dependency changes pending in change requests
Brings back the feature tab's pending section, split out of #8716 while the API's way to list change requests by feature is discussed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 3b390f1 commit 3a10762

12 files changed

Lines changed: 245 additions & 2 deletions

File tree

‎frontend/common/services/useFeatureDependency.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { FetchBaseQueryError } from '@reduxjs/toolkit/query'
12
import { Req } from 'common/types/requests'
23
import { Res } from 'common/types/responses'
34
import { service } from 'common/service'
@@ -36,6 +37,15 @@ const dependencyUrl = (query: Req['createFeatureDependency']) =>
3637
query.changeRequestId,
3738
)
3839

40+
// The API cannot list these yet, so the pending section reports an error
41+
// rather than claim nothing is pending.
42+
const notListedYet: { error: FetchBaseQueryError } = {
43+
error: {
44+
error: 'The API does not list staged dependencies yet.',
45+
status: 'CUSTOM_ERROR',
46+
},
47+
}
48+
3949
const pendingTags = (featureId: number): DependencyTag[] => [
4050
{ id: `PENDING-${featureId}`, type: 'FeatureDependency' },
4151
{ id: 'PENDING', type: 'FeatureDependency' },
@@ -99,6 +109,14 @@ export const featureDependencyService = service
99109
url: dependencyUrl(query),
100110
}),
101111
}),
112+
// Will be the change requests listed by feature.
113+
getChangeRequestIdsStaging: builder.query<
114+
Res['changeRequestIdsStaging'],
115+
Req['getChangeRequestIdsStaging']
116+
>({
117+
providesTags: (res, err, query) => pendingTags(query.featureId),
118+
queryFn: () => notListedYet,
119+
}),
102120
getFeatureDependencies: builder.query<
103121
Res['featureDependencies'],
104122
Req['getFeatureDependencies']
@@ -189,6 +207,7 @@ export async function getFeatureDependents(
189207
export const {
190208
useCreateFeatureDependencyMutation,
191209
useDeleteFeatureDependencyMutation,
210+
useGetChangeRequestIdsStagingQuery,
192211
useGetFeatureDependenciesQuery,
193212
useGetFeatureDependentsQuery,
194213
// END OF EXPORTS

‎frontend/common/types/requests.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,7 @@ export type Req = {
908908
approvals: Approval[]
909909
}
910910
deleteChangeRequest: { id: number }
911+
getChangeRequestIdsStaging: { featureId: number }
911912
getSamlConfiguration: { name: string }
912913
getSamlConfigurations: { organisation_id: number }
913914
getSamlConfigurationMetadata: { name: string }

‎frontend/common/types/responses.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,6 +1627,7 @@ export type Res = {
16271627
featureDependencies: { results: DependencyEdge[] }
16281628
featureDependents: { results: DependencyEdge[] }
16291629
featureDependency: DependencyEdge
1630+
changeRequestIdsStaging: number[]
16301631
featureVersions: PagedResponse<FeatureVersion>
16311632
users: User[]
16321633
enableFeatureVersioning: { id: string }

‎frontend/documentation/components/Chip.stories.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export const Accent: Story = {
2828
args: { children: '"hello"', variant: 'accent' },
2929
}
3030

31+
export const Warning: Story = {
32+
args: { children: 'Awaiting approval', variant: 'warning' },
33+
}
34+
3135
export const Sizes: Story = {
3236
render: () => (
3337
<div className='d-flex align-items-center gap-2'>

‎frontend/web/components/base/Chip/Chip.scss‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
border-color: var(--color-border-action);
1414
}
1515

16+
&--warning {
17+
border-color: var(--color-border-warning);
18+
}
19+
1620
// Sizes (default is the base above).
1721
&--sm {
1822
padding: 3px 8px;

‎frontend/web/components/base/Chip/Chip.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { colorIconSecondary } from 'common/theme/tokens'
55
import './Chip.scss'
66

77
export type ChipSize = 'default' | 'sm' | 'xs'
8-
export type ChipVariant = 'neutral' | 'accent'
8+
export type ChipVariant = 'neutral' | 'accent' | 'warning'
99

1010
export type ChipProps = {
1111
children: ReactNode
@@ -31,6 +31,7 @@ export type ChipProps = {
3131
const VARIANT_UTILITIES: Record<ChipVariant, string> = {
3232
accent: 'bg-surface-action-subtle text-action',
3333
neutral: 'bg-surface-subtle text-default',
34+
warning: 'bg-surface-warning text-warning',
3435
}
3536

3637
// Token-based chip primitive. Uses `ds-chip` rather than the legacy `.chip`
@@ -61,7 +62,7 @@ const Chip = ({
6162
'ds-chip d-inline-flex align-items-center align-middle gap-1 rounded-sm',
6263
VARIANT_UTILITIES[variant],
6364
{
64-
'ds-chip--accent': variant === 'accent',
65+
[`ds-chip--${variant}`]: variant !== 'neutral',
6566
'ds-chip--clickable': interactive,
6667
[`ds-chip--${size}`]: size !== 'default',
6768
'ds-chip--truncate': truncate,

‎frontend/web/components/modals/create-feature/tabs/FeatureDependenciesTab/FeatureDependenciesTab.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import ChangeRequestFooter from './ChangeRequestFooter'
1616
import ChangeRequestNotice from './ChangeRequestNotice'
1717
import DependenciesHeader from './DependenciesHeader'
1818
import PrerequisitePicker from './PrerequisitePicker'
19+
import PendingChangeRequests from './PendingChangeRequests'
1920
import { getDependenciesMode } from './dependenciesMode'
2021
import { withStagedChanges } from './prerequisiteState'
2122
import { usePrerequisites } from './hooks/usePrerequisites'
@@ -209,6 +210,15 @@ const FeatureDependenciesTab: FC<FeatureDependenciesTabProps> = ({
209210
/>
210211
)}
211212

213+
{isChangeRequest && (
214+
<PendingChangeRequests
215+
environmentId={environmentId}
216+
featureId={projectFlag.id}
217+
isEnabled={isEnabled}
218+
projectId={projectId}
219+
/>
220+
)}
221+
212222
<ModalHR className='mt-4' />
213223

214224
<DependentFeatures
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { FC } from 'react'
2+
import { ChangeRequestSummary } from 'common/types/responses'
3+
import Accordion from 'components/base/Accordion'
4+
import ChangeRequestLink from 'components/modals/create-feature/tabs/FeatureDependenciesTab/ChangeRequestLink'
5+
import Chip from 'components/base/Chip'
6+
import Icon from 'components/icons/Icon'
7+
import { colorIconSecondary } from 'common/theme/tokens'
8+
import ErrorMessage from 'components/ErrorMessage'
9+
import ChangeRequestDependencyChanges from 'components/feature-dependencies/ChangeRequestDependencyChanges'
10+
import {
11+
describeStatus,
12+
isScheduled,
13+
} from 'components/modals/create-feature/tabs/FeatureDependenciesTab/pendingChangeRequestState'
14+
import { usePendingChangeRequests } from 'components/modals/create-feature/tabs/FeatureDependenciesTab/hooks/usePendingChangeRequests'
15+
16+
type PendingChangeRequestsProps = {
17+
environmentId: string
18+
featureId: number
19+
projectId: number
20+
isEnabled: (prerequisiteId: number) => boolean
21+
}
22+
23+
// Changes to this feature's prerequisites that are waiting in open or
24+
// scheduled change requests, one group per change request.
25+
const PendingChangeRequests: FC<PendingChangeRequestsProps> = ({
26+
environmentId,
27+
featureId,
28+
isEnabled,
29+
projectId,
30+
}) => {
31+
const { changeRequests: groups, isError } = usePendingChangeRequests({
32+
environmentId,
33+
featureId,
34+
})
35+
36+
if (isError) {
37+
return (
38+
<ErrorMessage error='Could not load the change requests waiting on this feature.' />
39+
)
40+
}
41+
42+
if (!groups.length) return null
43+
44+
return (
45+
<div className='mt-4'>
46+
<h6 className='mb-1'>Pending in change requests</h6>
47+
<p className='text-secondary mb-3'>
48+
Not live yet. These go live when their change request is published.
49+
</p>
50+
<div className='d-flex flex-column gap-3'>
51+
{groups.map((changeRequest) => (
52+
<Accordion
53+
key={changeRequest.id}
54+
defaultOpen
55+
flush
56+
title={
57+
<>
58+
<Icon
59+
name='request'
60+
width={16}
61+
fill={colorIconSecondary}
62+
aria-hidden
63+
/>
64+
#{changeRequest.id} {changeRequest.title}
65+
<StatusChip
66+
className='ms-auto flex-shrink-0'
67+
changeRequest={changeRequest}
68+
/>
69+
</>
70+
}
71+
>
72+
<ChangeRequestDependencyChanges
73+
environmentId={environmentId}
74+
featureId={featureId}
75+
changeRequestId={changeRequest.id}
76+
isEnabled={isEnabled}
77+
/>
78+
{/* In the body, as the header is the toggle and a link cannot
79+
sit inside a button. */}
80+
<div className='px-3 py-2'>
81+
<ChangeRequestLink
82+
projectId={projectId}
83+
environmentId={environmentId}
84+
changeRequestId={changeRequest.id}
85+
>
86+
View change request
87+
</ChangeRequestLink>
88+
</div>
89+
</Accordion>
90+
))}
91+
</div>
92+
</div>
93+
)
94+
}
95+
96+
// Waiting on people reads as a warning; a scheduled change only needs time.
97+
const StatusChip: FC<{
98+
changeRequest: ChangeRequestSummary
99+
className?: string
100+
}> = ({ changeRequest, className }) => (
101+
<Chip
102+
className={className}
103+
size='xs'
104+
variant={isScheduled(changeRequest) ? undefined : 'warning'}
105+
>
106+
{describeStatus(changeRequest)}
107+
</Chip>
108+
)
109+
110+
export default PendingChangeRequests
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './PendingChangeRequests'
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ChangeRequestSummary } from 'common/types/responses'
2+
import { describeStatus } from 'components/modals/create-feature/tabs/FeatureDependenciesTab/pendingChangeRequestState'
3+
4+
const changeRequest = (
5+
id: number,
6+
fields: Partial<ChangeRequestSummary> = {},
7+
): ChangeRequestSummary => ({
8+
committed_at: null,
9+
committed_by: null,
10+
created_at: '2026-10-13T09:00:00Z',
11+
deleted_at: null,
12+
description: '',
13+
id,
14+
live_from: null,
15+
readOnly: false,
16+
title: 'Add prerequisites',
17+
updated_at: '2026-10-13T09:00:00Z',
18+
user: 1,
19+
...fields,
20+
})
21+
22+
describe('describeStatus', () => {
23+
it('reads an approved change request with a live date as scheduled', () => {
24+
expect(
25+
describeStatus(
26+
changeRequest(1, {
27+
committed_at: '2026-10-13T10:00:00Z',
28+
live_from: '2026-10-20T09:30:00Z',
29+
}),
30+
),
31+
).toMatch(/^Scheduled for 20 Oct, /)
32+
})
33+
34+
it('reads anything else as awaiting approval', () => {
35+
expect(describeStatus(changeRequest(1))).toBe('Awaiting approval')
36+
})
37+
})

0 commit comments

Comments
 (0)