Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3c60f52
refactor: migrate project settings to TS and break into smaller compo…
talissoncosta Nov 19, 2025
3fb783c
refactor: standardize organisationId type and improve error handling
talissoncosta Nov 19, 2025
9787499
refactor: add check on update func and clean up comments
talissoncosta Nov 19, 2025
441fe25
feat: add small transition on hide/show component
talissoncosta Nov 19, 2025
e31e205
fix: add disable state for delete project button
talissoncosta Nov 19, 2025
30c96be
fix: remove unused types
talissoncosta Nov 19, 2025
3bb76e2
fix: fix permissions data
talissoncosta Nov 19, 2025
c076140
refactor: simplify import tab passing project name and id
talissoncosta Nov 19, 2025
590ba2e
refactor: create useUpdateProjectWithToast custom hook
talissoncosta Nov 19, 2025
b561723
refactor: migrate settings components to use custom hook
talissoncosta Nov 19, 2025
5dc862b
refactor: use Pick for UpdateProjectBody type definition
talissoncosta Nov 20, 2025
38a9644
feat: add optimistic updates to project mutation
talissoncosta Nov 20, 2025
d7ee032
refactor: remove manual state syncing from project settings components
talissoncosta Nov 20, 2025
414c9ce
refactor: extract change requests setting into separate component
talissoncosta Nov 20, 2025
bd3d40b
refactor: remove unnecessary useCallback wrappers
talissoncosta Nov 20, 2025
5717852
refactor: small adjusts
talissoncosta Nov 20, 2025
391922d
fix: sync OrganisationStore after project updates for navbar refresh
talissoncosta Nov 20, 2025
0065414
fix: address PR review comments - type safety and code organization
talissoncosta Nov 20, 2025
bd6b2a1
fix: don't re-throw error
talissoncosta Nov 20, 2025
7e30d7b
fix: adjust index files on internal pages
talissoncosta Nov 21, 2025
4aaf163
fix: restore original toast messages and button states in project set…
talissoncosta Nov 21, 2025
e63638f
fix: correct inverted logic in case sensitivity toggle
talissoncosta Nov 21, 2025
6437dcd
fix: resolve state and persistence issues in ChangeRequestsApprovalsS…
talissoncosta Nov 24, 2025
f418d8b
feat: add data-test prop support to ChangeRequestsSetting
talissoncosta Nov 24, 2025
2066337
test: add comprehensive E2E coverage for change request approvals
talissoncosta Nov 24, 2025
fd418ba
fix: add space between label and input on stale flag detection
talissoncosta Nov 24, 2025
4fe578a
refactor: wrap change requests e2e tests in conditional block for sca…
talissoncosta Nov 24, 2025
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
56 changes: 56 additions & 0 deletions frontend/common/services/useProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,71 @@ export const projectService = service
.enhanceEndpoints({ addTagTypes: ['Project'] })
.injectEndpoints({
endpoints: (builder) => ({
deleteProject: builder.mutation<void, Req['deleteProject']>({
invalidatesTags: [{ id: 'LIST', type: 'Project' }],
query: ({ id }: Req['deleteProject']) => ({
method: 'DELETE',
url: `projects/${id}/`,
}),
}),
getProject: builder.query<Res['project'], Req['getProject']>({
providesTags: (res) => [{ id: res?.id, type: 'Project' }],
query: (query: Req['getProject']) => ({
url: `projects/${query.id}/`,
}),
}),
getProjectPermissions: builder.query<
Res['userPermissions'],
Req['getProjectPermissions']
>({
query: ({ projectId }: Req['getProjectPermissions']) => ({
url: `projects/${projectId}/user-permissions/`,
}),
}),
getProjects: builder.query<Res['projects'], Req['getProjects']>({
providesTags: [{ id: 'LIST', type: 'Project' }],
query: (data) => ({
url: `projects/?organisation=${data.organisationId}`,
}),
transformResponse: (res) => sortBy(res, (v) => v.name.toLowerCase()),
}),
migrateProject: builder.mutation<void, Req['migrateProject']>({
invalidatesTags: (res, error, { id }) => [{ id, type: 'Project' }],
query: ({ id }: Req['migrateProject']) => ({
method: 'POST',
url: `projects/${id}/migrate-to-edge/`,
}),
}),
updateProject: builder.mutation<Res['project'], Req['updateProject']>({
invalidatesTags: (res) => [
{ id: res?.id, type: 'Project' },
{ id: 'LIST', type: 'Project' },
],
async onQueryStarted({ body, id }, { dispatch, queryFulfilled }) {
// Optimistically update the cache before server responds
const patchResult = dispatch(
projectService.util.updateQueryData(
'getProject',
{ id },
(draft) => {
Object.assign(draft, body)
},
),
)

try {
await queryFulfilled
} catch {
// Automatically rollback on error
patchResult.undo()
}
},
query: ({ body, id }: Req['updateProject']) => ({
body,
method: 'PUT',
url: `projects/${id}/`,
}),
}),
// END OF ENDPOINTS
}),
})
Expand Down Expand Up @@ -47,8 +99,12 @@ export async function getProject(
// END OF FUNCTION_EXPORTS

export const {
useDeleteProjectMutation,
useGetProjectPermissionsQuery,
useGetProjectQuery,
useGetProjectsQuery,
useMigrateProjectMutation,
useUpdateProjectMutation,
// END OF EXPORTS
} = projectService

Expand Down
16 changes: 15 additions & 1 deletion frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
UserGroup,
AttributeName,
Identity,
ChangeRequest,
ProjectChangeRequest,
Role,
RolePermission,
Expand All @@ -27,6 +26,17 @@ import {
} from './responses'
import { UtmsType } from './utms'

export type UpdateProjectBody = {
name: string
hide_disabled_flags?: boolean
prevent_flag_defaults?: boolean
enable_realtime_updates?: boolean
minimum_change_request_approvals?: number | null
stale_flags_limit_days?: number | null
only_allow_lower_case_feature_names?: boolean
feature_name_regex?: string | null
}

export type PagedRequest<T> = T & {
page?: number
page_size?: number
Expand Down Expand Up @@ -580,6 +590,10 @@ export type Req = {
id: string
}
getProject: { id: string }
updateProject: { id: string; body: UpdateProjectBody }
deleteProject: { id: string }
migrateProject: { id: string }
getProjectPermissions: { projectId: string }
createGroup: {
orgId: string
data: Omit<UserGroup, 'id' | 'users'>
Expand Down
4 changes: 3 additions & 1 deletion frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ export type Project = {
total_features?: number
stale_flags_limit_days?: number
total_segments?: number
only_allow_lower_case_feature_names?: boolean
feature_name_regex?: string | null
environments: Environment[]
}
export type ImportStrategy = 'SKIP' | 'OVERWRITE_DESTRUCTIVE'
Expand Down Expand Up @@ -1104,7 +1106,7 @@ export type Res = {
}
profile: User
onboarding: {}
userPermissions: UserPermissions
userPermissions: UserPermission[]
releasePipelines: PagedResponse<ReleasePipeline>
releasePipeline: SingleReleasePipeline
pipelineStages: PagedResponse<PipelineStage>
Expand Down
45 changes: 45 additions & 0 deletions frontend/e2e/tests/project-test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import {
assertInputValue,
assertTextContent,
byId,
click,
getFlagsmith,
log,
login,
setText,
waitForElementNotExist,
waitForElementVisible,
} from '../helpers.cafe';
import { E2E_USER, PASSWORD } from '../config'

export default async function () {
const flagsmith = await getFlagsmith()
const hasSegmentChangeRequests = flagsmith.hasFeature('segment_change_requests')

log('Login')
await login(E2E_USER, PASSWORD)
await click('#project-select-0')
Expand All @@ -20,4 +26,43 @@ export default async function () {
await click('#save-proj-btn')
await assertTextContent(`#project-link`, 'Test Project')

if (hasSegmentChangeRequests) {
log('Test Change Requests Approvals Setting')

log('Test 1: Enable change requests (auto-save on toggle)')
await click('[data-test="js-change-request-approvals"]')
await waitForElementVisible('[name="env-name"]')
log('Verify auto-save persisted after navigation')
await click('#features-link')
await click('#project-settings-link')
await waitForElementVisible('[name="env-name"]')

log('Test 2: Change minimum approvals to 3 (manual save)')
await setText('[name="env-name"]', '3')
await click('#save-env-btn')
log('Verify value 3 persisted after navigation')
await click('#features-link')
await click('#project-settings-link')
await waitForElementVisible('[name="env-name"]')
await assertInputValue('[name="env-name"]', '3')

log('Test 3: Disable change requests (auto-save on toggle)')
await click('[data-test="js-change-request-approvals"]')
log('Verify disabled state persisted after navigation')
await click('#features-link')
await click('#project-settings-link')
await waitForElementNotExist('[name="env-name"]')

log('Test 4: Re-enable and change to 5 (manual save)')
await click('[data-test="js-change-request-approvals"]')
await waitForElementVisible('[name="env-name"]')
await setText('[name="env-name"]', '5')
await click('#save-env-btn')
log('Verify value 5 persisted after navigation')
await click('#features-link')
await click('#project-settings-link')
await waitForElementVisible('[name="env-name"]')
await assertInputValue('[name="env-name"]', '5')
}

}
13 changes: 13 additions & 0 deletions frontend/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,24 @@ declare global {
const Select: typeof _Select
const Column: typeof Component
const Loader: typeof Component
const Input: typeof Component
const Button: typeof Component
const E2E: boolean
const closeModal: () => void
const closeModal2: () => void
const toast: (message: string) => void
const Tooltip: FC<TooltipProps>
const API: {
trackPage: (title: string) => void
trackEvent: (data: {
category: string
event: string
label?: string
extra?: Record<string, any>
}) => void
trackTraits: (traits: Record<string, any>) => void
[key: string]: any
}
interface Window {
$crisp: Crisp
engagement: {
Expand Down
3 changes: 3 additions & 0 deletions frontend/web/components/ChangeRequestsSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ type ChangeRequestsSettingType = {
onChange: (value: number | null) => void
isLoading: boolean
feature: '4_EYES' | '4_EYES_PROJECT'
'data-test'?: string
}

const ChangeRequestsSetting: FC<ChangeRequestsSettingType> = ({
'data-test': dataTest,
feature,
isLoading,
onChange,
Expand All @@ -26,6 +28,7 @@ const ChangeRequestsSetting: FC<ChangeRequestsSettingType> = ({
return (
<FormGroup className='mt-4 col-md-8'>
<Setting
data-test={dataTest}
feature={feature}
checked={has4EyesPermission && Utils.changeRequestsEnabled(value)}
onChange={(v) => onToggle(v ? 0 : null)}
Expand Down
Loading
Loading