Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
80811d5
Componentise feature filters
kyle-ssg Sep 30, 2025
7971ac7
Merge branch 'chore/componentise-feature-override-row' into chore/com…
kyle-ssg Sep 30, 2025
ce67965
Merge branch 'chore/componentise-feature-override-row' into chore/com…
kyle-ssg Oct 7, 2025
12eee3a
Make filtering consistent across user and features page
kyle-ssg Oct 7, 2025
f061a48
simplify filters
kyle-ssg Oct 7, 2025
e441e03
Merge branch 'chore/componentise-feature-override-row' into chore/com…
kyle-ssg Oct 7, 2025
10719cb
re-add loading logic
kyle-ssg Oct 7, 2025
b790d58
Simplify loading logic
kyle-ssg Oct 7, 2025
17ea4ba
Don't assume code references exist
kyle-ssg Oct 7, 2025
772e147
Don't assume code references exist
kyle-ssg Oct 7, 2025
f3ba094
fix text wrapping
kyle-ssg Oct 7, 2025
7b18f28
Fix tag filter in url
kyle-ssg Oct 7, 2025
b4724ca
Merge branch 'chore/componentise-feature-filters' into feat/segment-f…
kyle-ssg Oct 7, 2025
eb0be4b
Update frontend/web/components/feature-page/FeatureFilters.tsx
kyle-ssg Oct 14, 2025
6174bd5
Remove releasePipelines
kyle-ssg Oct 14, 2025
9b562f6
Merge remote-tracking branch 'origin/chore/componentise-feature-filte…
kyle-ssg Oct 14, 2025
dbb085a
remove sortToHeader
kyle-ssg Oct 14, 2025
29bae85
Merge branch 'chore/componentise-feature-override-row' into chore/com…
kyle-ssg Oct 21, 2025
162daef
Fix table value filter
kyle-ssg Oct 21, 2025
8b6d03d
Merge branch 'chore/componentise-feature-filters' into feat/segment-f…
kyle-ssg Oct 21, 2025
2058123
Fixes
kyle-ssg Oct 21, 2025
a1c5d83
Use unified endpoint for segment feature states
kyle-ssg Oct 21, 2025
143f02e
namespace segment tab
kyle-ssg Oct 21, 2025
dcb6632
Merge branch 'feat/segment-feature-state-view' into feat/segment-feat…
kyle-ssg Oct 21, 2025
6934c5f
Tests
kyle-ssg Oct 21, 2025
72e8214
Clean up types
kyle-ssg Oct 21, 2025
ee90b70
Merge branch 'chore/componentise-feature-override-row' into feat/segm…
kyle-ssg Oct 28, 2025
ae76a46
Merge branch 'chore/componentise-feature-override-row' into feat/segm…
kyle-ssg Nov 11, 2025
1b90569
Remove !
kyle-ssg Nov 11, 2025
7dcb60c
Merge branch 'chore/componentise-feature-override-row' into feat/segm…
kyle-ssg Nov 11, 2025
0ed455b
Unused import
kyle-ssg Nov 11, 2025
1381fb0
Fix sort order types
kyle-ssg Nov 11, 2025
0ac5ded
Re-add remove segment button
kyle-ssg Nov 11, 2025
9ef5383
Fix sortorder functionality
kyle-ssg Nov 11, 2025
2ed1777
QA feedback
kyle-ssg Nov 11, 2025
e800bcc
QA feedback
kyle-ssg Nov 11, 2025
24572d7
Adjust environment select
kyle-ssg Nov 11, 2025
212de78
Adjust fetching and loading logic
kyle-ssg Nov 11, 2025
f55b8ef
Adjust fetching and loading logic
kyle-ssg Nov 11, 2025
4974a76
Prevent toggle feature from segment page
kyle-ssg Nov 16, 2025
1a6b706
Merge branch 'chore/componentise-feature-override-row' into feat/segm…
kyle-ssg Nov 18, 2025
62262c1
clean up confirm toggle function
kyle-ssg Nov 18, 2025
5b9588a
fix vertical alignment for feature row
kyle-ssg Nov 18, 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
2 changes: 1 addition & 1 deletion frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ module.exports = {
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-var-requires': 'off',
'default-case': 'error',
'dot-notation': 'error',
'eqeqeq': 'warn',
Expand Down
1 change: 1 addition & 0 deletions frontend/common/providers/withSegmentOverrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default (WrappedComponent) => {

getOverrides = () => {
if (this.props.projectFlag) {
//todo: migrate to useSegmentFeatureState
Promise.all([
data.get(
`${
Expand Down
5 changes: 4 additions & 1 deletion frontend/common/services/useProjectFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ export const projectFlagService = service
Req['getProjectFlags']
>({
providesTags: (res, _, req) => [
{ id: req?.project, type: 'ProjectFlag' },
{
id: `${req?.project}-${req?.environmentId}-${req?.segmentId}`,
type: 'ProjectFlag',
},
],
queryFn: async (args, _, _2, baseQuery) => {
return await recursivePageGet(
Expand Down
8 changes: 8 additions & 0 deletions frontend/common/services/useSegmentOverride.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'
import { projectFlagService } from './useProjectFlag'
import { getStore } from 'common/store'

export const segmentOverrideService = service
.enhanceEndpoints({ addTagTypes: ['SegmentOverride'] })
Expand All @@ -16,6 +18,12 @@ export const segmentOverrideService = service
method: 'POST',
url: `environments/${query.environmentId}/features/${query.featureId}/create-segment-override/`,
}),
transformResponse: (res) => {
getStore().dispatch(
projectFlagService.util.invalidateTags(['ProjectFlag']),
)
return res
},
}),
// END OF ENDPOINTS
}),
Expand Down
12 changes: 9 additions & 3 deletions frontend/common/stores/feature-list-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
updateProjectFlag,
} from 'common/services/useProjectFlag'
import OrganisationStore from './organisation-store'
import { SortOrder } from 'common/types/requests'
import {
ChangeRequest,
Environment,
Expand Down Expand Up @@ -167,7 +168,7 @@ const controller = {
if (onComplete) {
onComplete(res)
}
if (store.model) {
if (store.model?.features) {
const index = _.findIndex(store.model.features, { id: flag.id })
store.model.features[index] = controller.parseFlag(flag)
store.model.lastSaved = new Date().valueOf()
Expand Down Expand Up @@ -435,7 +436,7 @@ const controller = {

Promise.all([prom, segmentOverridesRequest])
.then(([res, segmentRes]) => {
if (store.model) {
if (store.model?.keyedEnvironmentFeatures) {
store.model.keyedEnvironmentFeatures[projectFlag.id] = res
if (segmentRes) {
const feature = _.find(
Expand Down Expand Up @@ -974,7 +975,12 @@ const store = Object.assign({}, BaseStore, {
},
id: 'features',
paging: {},
sort: { default: true, label: 'Name', sortBy: 'name', sortOrder: 'asc' },
sort: {
default: true,
label: 'Name',
sortBy: 'name',
sortOrder: SortOrder.ASC,
},
})

store.dispatcherIndex = Dispatcher.register(store, (payload) => {
Expand Down
23 changes: 19 additions & 4 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
StageTrigger,
StageActionType,
StageActionBody,
TagStrategy,
} from './responses'
import { UtmsType } from './utms'

Expand Down Expand Up @@ -84,7 +85,10 @@ export type RegisterRequest = {
marketing_consent_given?: boolean
utm_data?: UtmsType
}

export enum SortOrder {
ASC = 'ASC',
DESC = 'DESC',
}
export interface StageActionRequest {
action_type: StageActionType | ''
action_body: StageActionBody
Expand Down Expand Up @@ -219,7 +223,7 @@ export type Req = {
projectId: string
}
createTag: { projectId: string; tag: Omit<Tag, 'id'> }
getSegment: { projectId: string; id: string }
getSegment: { projectId: number; id: string }
updateAccount: Account
deleteAccount: {
current_password: string
Expand Down Expand Up @@ -320,9 +324,20 @@ export type Req = {
}
getProjectFlags: {
project: string
environmentId?: string
tags?: string[]
environment?: number
segment?: number
search?: string | null
releasePipelines?: number[]
page?: number
tag_strategy?: TagStrategy
tags?: string
is_archived?: boolean
value_search?: string | null
is_enabled?: boolean | null
owners?: number[]
group_owners?: number[]
sort_field?: string
sort_direction?: SortOrder
}
getProjectFlag: { project: string | number; id: string }
getRolesPermissionUsers: { organisation_id: number; role_id: number }
Expand Down
2 changes: 2 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,8 @@ export type ProjectFlag = {
created_date: string
default_enabled: boolean
description?: string
environment_feature_state?: FeatureState
segment_feature_state?: FeatureState
id: number
initial_value: FlagsmithValue
is_archived: boolean
Expand Down
42 changes: 25 additions & 17 deletions frontend/web/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
import React, { FC } from 'react'
import React, { FC, ReactNode } from 'react'
import { Link } from 'react-router-dom'

type BreadcrumbType = {
items: { title: string; url: string }[]
currentPage: string
currentPage: ReactNode
isCurrentPageMuted?: boolean
}

const Breadcrumb: FC<BreadcrumbType> = ({ currentPage, items }) => {
const Breadcrumb: FC<BreadcrumbType> = ({
currentPage,
isCurrentPageMuted = true,
items,
}) => {
return (
<nav aria-label='breadcrumb'>
<ol className='breadcrumb my-2 py-1'>
{items?.map((item) => (
<li key={item.url} className='breadcrumb-item h6 fs-lg lh-sm'>
<Link className='text-primary' to={item.url}>
{item.title}
</Link>
</li>
))}
<li
className='breadcrumb-item active h6 text-muted lh-sm '
<div className='d-flex align-items-center my-2 py-1'>
{items?.map((item) => (
<>
<Link className='text-primary h6 mb-0' to={item.url}>
{item.title}
</Link>
<div className='text-muted mx-2 h6 mb-0'>/</div>
</>
))}
{isCurrentPageMuted ? (
<div
className='active h6 text-muted lh-sm '
aria-current='page'
style={{ opacity: 0.6 }}
>
{currentPage}
</li>
</ol>
</nav>
</div>
) : (
currentPage
)}
</div>
)
}

Expand Down
14 changes: 9 additions & 5 deletions frontend/web/components/EnvironmentSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { FC, useMemo } from 'react'
import { useGetEnvironmentsQuery } from 'common/services/useEnvironment'
import { Props } from 'react-select/lib/Select'
import { Environment } from 'common/types/responses'

export type EnvironmentSelectType = Partial<Omit<Props, 'value'>> & {
projectId: number
value?: string
label?: string
onChange: (value: string) => void
onChange: (value: string, environment: Environment | null) => void
showAll?: boolean
readOnly?: boolean
idField?: 'id' | 'api_key'
Expand All @@ -30,6 +31,7 @@ const EnvironmentSelect: FC<EnvironmentSelectType> = ({
const environments = useMemo(() => {
return (data?.results || [])
?.map((v) => ({
environment: v,
label: v.name,
value: `${v[idField]}`,
}))
Expand Down Expand Up @@ -66,12 +68,14 @@ const EnvironmentSelect: FC<EnvironmentSelectType> = ({
}
}
options={(showAll
? [{ label: 'All Environments', value: '' }]
? [{ environment: null, label: 'All Environments', value: '' }]
: []
).concat(environments)}
onChange={(value: { value: string; label: string }) =>
onChange(value?.value || '')
}
onChange={(value: {
value: string
label: string
environment: Environment
}) => onChange(value?.value || '', value?.environment)}
/>
</div>
)
Expand Down
20 changes: 14 additions & 6 deletions frontend/web/components/PanelSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ import Paging from './Paging'
import _ from 'lodash'
import Panel from './base/grid/Panel'
import Utils from 'common/utils/utils'
import { SortOrder } from 'common/types/requests'

export type SortOption = {
value: string
order: 'asc' | 'desc'
order: SortOrder
default?: boolean
label: string
}
Expand Down Expand Up @@ -56,7 +57,7 @@ export interface PanelSearchProps<T> {
className?: string
onSortChange?: (args: {
sortBy: string | null
sortOrder: 'asc' | 'desc' | null
sortOrder: SortOrder | null
}) => void
itemHeight?: number
action?: ReactNode
Expand Down Expand Up @@ -90,7 +91,7 @@ const PanelSearch = <T,>(props: PanelSearchProps<T>): ReactElement => {
const [sortBy, setSortBy] = useState<string | null>(
defaultSortingOption ? defaultSortingOption.value : null,
)
const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(
const [sortOrder, setSortOrder] = useState<SortOrder | null>(
defaultSortingOption ? defaultSortingOption.order : null,
)
const [internalSearch, setInternalSearch] = useState<string>('')
Expand All @@ -102,7 +103,11 @@ const PanelSearch = <T,>(props: PanelSearchProps<T>): ReactElement => {
const sortItems = useCallback(
(itemsToSort: T[]): T[] => {
if (sortBy) {
return _.orderBy(itemsToSort, [sortBy], [sortOrder || 'asc'])
return _.orderBy(
itemsToSort,
[sortBy],
[(sortOrder?.toLowerCase() || 'asc') as 'asc' | 'desc'],
)
}
return itemsToSort
},
Expand All @@ -127,7 +132,8 @@ const PanelSearch = <T,>(props: PanelSearchProps<T>): ReactElement => {
(e: React.MouseEvent<HTMLAnchorElement>, sortOption: SortOption) => {
e.preventDefault()
if (sortOption.value === sortBy) {
const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc'
const newSortOrder =
sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC
setSortOrder(newSortOrder)
onSortChange && onSortChange({ sortBy, sortOrder: newSortOrder })
} else {
Expand Down Expand Up @@ -247,7 +253,9 @@ const PanelSearch = <T,>(props: PanelSearchProps<T>): ReactElement => {
{currentSort?.value === sortOption.value && (
<IonIcon
icon={
sortOrder === 'asc' ? chevronUp : chevronDown
sortOrder === SortOrder.ASC
? chevronUp
: chevronDown
}
/>
)}
Expand Down
3 changes: 2 additions & 1 deletion frontend/web/components/ProjectManageWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ConfigProvider from 'common/providers/ConfigProvider'
import { useGetOrganisationsQuery } from 'common/services/useOrganisation'
import OrganisationProvider from 'common/providers/OrganisationProvider'
import { Project } from 'common/types/responses'
import { SortOrder } from 'common/types/requests'
import Button from './base/forms/Button'
import PanelSearch from './PanelSearch'
import Icon from './Icon'
Expand Down Expand Up @@ -229,7 +230,7 @@ const ProjectManageWidget: FC<SegmentsPageType> = ({ organisationId }) => {
{
default: true,
label: 'Name',
order: 'asc',
order: SortOrder.ASC,
value: 'name',
},
]}
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/SegmentOverrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ class TheComponent extends Component {
data-test='select-segment'
placeholder='Create a Segment Override...'
filter={filter}
value={this.state.selectedSegment}
value={this.state.selectedSegment?.value}
onChange={(selectedSegment) =>
this.setState({ selectedSegment }, this.addItem)
}
Expand Down
5 changes: 4 additions & 1 deletion frontend/web/components/SegmentSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ type SegmentSelectType = {
projectId: string
'data-test'?: string
placeholder?: string
className?: string
value: SelectProps['value']
onChange: SelectProps['onChange']
filter?: (segments: Segment) => Segment[]
}

const SegmentSelect: FC<SegmentSelectType> = ({
className,
filter,
projectId,
...rest
Expand All @@ -41,12 +43,13 @@ const SegmentSelect: FC<SegmentSelectType> = ({
<Select
data-test={rest['data-test']}
placeholder={rest.placeholder}
value={rest.value}
value={rest.value ? options.find((v) => v.value === rest.value) : null}
isDisabled={rest.disabled}
onChange={rest.onChange}
onInputChange={(e: any) => {
searchItems(Utils.safeParseEventValue(e))
}}
className={className}
components={{
Menu: ({ ...props }: any) => {
return (
Expand Down
Loading
Loading