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 ad3c747bc0cb..1b44eaa9fdc8 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, @@ -170,7 +171,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() @@ -438,7 +439,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( @@ -729,6 +730,12 @@ const controller = { if (version.error) { throw version.error } + getStore().dispatch( + projectFlagService.util.invalidateTags(['ProjectFlag']), + ) + if(!store.model) { + return + } // Fetch and update the latest environment feature state return getVersionFeatureState(getStore(), { environmentId: ProjectStore.getEnvironmentIdFromKey(environmentId), @@ -977,7 +984,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 c7f59b6b9a29..c2dbbeddfa7a 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' @@ -102,7 +103,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 @@ -240,7 +244,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 @@ -341,9 +345,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 4fde1a121f0c..22b375c3179b 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -506,6 +506,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..ff6e896bba21 100644 --- a/frontend/web/components/SegmentOverrides.js +++ b/frontend/web/components/SegmentOverrides.js @@ -16,9 +16,11 @@ import Icon from './Icon' import SegmentOverrideLimit from './SegmentOverrideLimit' import { getStore } from 'common/store' import { getEnvironment } from 'common/services/useEnvironment' +import { getSegment } from 'common/services/useSegment' import Tooltip from './Tooltip' import SegmentsIcon from './svg/SegmentsIcon' import SegmentOverrideActions from './SegmentOverrideActions' +import Button from './base/forms/Button' const arrayMoveMutate = (array, from, to) => { array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]) @@ -49,6 +51,7 @@ const SegmentOverrideInner = class Override extends React.Component { disabled, environmentId, hideViewSegment, + highlightSegmentId, index, multivariateOptions, name, @@ -82,6 +85,7 @@ const SegmentOverrideInner = class Override extends React.Component { const changed = !v.id || this.state.changed const showValue = !(multivariateOptions && multivariateOptions.length) const controlPercent = Utils.calculateControl(mvOptions) + const isHighlighted = highlightSegmentId && v.segment === highlightSegmentId if (!v || v.toRemove) { if (this.props.id) { return ( @@ -101,7 +105,7 @@ const SegmentOverrideInner = class Override extends React.Component { this.props.id ? '' : ' panel user-select-none panel-without-heading panel--draggable pb-0' - }`} + }${isHighlighted ? ' border-2 border-primary' : ''}`} >
@@ -349,6 +353,7 @@ const SegmentOverrideListInner = ({ disabled, environmentId, hideViewSegment, + highlightSegmentId, id, items, multivariateOptions, @@ -374,6 +379,7 @@ const SegmentOverrideListInner = ({ name={name} segment={value.segment} hideViewSegment={hideViewSegment} + highlightSegmentId={highlightSegmentId} onSortEnd={onSortEnd} disabled={disabled} showEditSegment={showEditSegment} @@ -433,6 +439,38 @@ class TheComponent extends Component { totalSegmentOverrides: res.data.total_segment_overrides, }) }) + this.checkPreselectedSegment() + } + + componentDidUpdate(prevProps) { + if ( + this.props.highlightSegmentId && + this.props.highlightSegmentId !== prevProps.highlightSegmentId + ) { + this.checkPreselectedSegment() + } + } + + checkPreselectedSegment = () => { + const { highlightSegmentId, projectId, value } = this.props + if (!highlightSegmentId) return + + const existingOverride = value?.find((v) => v.segment === highlightSegmentId) + if (existingOverride) { + return + } + + getSegment(getStore(), { + id: highlightSegmentId, + projectId: projectId, + }).then((res) => { + this.setState({ + selectedSegment: { + label: res.data.name, + value: res.data.id, + }, + }) + }) } addItem = () => { @@ -573,19 +611,37 @@ class TheComponent extends Component { !this.props.disableCreate && !this.props.showCreateSegment && !this.props.readOnly && ( - - - this.setState({ selectedSegment }, this.addItem) - } - /> - + +
+ { + if (this.props.highlightSegmentId) { + this.setState({ selectedSegment }) + } else { + this.setState({ selectedSegment }, this.addItem) + } + }} + /> +
+ {this.props.highlightSegmentId && + this.state.selectedSegment && ( + + )} +
)} {this.props.showCreateSegment && !this.state.segmentEditId && (
@@ -710,6 +766,7 @@ class TheComponent extends Component { onSortEnd={this.onSortEnd} projectFlag={this.props.projectFlag} hideViewSegment={this.props.hideViewSegment} + highlightSegmentId={this.props.highlightSegmentId} />
Segment[] } const SegmentSelect: FC = ({ + className, filter, projectId, ...rest @@ -41,12 +43,13 @@ const SegmentSelect: FC = ({