diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index e65a9a5c569e..bd873adef8a3 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -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', diff --git a/frontend/common/providers/withSegmentOverrides.js b/frontend/common/providers/withSegmentOverrides.js index 497624f4c17c..a958e8a51e70 100644 --- a/frontend/common/providers/withSegmentOverrides.js +++ b/frontend/common/providers/withSegmentOverrides.js @@ -27,6 +27,7 @@ export default (WrappedComponent) => { getOverrides = () => { if (this.props.projectFlag) { + //todo: migrate to useSegmentFeatureState Promise.all([ data.get( `${ diff --git a/frontend/common/services/useProjectFlag.ts b/frontend/common/services/useProjectFlag.ts index f5e6f6dc35ff..bcce029b7a97 100644 --- a/frontend/common/services/useProjectFlag.ts +++ b/frontend/common/services/useProjectFlag.ts @@ -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( diff --git a/frontend/common/services/useSegmentOverride.ts b/frontend/common/services/useSegmentOverride.ts index 5941581a068e..4ba22f8aad26 100644 --- a/frontend/common/services/useSegmentOverride.ts +++ b/frontend/common/services/useSegmentOverride.ts @@ -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'] }) @@ -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 }), diff --git a/frontend/common/stores/feature-list-store.ts b/frontend/common/stores/feature-list-store.ts index 13334f677097..eb41180eab36 100644 --- a/frontend/common/stores/feature-list-store.ts +++ b/frontend/common/stores/feature-list-store.ts @@ -12,6 +12,7 @@ import { updateProjectFlag, } from 'common/services/useProjectFlag' import OrganisationStore from './organisation-store' +import { SortOrder } from 'common/types/requests' import { ChangeRequest, Environment, @@ -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() @@ -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( @@ -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) => { diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index acfc25c178b1..2d0ddbfbec9d 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -23,6 +23,7 @@ import { StageTrigger, StageActionType, StageActionBody, + TagStrategy, } from './responses' import { UtmsType } from './utms' @@ -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 @@ -219,7 +223,7 @@ export type Req = { projectId: string } createTag: { projectId: string; tag: Omit } - getSegment: { projectId: string; id: string } + getSegment: { projectId: number; id: string } updateAccount: Account deleteAccount: { current_password: string @@ -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 } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 197691b3a31c..52c0a7898e23 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -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 diff --git a/frontend/web/components/Breadcrumb.tsx b/frontend/web/components/Breadcrumb.tsx index cc4434e8b0ba..ffc230626f30 100644 --- a/frontend/web/components/Breadcrumb.tsx +++ b/frontend/web/components/Breadcrumb.tsx @@ -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 = ({ currentPage, items }) => { +const Breadcrumb: FC = ({ + currentPage, + isCurrentPageMuted = true, + items, +}) => { return ( - + + ) : ( + currentPage + )} + ) } diff --git a/frontend/web/components/EnvironmentSelect.tsx b/frontend/web/components/EnvironmentSelect.tsx index 7de2f6cbf558..6b3800d47b7a 100644 --- a/frontend/web/components/EnvironmentSelect.tsx +++ b/frontend/web/components/EnvironmentSelect.tsx @@ -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> & { 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' @@ -30,6 +31,7 @@ const EnvironmentSelect: FC = ({ const environments = useMemo(() => { return (data?.results || []) ?.map((v) => ({ + environment: v, label: v.name, value: `${v[idField]}`, })) @@ -66,12 +68,14 @@ const EnvironmentSelect: FC = ({ } } 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)} /> ) diff --git a/frontend/web/components/PanelSearch.tsx b/frontend/web/components/PanelSearch.tsx index fb75c398a0cd..fecbf6aaf8e6 100644 --- a/frontend/web/components/PanelSearch.tsx +++ b/frontend/web/components/PanelSearch.tsx @@ -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 } @@ -56,7 +57,7 @@ export interface PanelSearchProps { className?: string onSortChange?: (args: { sortBy: string | null - sortOrder: 'asc' | 'desc' | null + sortOrder: SortOrder | null }) => void itemHeight?: number action?: ReactNode @@ -90,7 +91,7 @@ const PanelSearch = (props: PanelSearchProps): ReactElement => { const [sortBy, setSortBy] = useState( defaultSortingOption ? defaultSortingOption.value : null, ) - const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>( + const [sortOrder, setSortOrder] = useState( defaultSortingOption ? defaultSortingOption.order : null, ) const [internalSearch, setInternalSearch] = useState('') @@ -102,7 +103,11 @@ const PanelSearch = (props: PanelSearchProps): 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 }, @@ -127,7 +132,8 @@ const PanelSearch = (props: PanelSearchProps): ReactElement => { (e: React.MouseEvent, 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 { @@ -247,7 +253,9 @@ const PanelSearch = (props: PanelSearchProps): ReactElement => { {currentSort?.value === sortOption.value && ( )} diff --git a/frontend/web/components/ProjectManageWidget.tsx b/frontend/web/components/ProjectManageWidget.tsx index 7cd29e5e617f..69e4a0f62c09 100644 --- a/frontend/web/components/ProjectManageWidget.tsx +++ b/frontend/web/components/ProjectManageWidget.tsx @@ -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' @@ -229,7 +230,7 @@ const ProjectManageWidget: FC = ({ organisationId }) => { { default: true, label: 'Name', - order: 'asc', + order: SortOrder.ASC, value: 'name', }, ]} diff --git a/frontend/web/components/SegmentOverrides.js b/frontend/web/components/SegmentOverrides.js index 81f62416e61e..5d26c0a2e2ec 100644 --- a/frontend/web/components/SegmentOverrides.js +++ b/frontend/web/components/SegmentOverrides.js @@ -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) } diff --git a/frontend/web/components/SegmentSelect.tsx b/frontend/web/components/SegmentSelect.tsx index 3797bbbfd59e..020e3cfea366 100644 --- a/frontend/web/components/SegmentSelect.tsx +++ b/frontend/web/components/SegmentSelect.tsx @@ -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 = ({ + className, filter, projectId, ...rest @@ -41,12 +43,13 @@ const SegmentSelect: FC = ({