Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cc03ab7
Revert "fix: revert: "fix(features): restore FeaturesPage RTK Query m…
talissoncosta Jan 5, 2026
9e5ac0f
fix(features): sync FeatureListStore with search/filter state
talissoncosta Jan 6, 2026
a97a544
refactor(features): use shared FEATURES_PAGE_SIZE constant
talissoncosta Jan 6, 2026
459f824
fix(useProjectFlag): include multivariate_feature_state_values in env…
talissoncosta Jan 6, 2026
4409923
fix: mv id missing in creating cr (#6471)
Zaimwa9 Jan 6, 2026
ce6d4a4
test(e2e): add multivariate toggle and change request E2E tests
talissoncosta Jan 6, 2026
b8489d5
refactor(e2e): separate change request test into dedicated test file
talissoncosta Jan 7, 2026
26b73d8
test(e2e): add multivariate toggle from list click test
talissoncosta Jan 7, 2026
1e6a532
revert(e2e): remove flaky change request and list click tests
talissoncosta Jan 7, 2026
f0498f9
Merge branch 'main' into current branch
talissoncosta Jan 7, 2026
c63c82b
fix(features): add row-level loading indicator for toggle operations
talissoncosta Jan 7, 2026
d43774b
fix(features): add loading indicator when deleting a feature
talissoncosta Jan 7, 2026
77ea59a
refactor(features): consolidate row state into useFeatureRowState hook
talissoncosta Jan 7, 2026
0c9e37c
fix(features): use new MV weights in change request creation
talissoncosta Jan 7, 2026
1904bfd
fix(features): fix segment override MV weights 400 error
talissoncosta Jan 7, 2026
025a26c
fix(features): fix view mode toggle render issue
talissoncosta Jan 7, 2026
8a90e69
fix(features): use project.organisation for Groups filter orgId
talissoncosta Jan 8, 2026
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
40 changes: 40 additions & 0 deletions frontend/common/hooks/useFeatureListWithApiKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useMemo } from 'react'
import { skipToken } from '@reduxjs/toolkit/query'
import { useGetFeatureListQuery } from 'common/services/useProjectFlag'
import { useProjectEnvironments } from './useProjectEnvironments'
import { buildApiFilterParams } from 'common/utils/featureFilterParams'
import type { FilterState } from 'common/types/featureFilters'

/**
* Fetches filtered feature list, accepting environment API key instead of numeric ID.
*
* TODO: This wrapper will be removed once we standardize environmentId and environmentApiKey on RouteContext.
*/
export function useFeatureListWithApiKey(
filters: FilterState,
page: number,
environmentApiKey: string | undefined,
projectId: number | undefined,
): ReturnType<typeof useGetFeatureListQuery> {
const { getEnvironmentIdFromKey, isLoading: isLoadingEnvironments } =
useProjectEnvironments(projectId!)

const apiParams = useMemo(() => {
if (!environmentApiKey || !projectId || !getEnvironmentIdFromKey) {
return null
}
return buildApiFilterParams(
filters,
page,
environmentApiKey,
projectId,
getEnvironmentIdFromKey,
)
}, [filters, page, environmentApiKey, projectId, getEnvironmentIdFromKey])

return useGetFeatureListQuery(apiParams ?? skipToken, {
refetchOnFocus: true,
refetchOnMountOrArgChange: true,
skip: isLoadingEnvironments,
})
}
84 changes: 84 additions & 0 deletions frontend/common/hooks/usePageTracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useEffect } from 'react'

/**
* Options for configuring page tracking behavior.
*/
export type PageTrackingOptions = {
/** The page constant name from Constants.pages */
pageName: string
/** Context data for tracking and storage persistence */
context?: {
environmentId?: string
projectId?: number
organisationId?: number
}
/** Whether to save context to AsyncStorage (default: false) */
saveToStorage?: boolean
/** Custom dependencies for re-tracking on changes */
deps?: React.DependencyList
}

/**
* Unified hook for tracking page views with optional context persistence.
*
* Consolidates both page tracking and environment context storage into a single,
* flexible hook. Automatically calls API.trackPage and optionally persists
* environment context to AsyncStorage.
*
* @param options - Configuration object for page tracking
*
* @example
* ```tsx
* // Basic page tracking only
* usePageTracking({ pageName: Constants.pages.FEATURES })
*
* // With context and storage persistence
* usePageTracking({
* pageName: Constants.pages.FEATURES,
* context: { environmentId, projectId, organisationId },
* saveToStorage: true,
* })
*
* // With custom dependencies
* usePageTracking({
* pageName: Constants.pages.FEATURES,
* context: { projectId },
* deps: [projectId, someOtherDep],
* })
* ```
*/
export function usePageTracking(options: PageTrackingOptions): void {
const { context, deps = [], pageName, saveToStorage = false } = options

// Track page view
useEffect(() => {
if (typeof API !== 'undefined' && API.trackPage) {
API.trackPage(pageName)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)

// Persist environment context to storage if enabled
useEffect(() => {
if (saveToStorage && context) {
if (typeof AsyncStorage !== 'undefined' && AsyncStorage.setItem) {
AsyncStorage.setItem(
'lastEnv',
JSON.stringify({
environmentId: context.environmentId,
orgId: context.organisationId,
projectId: context.projectId,
}),
)
}
}
// We intentionally use individual properties instead of context object
// to prevent re-runs when object reference changes but values don't
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
saveToStorage,
context?.environmentId,
context?.organisationId,
context?.projectId,
])
}
58 changes: 58 additions & 0 deletions frontend/common/hooks/useProjectEnvironments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useCallback, useMemo } from 'react'
import { useGetEnvironmentsQuery } from 'common/services/useEnvironment'
import { useGetProjectQuery } from 'common/services/useProject'
import type { Environment, Project } from 'common/types/responses'

interface UseProjectEnvironmentsResult {
project: Project | undefined
environments: Environment[]
getEnvironmentIdFromKey: (apiKey: string) => number | undefined
getEnvironment: (apiKey: string) => Environment | undefined
isLoading: boolean
error: Error | undefined
}

/** Fetches project and environment data with accessor functions for API key lookups. */
export function useProjectEnvironments(
projectId: number,
): UseProjectEnvironmentsResult {
const {
data: project,
error: projectError,
isLoading: isLoadingProject,
} = useGetProjectQuery({ id: projectId }, { skip: !projectId })

const {
data: environmentsData,
error: environmentsError,
isLoading: isLoadingEnvironments,
} = useGetEnvironmentsQuery({ projectId }, { skip: !projectId })

const environments = useMemo(
() => environmentsData?.results ?? [],
[environmentsData?.results],
)

const getEnvironmentIdFromKey = useCallback(
(apiKey: string): number | undefined => {
return environments.find((env) => env.api_key === apiKey)?.id
},
[environments],
)

const getEnvironment = useCallback(
(apiKey: string): Environment | undefined => {
return environments.find((env) => env.api_key === apiKey)
},
[environments],
)

return {
environments,
error: projectError || environmentsError,
getEnvironment,
getEnvironmentIdFromKey,
isLoading: isLoadingProject || isLoadingEnvironments,
project,
}
}
36 changes: 20 additions & 16 deletions frontend/common/providers/FeatureListProvider.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import FeatureListStore from 'common/stores/feature-list-store'
import ProjectStore from 'common/stores/project-store'
import Utils from 'common/utils/utils'

const FeatureListProvider = class extends React.Component {
static displayName = 'FeatureListProvider'
Expand Down Expand Up @@ -147,7 +148,10 @@ const FeatureListProvider = class extends React.Component {
projectFlag,
{
...environmentFlag,
multivariate_feature_state_values: flag.multivariate_options,
multivariate_feature_state_values: Utils.mapMvOptionsToStateValues(
flag.multivariate_options,
environmentFlag.multivariate_feature_state_values,
),
},
segmentOverrides,
'SEGMENT',
Expand Down Expand Up @@ -193,30 +197,30 @@ const FeatureListProvider = class extends React.Component {
) => {
AppActions.editFeatureMv(
projectId,
Object.assign({}, projectFlag, flag, {
multivariate_options:
flag.multivariate_options &&
flag.multivariate_options.map((v) => {
const matchingProjectVariate =
(projectFlag.multivariate_options &&
projectFlag.multivariate_options.find((p) => p.id === v.id)) ||
v
Object.assign({}, projectFlag, flag),
(newProjectFlag) => {
const mvStateValues = newProjectFlag.multivariate_options?.map(
(mvOption) => {
const existing =
environmentFlag.multivariate_feature_state_values?.find(
(e) => e.multivariate_feature_option === mvOption.id,
)
return {
...v,
default_percentage_allocation:
matchingProjectVariate.default_percentage_allocation,
id: existing?.id,
multivariate_feature_option: mvOption.id,
percentage_allocation:
mvOption.default_percentage_allocation ?? 0,
}
}),
}),
(newProjectFlag) => {
},
)
AppActions.editEnvironmentFlagChangeRequest(
projectId,
environmentId,
flag,
newProjectFlag,
{
...environmentFlag,
multivariate_feature_state_values: flag.multivariate_options,
multivariate_feature_state_values: mvStateValues,
},
segmentOverrides,
changeRequest,
Expand Down
Loading
Loading