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
4 changes: 0 additions & 4 deletions frontend/common/dispatcher/action-constants.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import BaseActions from './base/_action-constants'
const Actions = Object.assign({}, BaseActions, {
'ACCEPT_INVITE': 'ACCEPT_INVITE',
'ACTION_CHANGE_REQUEST': 'ACTION_CHANGE_REQUEST',
'CHANGE_USER_FLAG': 'CHANGE_USER_FLAG',
'CONFIRM_TWO_FACTOR': 'CONFIRM_TWO_FACTOR',
'CREATE_ENV': 'CREATE_ENV',
'CREATE_FLAG': 'CREATE_FLAG',
'CREATE_ORGANISATION': 'CREATE_ORGANISATION',
'CREATE_PROJECT': 'CREATE_PROJECT',
'DELETE_CHANGE_REQUEST': 'DELETE_CHANGE_REQUEST',
'DELETE_ENVIRONMENT': 'DELETE_ENVIRONMENT',
'DELETE_ORGANISATION': 'DELETE_ORGANISATION',
'DELETE_PROJECT': 'DELETE_PROJECT',
Expand All @@ -24,7 +22,6 @@ const Actions = Object.assign({}, BaseActions, {
'EDIT_PROJECT': 'EDIT_PROJECT',
'EDIT_USER_FLAG': 'EDIT_USER_FLAG',
'ENABLE_TWO_FACTOR': 'ENABLE_TWO_FACTOR',
'GET_CHANGE_REQUEST': 'GET_CHANGE_REQUEST',
'GET_ENVIRONMENT': 'GET_ENVIRONMENT',
'GET_IDENTITY': 'GET_IDENTITY',
'GET_ORGANISATION': 'GET_ORGANISATION',
Expand All @@ -37,7 +34,6 @@ const Actions = Object.assign({}, BaseActions, {
'SELECT_ORGANISATION': 'SELECT_ORGANISATION',
'TOGGLE_USER_FLAG': 'TOGGLE_USER_FLAG',
'TWO_FACTOR_LOGIN': 'TWO_FACTOR_LOGIN',
'UPDATE_CHANGE_REQUEST': 'UPDATE_CHANGE_REQUEST',
'UPDATE_SUBSCRIPTION': 'UPDATE_SUBSCRIPTION',
'UPDATE_USER_ROLE': 'UPDATE_USER_ROLE',
})
Expand Down
29 changes: 0 additions & 29 deletions frontend/common/dispatcher/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ const AppActions = Object.assign({}, BaseAppActions, {
id,
})
},
actionChangeRequest(id, action, cb) {
Dispatcher.handleViewAction({
action,
actionType: Actions.ACTION_CHANGE_REQUEST,
cb,
id,
})
},
changeUserFlag(identity) {
Dispatcher.handleViewAction({
actionType: Actions.CHANGE_USER_FLAG,
Expand Down Expand Up @@ -59,13 +51,6 @@ const AppActions = Object.assign({}, BaseAppActions, {
})
},

deleteChangeRequest(id, cb) {
Dispatcher.handleViewAction({
actionType: Actions.DELETE_CHANGE_REQUEST,
cb,
id,
})
},
deleteEnv(env) {
Dispatcher.handleViewAction({
actionType: Actions.DELETE_ENVIRONMENT,
Expand Down Expand Up @@ -187,14 +172,6 @@ const AppActions = Object.assign({}, BaseAppActions, {
actionType: Actions.ENABLE_TWO_FACTOR,
})
},
getChangeRequest(id, projectId, environmentId) {
Dispatcher.handleViewAction({
actionType: Actions.GET_CHANGE_REQUEST,
environmentId,
id,
projectId,
})
},
getIdentity(envId, id) {
Dispatcher.handleViewAction({
actionType: Actions.GET_IDENTITY,
Expand Down Expand Up @@ -274,12 +251,6 @@ const AppActions = Object.assign({}, BaseAppActions, {
pin,
})
},
updateChangeRequest(changeRequest) {
Dispatcher.handleViewAction({
actionType: Actions.UPDATE_CHANGE_REQUEST,
changeRequest,
})
},
updateSubscription(hostedPageId) {
Dispatcher.handleViewAction({
actionType: Actions.UPDATE_SUBSCRIPTION,
Expand Down
98 changes: 98 additions & 0 deletions frontend/common/hooks/useChangeRequestDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
ChangeRequest,
FeatureState,
ProjectFlag,
} from 'common/types/responses'
import {
useActionChangeRequestMutation,
useDeleteChangeRequestMutation,
useGetChangeRequestQuery,
useUpdateChangeRequestAssigneesMutation,
} from 'common/services/useChangeRequest'
import { isNotFoundError, toErrorMessage } from 'common/utils/requestError'

export type ChangeRequestAssignees = Pick<
ChangeRequest,
'approvals' | 'group_assignments'
>

type ChangeRequestDetailParams = {
environmentId: string
id: string
projectId: string
}

export type ChangeRequestDetail = {
changeRequest?: ChangeRequest
environmentFlag?: FeatureState
projectFlag?: ProjectFlag
error?: unknown
isUpdating: boolean
notFound: boolean
approve: () => void
commit: (onDone: () => void) => void
remove: (onDone: () => void) => void
updateAssignees: (assignees: ChangeRequestAssignees) => void
}

export const useChangeRequestDetail = ({
environmentId,
id,
projectId,
}: ChangeRequestDetailParams): ChangeRequestDetail => {
const {
data,
error: loadError,
isFetching,
} = useGetChangeRequestQuery(
{ environmentId, id, projectId },
// Refetch after an action, not on window focus
{ refetchOnFocus: false },
)
const [action, actionState] = useActionChangeRequestMutation()
const [updateAssignees, updateState] =
useUpdateChangeRequestAssigneesMutation()
const [deleteChangeRequest, deleteState] = useDeleteChangeRequestMutation()

const isUpdating =
isFetching ||
actionState.isLoading ||
updateState.isLoading ||
deleteState.isLoading
const error =
actionState.error || updateState.error || deleteState.error || loadError
// Each mutation keeps its last error, so clear them when a new action starts
const clearErrors = () => {
actionState.reset()
updateState.reset()
deleteState.reset()
}

return {
approve: () => {
clearErrors()
action({ actionType: 'approve', id })
},
changeRequest: data?.changeRequest,
commit: async (onDone) => {
clearErrors()
const res = await action({ actionType: 'commit', id })
if (!('error' in res)) onDone()
},
environmentFlag: data?.environmentFlag,
error: toErrorMessage(error),
isUpdating,
notFound: !data && isNotFoundError(loadError),
projectFlag: data?.projectFlag,
remove: async (onDone) => {
clearErrors()
const res = await deleteChangeRequest({ id: Number(id) })
if (!('error' in res)) onDone()
},
updateAssignees: (assignees) => {
if (isUpdating || !data) return
clearErrors()
updateAssignees({ ...assignees, id })
},
}
}
138 changes: 138 additions & 0 deletions frontend/common/services/useChangeRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
ChangeRequest,
ChangeSet,
FeatureState,
PagedResponse,
ProjectFlag,
Res,
} from 'common/types/responses'
import { Req } from 'common/types/requests'
Expand All @@ -10,11 +12,42 @@ import Utils from 'common/utils/utils'
import sortBy from 'lodash/sortBy'
import moment from 'moment'
import transformCorePaging from 'common/transformCorePaging'
import { sortMultivariateOptions } from 'common/utils/multivariate'
import { isNotFoundError } from 'common/utils/requestError'
import {
addFeatureSegmentsToFeatureStates,
featureStateService,
} from './useFeatureState'

export const changeRequestService = service
.enhanceEndpoints({ addTagTypes: ['ChangeRequest'] })
.injectEndpoints({
endpoints: (builder) => ({
actionChangeRequest: builder.mutation<
Res['actionChangeRequest'],
Req['actionChangeRequest']
>({
invalidatesTags: (_, error, { id }) =>
error
? []
: [
{ id, type: 'ChangeRequest' },
{ id: 'LIST', type: 'ChangeRequest' },
],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
try {
await queryFulfilled
dispatch(featureStateService.util.invalidateTags(['FeatureState']))
} catch {
// The mutation's own error state reports the failure
}
},
query: ({ actionType, id }) => ({
body: {},
method: 'POST',
url: `features/workflows/change-requests/${id}/${actionType}/`,
}),
}),
createEnvironmentChangeRequest: builder.mutation<
Res['createEnvironmentChangeRequest'],
Req['createEnvironmentChangeRequest']
Expand Down Expand Up @@ -57,6 +90,60 @@ export const changeRequestService = service
url: `features/workflows/change-requests/${id}/`,
}),
}),
getChangeRequest: builder.query<
Res['changeRequestDetail'],
Req['getChangeRequest']
>({
providesTags: (_, _2, { id }) => [{ id, type: 'ChangeRequest' }],
queryFn: async (
{ environmentId, id, projectId },
_api,
_extraOptions,
baseQuery,
) => {
const res = await baseQuery({
url: `features/workflows/change-requests/${id}/`,
})
if (res.error) return { error: res.error }
const raw = res.data as ChangeRequest
const changeRequest: ChangeRequest = {
...raw,
feature_states: await Promise.all(
raw.feature_states.map(addFeatureSegmentsToFeatureStates),
),
}
const feature =
changeRequest.feature_states[0]?.feature ||
changeRequest.change_sets?.[0]?.feature
const [environmentFlags, projectFlagRes] = await Promise.all([
baseQuery({
url: `environments/${environmentId}/featurestates/?feature=${feature}`,
}),
baseQuery({ url: `projects/${projectId}/features/${feature}/` }),
])
// A 404 means the feature was deleted since, which the page explains
if (isNotFoundError(projectFlagRes.error)) {
return { data: { changeRequest } }
}
if (projectFlagRes.error) return { error: projectFlagRes.error }
if (environmentFlags.error) return { error: environmentFlags.error }
const projectFlag = projectFlagRes.data as ProjectFlag
return {
data: {
changeRequest,
environmentFlag: (
environmentFlags.data as PagedResponse<FeatureState>
).results[0],
projectFlag: {
...projectFlag,
multivariate_options: sortMultivariateOptions(
projectFlag.multivariate_options,
),
},
},
}
},
}),
getChangeRequests: builder.query<
Res['changeRequests'],
Req['getChangeRequests']
Expand All @@ -80,6 +167,43 @@ export const changeRequestService = service
url: `features/workflows/change-requests/${changeRequest.id}/`,
}),
}),
updateChangeRequestAssignees: builder.mutation<
Res['updateChangeRequest'],
Req['updateChangeRequestAssignees']
>({
invalidatesTags: (_, error, { id }) =>
error
? []
: [
{ id: `${id}`, type: 'ChangeRequest' },
{ id: 'LIST', type: 'ChangeRequest' },
],
// Re-read first so the PUT carries the stored change, not the page's copy
queryFn: async (
{ approvals, group_assignments, id },
_api,
_extraOptions,
baseQuery,
) => {
const url = `features/workflows/change-requests/${id}/`
const res = await baseQuery({ url })
if (res.error) return { error: res.error }
const current = res.data as ChangeRequest
const put = await baseQuery({
body: {
...current,
approvals,
environment_feature_versions:
current.environment_feature_versions?.map((v) => v.uuid),
group_assignments,
},
method: 'PUT',
url,
})
if (put.error) return { error: put.error }
return { data: put.data as Res['updateChangeRequest'] }
},
}),
// END OF ENDPOINTS
}),
})
Expand All @@ -106,12 +230,26 @@ export async function updateChangeRequest(
changeRequestService.endpoints.updateChangeRequest.initiate(data, options),
)
}
export async function actionChangeRequest(
store: any,
data: Req['actionChangeRequest'],
options?: Parameters<
typeof changeRequestService.endpoints.actionChangeRequest.initiate
>[1],
) {
return store.dispatch(
changeRequestService.endpoints.actionChangeRequest.initiate(data, options),
)
}
// END OF FUNCTION_EXPORTS

export const {
useActionChangeRequestMutation,
useCreateEnvironmentChangeRequestMutation,
useDeleteChangeRequestMutation,
useGetChangeRequestQuery,
useGetChangeRequestsQuery,
useUpdateChangeRequestAssigneesMutation,
useUpdateChangeRequestMutation,
// END OF EXPORTS
} = changeRequestService
Expand Down
Loading
Loading