From 2ab780b533d6e6489353f7fd0f642c2103f8b1b7 Mon Sep 17 00:00:00 2001 From: Tomas Engebretsen Date: Mon, 9 Dec 2024 23:37:14 +0100 Subject: [PATCH] feat: Interface for feature flags (#14231) --- .../SettingsModal/SettingsModal.tsx | 8 +-- .../types/HeaderMenu/HeaderMenuItem.ts | 4 +- .../utils/headerMenu/headerMenuUtils.test.ts | 6 +- frontend/language/src/nb.json | 1 + .../src/contexts/BpmnContext.tsx | 7 ++- .../src/utils/featureToggleUtils.test.ts | 31 +++++----- .../shared/src/utils/featureToggleUtils.ts | 43 +++++++------- .../src/components/TextResource.test.tsx | 4 +- .../src/components/TextResource.tsx | 4 +- .../config/EditFormComponent.test.tsx | 7 ++- .../components/config/EditFormComponent.tsx | 11 ++-- .../Elements/LayoutSetsContainer.test.tsx | 9 +-- .../Elements/LayoutSetsContainer.tsx | 6 +- .../EditBinding/EditBinding.tsx | 6 +- .../EditBinding/SelectDataModelBinding.tsx | 4 +- .../OptionTabs/OptionTabs.test.tsx | 6 +- .../EditOptions/OptionTabs/OptionTabs.tsx | 4 +- .../OptionTabs/SelectTab/SelectTab.tsx | 2 +- .../AddItem/ToggleAddComponentPoc.tsx | 11 ++-- .../src/containers/DesignView/FormLayout.tsx | 4 +- .../DesignView/FormTree/FormItem/FormItem.tsx | 4 +- .../ux-editor/src/containers/FormDesigner.tsx | 10 ++-- .../ux-editor/src/data/formItemConfig.ts | 8 +-- .../pages/ResourcePage/ResourcePage.test.tsx | 4 +- .../pages/ResourcePage/ResourcePage.tsx | 4 +- frontend/studio-root/app/App.tsx | 3 +- .../pages/FlagsPage/FlagsPage.module.css | 6 ++ .../pages/FlagsPage/FlagsPage.test.tsx | 57 +++++++++++++++++++ .../studio-root/pages/FlagsPage/FlagsPage.tsx | 51 +++++++++++++++++ frontend/studio-root/pages/FlagsPage/index.ts | 1 + .../setFeatureFlagInLocalStorage.test.ts | 20 +++++++ .../FlagsPage/setFeatureFlagInLocalStorage.ts | 14 +++++ 32 files changed, 259 insertions(+), 101 deletions(-) create mode 100644 frontend/studio-root/pages/FlagsPage/FlagsPage.module.css create mode 100644 frontend/studio-root/pages/FlagsPage/FlagsPage.test.tsx create mode 100644 frontend/studio-root/pages/FlagsPage/FlagsPage.tsx create mode 100644 frontend/studio-root/pages/FlagsPage/index.ts create mode 100644 frontend/studio-root/pages/FlagsPage/setFeatureFlagInLocalStorage.test.ts create mode 100644 frontend/studio-root/pages/FlagsPage/setFeatureFlagInLocalStorage.ts diff --git a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx index c5dad2b42fe..6df151a2914 100644 --- a/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx +++ b/frontend/app-development/layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/SettingsModal.tsx @@ -3,9 +3,9 @@ import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } import classes from './SettingsModal.module.css'; import { CogIcon } from '@studio/icons'; import { - StudioModal, StudioContentMenu, type StudioContentMenuButtonTabProps, + StudioModal, } from '@studio/components'; import type { SettingsModalTabId } from '../../../../../types/SettingsModalTabId'; import { useTranslation } from 'react-i18next'; @@ -16,7 +16,7 @@ import { SetupTab } from './components/Tabs/SetupTab'; import { type SettingsModalHandle } from '../../../../../types/SettingsModalHandle'; import { useSettingsModalMenuTabConfigs } from './hooks/useSettingsModalMenuTabConfigs'; import { Maskinporten } from './components/Tabs/Maskinporten'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; export const SettingsModal = forwardRef(({}, ref): ReactElement => { const { t } = useTranslation(); @@ -54,7 +54,7 @@ export const SettingsModal = forwardRef(({}, ref): Reac return ; } case 'maskinporten': { - return shouldDisplayFeature('maskinporten') ? : null; + return shouldDisplayFeature(FeatureFlag.Maskinporten) ? : null; } } }; @@ -94,7 +94,7 @@ SettingsModal.displayName = 'SettingsModal'; function filterFeatureFlag( menuTabConfigs: Array>, ) { - return shouldDisplayFeature('maskinporten') + return shouldDisplayFeature(FeatureFlag.Maskinporten) ? menuTabConfigs : menuTabConfigs.filter((tab) => tab.tabId !== 'maskinporten'); } diff --git a/frontend/app-development/types/HeaderMenu/HeaderMenuItem.ts b/frontend/app-development/types/HeaderMenu/HeaderMenuItem.ts index f0cb676fda3..562b13cd428 100644 --- a/frontend/app-development/types/HeaderMenu/HeaderMenuItem.ts +++ b/frontend/app-development/types/HeaderMenu/HeaderMenuItem.ts @@ -1,14 +1,14 @@ import { type HeaderMenuGroupKey } from 'app-development/enums/HeaderMenuGroupKey'; import { type HeaderMenuItemKey } from 'app-development/enums/HeaderMenuItemKey'; import { type RepositoryType } from 'app-shared/types/global'; -import { type SupportedFeatureFlags } from 'app-shared/utils/featureToggleUtils'; +import { type FeatureFlag } from 'app-shared/utils/featureToggleUtils'; export interface HeaderMenuItem { key: HeaderMenuItemKey; link: string; icon?: React.FC>; repositoryTypes: RepositoryType[]; - featureFlagName?: SupportedFeatureFlags; + featureFlagName?: FeatureFlag; isBeta?: boolean; group: HeaderMenuGroupKey; } diff --git a/frontend/app-development/utils/headerMenu/headerMenuUtils.test.ts b/frontend/app-development/utils/headerMenu/headerMenuUtils.test.ts index 29a4b8f3d16..14d2bfe5f05 100644 --- a/frontend/app-development/utils/headerMenu/headerMenuUtils.test.ts +++ b/frontend/app-development/utils/headerMenu/headerMenuUtils.test.ts @@ -15,7 +15,7 @@ import { RoutePaths } from 'app-development/enums/RoutePaths'; import { DatabaseIcon } from '@studio/icons'; import { HeaderMenuGroupKey } from 'app-development/enums/HeaderMenuGroupKey'; import { typedLocalStorage } from '@studio/pure-functions'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; jest.mock('app-shared/utils/featureToggleUtils'); @@ -137,7 +137,7 @@ describe('headerMenuUtils', () => { icon: DatabaseIcon, repositoryTypes: [RepositoryType.App, RepositoryType.DataModels], group: HeaderMenuGroupKey.Tools, - featureFlagName: 'shouldOverrideAppLibCheck', + featureFlagName: FeatureFlag.ShouldOverrideAppLibCheck, }; expect(filterRoutesByFeatureFlag(menuItem)).toBe(true); @@ -152,7 +152,7 @@ describe('headerMenuUtils', () => { icon: DatabaseIcon, repositoryTypes: [RepositoryType.App, RepositoryType.DataModels], group: HeaderMenuGroupKey.Tools, - featureFlagName: 'shouldOverrideAppLibCheck', + featureFlagName: FeatureFlag.ShouldOverrideAppLibCheck, }; expect(filterRoutesByFeatureFlag(menuItem)).toBe(false); diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 9c19cc6a6fa..36829085b11 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -281,6 +281,7 @@ "expression.valueType.null": "Ikke satt", "expression.valueType.number": "Tall", "expression.valueType.string": "Tekst", + "feature_flags.heading": "Funksjonsflagg", "form_filler.file_uploader_validation_error_upload": "Noe gikk galt da filen skulle lastes opp, prøv igjen senere.", "general.action": "Handling", "general.actions": "Handlinger", diff --git a/frontend/packages/process-editor/src/contexts/BpmnContext.tsx b/frontend/packages/process-editor/src/contexts/BpmnContext.tsx index 7f858681841..b07090475eb 100644 --- a/frontend/packages/process-editor/src/contexts/BpmnContext.tsx +++ b/frontend/packages/process-editor/src/contexts/BpmnContext.tsx @@ -1,6 +1,6 @@ -import React, { createContext, useContext, useRef, useState, type MutableRefObject } from 'react'; +import React, { createContext, type MutableRefObject, useContext, useRef, useState } from 'react'; import { supportsProcessEditor } from '../utils/processEditorUtils'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; import type Modeler from 'bpmn-js/lib/Modeler'; import type { BpmnDetails } from '../types/BpmnDetails'; @@ -29,7 +29,8 @@ export const BpmnContextProvider = ({ const [bpmnDetails, setBpmnDetails] = useState(null); const isEditAllowed = - supportsProcessEditor(appLibVersion) || shouldDisplayFeature('shouldOverrideAppLibCheck'); + supportsProcessEditor(appLibVersion) || + shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck); const modelerRef = useRef(null); diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.test.ts b/frontend/packages/shared/src/utils/featureToggleUtils.test.ts index 82ad734b839..592f3299e99 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.test.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.test.ts @@ -3,6 +3,7 @@ import { addFeatureFlagToLocalStorage, removeFeatureFlagFromLocalStorage, shouldDisplayFeature, + FeatureFlag, } from './featureToggleUtils'; describe('featureToggle localStorage', () => { @@ -10,21 +11,21 @@ describe('featureToggle localStorage', () => { it('should return true if feature is enabled in the localStorage', () => { typedLocalStorage.setItem('featureFlags', ['shouldOverrideAppLibCheck']); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeTruthy(); }); it('should return true if featureFlag includes in feature params', () => { typedLocalStorage.setItem('featureFlags', ['demo', 'shouldOverrideAppLibCheck']); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeTruthy(); }); it('should return false if feature is not enabled in the localStorage', () => { typedLocalStorage.setItem('featureFlags', ['demo']); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeFalsy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeFalsy(); }); it('should return false if feature is not enabled in the localStorage', () => { - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeFalsy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeFalsy(); }); }); @@ -35,22 +36,22 @@ describe('featureToggle url', () => { }); it('should return true if feature is enabled in the url', () => { window.history.pushState({}, 'PageUrl', '/?featureFlags=shouldOverrideAppLibCheck'); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeTruthy(); }); it('should return true if featureFlag includes in feature params', () => { window.history.pushState({}, 'PageUrl', '/?featureFlags=demo,shouldOverrideAppLibCheck'); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeTruthy(); }); it('should return false if feature is not included in the url', () => { window.history.pushState({}, 'PageUrl', '/?featureFlags=demo'); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeFalsy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeFalsy(); }); it('should return false if feature is not included in the url', () => { window.history.pushState({}, 'PageUrl', '/'); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeFalsy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeFalsy(); }); it('should persist features in sessionStorage when persistFeatureFlag is set in url', () => { @@ -59,9 +60,9 @@ describe('featureToggle url', () => { 'PageUrl', '/?featureFlags=resourceMigration,shouldOverrideAppLibCheck&persistFeatureFlag=true', ); - expect(shouldDisplayFeature('componentConfigBeta')).toBeFalsy(); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); - expect(shouldDisplayFeature('resourceMigration')).toBeTruthy(); + expect(shouldDisplayFeature(FeatureFlag.ComponentConfigBeta)).toBeFalsy(); + expect(shouldDisplayFeature(FeatureFlag.ShouldOverrideAppLibCheck)).toBeTruthy(); + expect(shouldDisplayFeature(FeatureFlag.ResourceMigration)).toBeTruthy(); expect(typedSessionStorage.getItem('featureFlags')).toEqual([ 'shouldOverrideAppLibCheck', 'resourceMigration', @@ -75,14 +76,14 @@ describe('addFeatureToLocalStorage', () => { typedLocalStorage.removeItem('featureFlags'); }); it('should add feature to local storage', () => { - addFeatureFlagToLocalStorage('shouldOverrideAppLibCheck'); + addFeatureFlagToLocalStorage(FeatureFlag.ShouldOverrideAppLibCheck); expect(typedLocalStorage.getItem('featureFlags')).toEqual([ 'shouldOverrideAppLibCheck', ]); }); it('should append provided feature to existing features in local storage', () => { typedLocalStorage.setItem('featureFlags', ['demo']); - addFeatureFlagToLocalStorage('shouldOverrideAppLibCheck'); + addFeatureFlagToLocalStorage(FeatureFlag.ShouldOverrideAppLibCheck); expect(typedLocalStorage.getItem('featureFlags')).toEqual([ 'demo', 'shouldOverrideAppLibCheck', @@ -96,12 +97,12 @@ describe('removeFeatureFromLocalStorage', () => { }); it('should remove feature from local storage', () => { typedLocalStorage.setItem('featureFlags', ['shouldOverrideAppLibCheck']); - removeFeatureFlagFromLocalStorage('shouldOverrideAppLibCheck'); + removeFeatureFlagFromLocalStorage(FeatureFlag.ShouldOverrideAppLibCheck); expect(typedLocalStorage.getItem('featureFlags')).toEqual([]); }); it('should only remove specified feature from local storage', () => { typedLocalStorage.setItem('featureFlags', ['shouldOverrideAppLibCheck', 'demo']); - removeFeatureFlagFromLocalStorage('shouldOverrideAppLibCheck'); + removeFeatureFlagFromLocalStorage(FeatureFlag.ShouldOverrideAppLibCheck); expect(typedLocalStorage.getItem('featureFlags')).toEqual(['demo']); }); }); diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 6314d0254d2..242a026db79 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -3,26 +3,25 @@ import { typedLocalStorage, typedSessionStorage } from '@studio/pure-functions'; const featureFlagKey = 'featureFlags'; const persistFeatureKey = 'persistFeatureFlag'; -// All the features that you want to be toggle on/off should be added here. To ensure that we type check the feature name. -export type SupportedFeatureFlags = - | 'componentConfigBeta' - | 'shouldOverrideAppLibCheck' - | 'resourceMigration' - | 'multipleDataModelsPerTask' - | 'exportForm' - | 'addComponentModal' - | 'subform' - | 'summary2' - | 'codeListEditor' - | 'optionListEditor' - | 'maskinporten'; +export enum FeatureFlag { + AddComponentModal = 'addComponentModal', + ComponentConfigBeta = 'componentConfigBeta', + ExportForm = 'exportForm', + Maskinporten = 'maskinporten', + MultipleDataModelsPerTask = 'multipleDataModelsPerTask', + OptionListEditor = 'optionListEditor', + ResourceMigration = 'resourceMigration', + ShouldOverrideAppLibCheck = 'shouldOverrideAppLibCheck', + Subform = 'subform', + Summary2 = 'summary2', +} /* * Please add all the features that you want to be toggle on by default here. * Remember that all the features that are listed here will be available to the users in production, * since this is the default active features. */ -const defaultActiveFeatures: SupportedFeatureFlags[] = []; +const defaultActiveFeatures: FeatureFlag[] = []; /** * @param featureFlag @@ -31,7 +30,7 @@ const defaultActiveFeatures: SupportedFeatureFlags[] = []; * @example shouldDisplayFeature('myFeatureName') && * @example The feature can be toggled and persisted by the url query, (url)?featureFlags=[featureName]&persistFeatureFlag=true */ -export const shouldDisplayFeature = (featureFlag: SupportedFeatureFlags): boolean => { +export const shouldDisplayFeature = (featureFlag: FeatureFlag): boolean => { // Check if feature should be persisted in session storage, (url)?persistFeatureFlag=true if (shouldPersistInSession() && isFeatureActivatedByUrl(featureFlag)) { addFeatureFlagToSessionStorage(featureFlag); @@ -46,12 +45,12 @@ export const shouldDisplayFeature = (featureFlag: SupportedFeatureFlags): boolea }; // Check if the feature is one of the default active features -const isDefaultActivatedFeature = (featureFlag: SupportedFeatureFlags): boolean => { +const isDefaultActivatedFeature = (featureFlag: FeatureFlag): boolean => { return defaultActiveFeatures.includes(featureFlag); }; // Check if feature includes in the url query, (url)?featureFlags=[featureName] -const isFeatureActivatedByUrl = (featureFlag: SupportedFeatureFlags): boolean => { +const isFeatureActivatedByUrl = (featureFlag: FeatureFlag): boolean => { const urlParams = new URLSearchParams(window.location.search); const featureParam = urlParams.get(featureFlagKey); @@ -64,7 +63,7 @@ const isFeatureActivatedByUrl = (featureFlag: SupportedFeatureFlags): boolean => }; // Check if feature includes in local storage, featureFlags: ["featureName"] -const isFeatureActivatedByLocalStorage = (featureFlag: SupportedFeatureFlags): boolean => { +export const isFeatureActivatedByLocalStorage = (featureFlag: FeatureFlag): boolean => { const featureFlagsFromStorage = typedLocalStorage.getItem(featureFlagKey) || []; return featureFlagsFromStorage.includes(featureFlag); }; @@ -74,7 +73,7 @@ const isFeatureActivatedByLocalStorage = (featureFlag: SupportedFeatureFlags): b * @description This function will add the feature flag to local storage * @example addFeatureToLocalStorage('myFeatureName') */ -export const addFeatureFlagToLocalStorage = (featureFlag: SupportedFeatureFlags): void => { +export const addFeatureFlagToLocalStorage = (featureFlag: FeatureFlag): void => { const featureFlagsFromStorage = typedLocalStorage.getItem(featureFlagKey) || []; featureFlagsFromStorage.push(featureFlag); typedLocalStorage.setItem(featureFlagKey, featureFlagsFromStorage); @@ -85,14 +84,14 @@ export const addFeatureFlagToLocalStorage = (featureFlag: SupportedFeatureFlags) * @description This function will remove the feature flag from local storage * @example removeFeatureFromLocalStorage('myFeatureName') */ -export const removeFeatureFlagFromLocalStorage = (featureFlag: SupportedFeatureFlags): void => { +export const removeFeatureFlagFromLocalStorage = (featureFlag: FeatureFlag): void => { const featureFlagsFromStorage = typedLocalStorage.getItem(featureFlagKey) || []; const filteredFeatureFlags = featureFlagsFromStorage.filter((feature) => feature !== featureFlag); typedLocalStorage.setItem(featureFlagKey, filteredFeatureFlags); }; // Check if feature includes in session storage, featureFlags: ["featureName"] -const isFeatureActivatedBySessionStorage = (featureFlag: SupportedFeatureFlags): boolean => { +const isFeatureActivatedBySessionStorage = (featureFlag: FeatureFlag): boolean => { const featureFlagsFromStorage = typedSessionStorage.getItem(featureFlagKey) || []; return featureFlagsFromStorage.includes(featureFlag); }; @@ -105,7 +104,7 @@ const shouldPersistInSession = (): boolean => { }; // Add feature to session storage to persist the feature in the current session -const addFeatureFlagToSessionStorage = (featureFlag: SupportedFeatureFlags): void => { +const addFeatureFlagToSessionStorage = (featureFlag: FeatureFlag): void => { const featureFlagsFromStorage = typedSessionStorage.getItem(featureFlagKey) || []; const featureFlagAlreadyExist = featureFlagsFromStorage.includes(featureFlag); diff --git a/frontend/packages/ux-editor-v3/src/components/TextResource.test.tsx b/frontend/packages/ux-editor-v3/src/components/TextResource.test.tsx index c36a3c55dce..85d94bd88f7 100644 --- a/frontend/packages/ux-editor-v3/src/components/TextResource.test.tsx +++ b/frontend/packages/ux-editor-v3/src/components/TextResource.test.tsx @@ -11,7 +11,7 @@ import { textMock } from '@studio/testing/mocks/i18nMock'; import { useTextResourcesQuery } from 'app-shared/hooks/queries/useTextResourcesQuery'; import { DEFAULT_LANGUAGE } from 'app-shared/constants'; import { typedLocalStorage } from '@studio/pure-functions'; -import { addFeatureFlagToLocalStorage } from 'app-shared/utils/featureToggleUtils'; +import { addFeatureFlagToLocalStorage, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; import { app, org } from '@studio/testing/testids'; const user = userEvent.setup(); @@ -226,7 +226,7 @@ describe('TextResource', () => { }); it('Renders delete button as enabled when handleRemoveTextResource is given and componentConfigBeta feature flag is enabled', async () => { - addFeatureFlagToLocalStorage('componentConfigBeta'); + addFeatureFlagToLocalStorage(FeatureFlag.ComponentConfigBeta); await render({ textResourceId: 'test', handleRemoveTextResource: jest.fn() }); expect(screen.getByRole('button', { name: textMock('general.delete') })).toBeEnabled(); }); diff --git a/frontend/packages/ux-editor-v3/src/components/TextResource.tsx b/frontend/packages/ux-editor-v3/src/components/TextResource.tsx index 333dd7a169e..05bb3ca70d8 100644 --- a/frontend/packages/ux-editor-v3/src/components/TextResource.tsx +++ b/frontend/packages/ux-editor-v3/src/components/TextResource.tsx @@ -19,7 +19,7 @@ import type { ITextResource } from 'app-shared/types/global'; import { FormField } from './FormField'; import { AltinnConfirmDialog } from 'app-shared/components/AltinnConfirmDialog'; import { useTranslation } from 'react-i18next'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; import { StudioButton, StudioNativeSelect } from '@studio/components'; export interface TextResourceProps { @@ -181,7 +181,7 @@ export const TextResource = ({ color='second' disabled={ !handleRemoveTextResource || - !(!!textResourceId || shouldDisplayFeature('componentConfigBeta')) + !(!!textResourceId || shouldDisplayFeature(FeatureFlag.ComponentConfigBeta)) } icon={} onClick={() => setIsConfirmDeleteDialogOpen(true)} diff --git a/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx b/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx index 6ddf4fcd5c1..4157a123ddb 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx +++ b/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx @@ -11,7 +11,10 @@ import type { DataModelMetadataResponse } from 'app-shared/types/api'; import { dataModelNameMock, layoutSet1NameMock } from '@altinn/ux-editor-v3/testing/layoutSetsMock'; import { app, org } from '@studio/testing/testids'; import { textMock } from '@studio/testing/mocks/i18nMock'; -import { removeFeatureFlagFromLocalStorage } from 'app-shared/utils/featureToggleUtils'; +import { + removeFeatureFlagFromLocalStorage, + FeatureFlag, +} from 'app-shared/utils/featureToggleUtils'; // Test data: const srcValueLabel = 'Source'; @@ -68,7 +71,7 @@ const getDataModelMetadata = () => describe('EditFormComponent', () => { beforeEach(() => { - removeFeatureFlagFromLocalStorage('componentConfigBeta'); + removeFeatureFlagFromLocalStorage(FeatureFlag.ComponentConfigBeta); jest.clearAllMocks(); }); diff --git a/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.tsx b/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.tsx index d1238471a18..1608ead0f91 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.tsx +++ b/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.tsx @@ -1,9 +1,9 @@ import React from 'react'; import type { EditSettings, IGenericEditComponent } from './componentConfig'; -import { configComponents, componentSpecificEditConfig } from './componentConfig'; +import { componentSpecificEditConfig, configComponents } from './componentConfig'; import { ComponentSpecificContent } from './componentSpecificContent'; -import { Switch, Fieldset, Heading } from '@digdir/designsystemet-react'; +import { Fieldset, Heading, Switch } from '@digdir/designsystemet-react'; import classes from './EditFormComponent.module.css'; import type { FormComponent } from '../../types/FormComponent'; import { selectedLayoutNameSelector } from '../../selectors/formLayoutSelectors'; @@ -19,6 +19,7 @@ import { addFeatureFlagToLocalStorage, removeFeatureFlagFromLocalStorage, shouldDisplayFeature, + FeatureFlag, } from 'app-shared/utils/featureToggleUtils'; import { formItemConfigs } from '../../data/formItemConfig'; import { UnknownComponentAlert } from '../UnknownComponentAlert'; @@ -37,7 +38,7 @@ export const EditFormComponent = ({ const selectedLayout = useSelector(selectedLayoutNameSelector); const { t } = useTranslation(); const [showComponentConfigBeta, setShowComponentConfigBeta] = React.useState( - shouldDisplayFeature('componentConfigBeta'), + shouldDisplayFeature(FeatureFlag.ComponentConfigBeta), ); useLayoutSchemaQuery(); // Ensure we load the layout schemas so that component schemas can be loaded @@ -65,9 +66,9 @@ export const EditFormComponent = ({ setShowComponentConfigBeta(event.target.checked); // Ensure choice of feature toggling is persisted in local storage if (event.target.checked) { - addFeatureFlagToLocalStorage('componentConfigBeta'); + addFeatureFlagToLocalStorage(FeatureFlag.ComponentConfigBeta); } else { - removeFeatureFlagFromLocalStorage('componentConfigBeta'); + removeFeatureFlagFromLocalStorage(FeatureFlag.ComponentConfigBeta); } }; diff --git a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.test.tsx b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.test.tsx index 258cb6bd773..bc76a9faeb4 100644 --- a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.test.tsx +++ b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.test.tsx @@ -17,6 +17,7 @@ import { app, org } from '@studio/testing/testids'; import { addFeatureFlagToLocalStorage, removeFeatureFlagFromLocalStorage, + FeatureFlag, } from 'app-shared/utils/featureToggleUtils'; import { textMock } from '@studio/testing/mocks/i18nMock'; import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse'; @@ -65,7 +66,7 @@ describe('LayoutSetsContainer', () => { }); it('should render the delete subform button when feature is enabled and selected layoutset is a subform', () => { - addFeatureFlagToLocalStorage('subform'); + addFeatureFlagToLocalStorage(FeatureFlag.Subform); render({ layoutSets: { sets: [{ id: layoutSet3SubformNameMock, type: 'subform' }] }, selectedLayoutSet: layoutSet3SubformNameMock, @@ -74,11 +75,11 @@ describe('LayoutSetsContainer', () => { name: textMock('ux_editor.delete.subform'), }); expect(deleteSubformButton).toBeInTheDocument(); - removeFeatureFlagFromLocalStorage('subform'); + removeFeatureFlagFromLocalStorage(FeatureFlag.Subform); }); it('should not render the delete subform button when feature is enabled and selected layoutset is not a subform', () => { - addFeatureFlagToLocalStorage('subform'); + addFeatureFlagToLocalStorage(FeatureFlag.Subform); render({ layoutSets: { sets: [{ id: layoutSet1NameMock, dataType: 'data-model' }] }, selectedLayoutSet: layoutSet1NameMock, @@ -87,7 +88,7 @@ describe('LayoutSetsContainer', () => { name: textMock('ux_editor.delete.subform'), }); expect(deleteSubformButton).not.toBeInTheDocument(); - removeFeatureFlagFromLocalStorage('subform'); + removeFeatureFlagFromLocalStorage(FeatureFlag.Subform); }); it('should not render the delete subform button when feature is disabled', () => { diff --git a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx index 5604810d357..e2c9d1a17a2 100644 --- a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx +++ b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx @@ -3,7 +3,7 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen import { useAppContext } from '../../hooks'; import classes from './LayoutSetsContainer.module.css'; import { ExportForm } from './ExportForm'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; import { StudioCombobox } from '@studio/components'; import { DeleteSubformWrapper } from './Subform/DeleteSubformWrapper'; import { useLayoutSetsExtendedQuery } from 'app-shared/hooks/queries/useLayoutSetsExtendedQuery'; @@ -60,8 +60,8 @@ export function LayoutSetsContainer() { ))} - {shouldDisplayFeature('exportForm') && } - {shouldDisplayFeature('subform') && ( + {shouldDisplayFeature(FeatureFlag.ExportForm) && } + {shouldDisplayFeature(FeatureFlag.Subform) && ( { }); it('should render EditOptionChoice when featureFlag is enabled', async () => { - addFeatureFlagToLocalStorage('optionListEditor'); + addFeatureFlagToLocalStorage(FeatureFlag.OptionListEditor); const optionsId = 'optionsId'; renderEditOptions({ componentProps: { @@ -131,7 +131,7 @@ describe('EditOptions', () => { }); it('should switch to referenceId input clicking referenceId tab', async () => { - addFeatureFlagToLocalStorage('optionListEditor'); + addFeatureFlagToLocalStorage(FeatureFlag.OptionListEditor); const user = userEvent.setup(); renderEditOptions({ componentProps: { options: [] }, diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/OptionTabs.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/OptionTabs.tsx index 3c1dafe76c8..062d7e595d9 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/OptionTabs.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/OptionTabs.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { StudioTabs } from '@studio/components'; import { ReferenceTab } from './ReferenceTab/ReferenceTab'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; import { ManualTab } from './ManualTab'; import { EditTab } from './EditTab'; import { SelectedOptionsType } from '../EditOptions'; @@ -22,7 +22,7 @@ type OptionTabsProps = { export function OptionTabs({ component, handleComponentChange, optionListIds }: OptionTabsProps) { return ( <> - {shouldDisplayFeature('optionListEditor') ? ( + {shouldDisplayFeature(FeatureFlag.OptionListEditor) ? ( { - if (shouldDisplayFeature('addComponentModal')) { - removeFeatureFlagFromLocalStorage('addComponentModal'); + if (shouldDisplayFeature(FeatureFlag.AddComponentModal)) { + removeFeatureFlagFromLocalStorage(FeatureFlag.AddComponentModal); } else { - addFeatureFlagToLocalStorage('addComponentModal'); + addFeatureFlagToLocalStorage(FeatureFlag.AddComponentModal); } window.location.reload(); }; @@ -30,7 +31,7 @@ export function ToggleAddComponentPoc(): React.ReactElement { <>
@@ -51,7 +52,7 @@ export function ToggleAddComponentPoc(): React.ReactElement {
- {shouldDisplayFeature('addComponentModal') && } + {shouldDisplayFeature(FeatureFlag.AddComponentModal) && } ); } diff --git a/frontend/packages/ux-editor/src/containers/DesignView/FormLayout.tsx b/frontend/packages/ux-editor/src/containers/DesignView/FormLayout.tsx index 36ec06ee09c..65e02b368b6 100644 --- a/frontend/packages/ux-editor/src/containers/DesignView/FormLayout.tsx +++ b/frontend/packages/ux-editor/src/containers/DesignView/FormLayout.tsx @@ -7,7 +7,7 @@ import { Alert, Paragraph } from '@digdir/designsystemet-react'; import { FormLayoutWarning } from './FormLayoutWarning'; import { BASE_CONTAINER_ID } from 'app-shared/constants'; import { AddItem } from './AddItem'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; export interface FormLayoutProps { layout: IInternalLayout; @@ -24,7 +24,7 @@ export const FormLayout = ({ layout, isInvalid, duplicateComponents }: FormLayou {hasMultiPageGroup(layout) && } {/** The following check and component are added as part of a live user test behind a feature flag. Can be removed if we decide not to use after user test. */} - {shouldDisplayFeature('addComponentModal') && ( + {shouldDisplayFeature(FeatureFlag.AddComponentModal) && ( )} diff --git a/frontend/packages/ux-editor/src/containers/DesignView/FormTree/FormItem/FormItem.tsx b/frontend/packages/ux-editor/src/containers/DesignView/FormTree/FormItem/FormItem.tsx index 7fcfe5f7496..3a3c5299c92 100644 --- a/frontend/packages/ux-editor/src/containers/DesignView/FormTree/FormItem/FormItem.tsx +++ b/frontend/packages/ux-editor/src/containers/DesignView/FormTree/FormItem/FormItem.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; import { UnknownReferencedItem } from '../UnknownReferencedItem'; import { QuestionmarkDiamondIcon } from '@studio/icons'; import { useComponentTitle } from '@altinn/ux-editor/hooks'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; export type FormItemProps = { layout: IInternalLayout; @@ -39,7 +39,7 @@ export const FormItem = ({ layout, id, duplicateComponents }: FormItemProps) => ); const shouldDisplayAddButton = - isContainer(layout, id) && shouldDisplayFeature('addComponentModal'); + isContainer(layout, id) && shouldDisplayFeature(FeatureFlag.AddComponentModal); return ( { const { org, app } = useStudioEnvironmentParams(); @@ -164,7 +164,7 @@ export const FormDesigner = (): JSX.Element => { * The following check is done for a live user test behind feature flag. It can be removed if this is not something * that is going to be used in the future. */} - {!shouldDisplayFeature('addComponentModal') && ( + {!shouldDisplayFeature(FeatureFlag.AddComponentModal) && ( { )} diff --git a/frontend/packages/ux-editor/src/data/formItemConfig.ts b/frontend/packages/ux-editor/src/data/formItemConfig.ts index a8e7c62e615..c22711e41e1 100644 --- a/frontend/packages/ux-editor/src/data/formItemConfig.ts +++ b/frontend/packages/ux-editor/src/data/formItemConfig.ts @@ -21,7 +21,6 @@ import { LongTextIcon, NavBarIcon, PaperclipIcon, - TextIcon, PaymentDetailsIcon, PinIcon, PresentationIcon, @@ -31,6 +30,7 @@ import { ShortTextIcon, TableIcon, TasklistIcon, + TextIcon, TitleIcon, WalletIcon, } from '@studio/icons'; @@ -38,7 +38,7 @@ import type { ContainerComponentType } from '../types/ContainerComponent'; import { LayoutItemType } from '../types/global'; import type { ComponentSpecificConfig } from 'app-shared/types/ComponentSpecificConfig'; import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; import { FilterUtils } from './FilterUtils'; export type FormItemConfig = { @@ -507,7 +507,7 @@ export const advancedItems: FormItemConfigs[ComponentType][] = [ formItemConfigs[ComponentType.Custom], formItemConfigs[ComponentType.RepeatingGroup], formItemConfigs[ComponentType.PaymentDetails], - shouldDisplayFeature('subform') && formItemConfigs[ComponentType.Subform], + shouldDisplayFeature(FeatureFlag.Subform) && formItemConfigs[ComponentType.Subform], ].filter(FilterUtils.filterOutDisabledFeatureItems); export const schemaComponents: FormItemConfigs[ComponentType][] = [ @@ -532,7 +532,7 @@ export const schemaComponents: FormItemConfigs[ComponentType][] = [ formItemConfigs[ComponentType.IFrame], formItemConfigs[ComponentType.InstanceInformation], formItemConfigs[ComponentType.Summary], - shouldDisplayFeature('summary2') && formItemConfigs[ComponentType.Summary2], + shouldDisplayFeature(FeatureFlag.Summary2) && formItemConfigs[ComponentType.Summary2], ].filter(FilterUtils.filterOutDisabledFeatureItems); export const textComponents: FormItemConfigs[ComponentType][] = [ diff --git a/frontend/resourceadm/pages/ResourcePage/ResourcePage.test.tsx b/frontend/resourceadm/pages/ResourcePage/ResourcePage.test.tsx index 306efcc6677..4fd93e753e5 100644 --- a/frontend/resourceadm/pages/ResourcePage/ResourcePage.test.tsx +++ b/frontend/resourceadm/pages/ResourcePage/ResourcePage.test.tsx @@ -10,7 +10,7 @@ import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext'; import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; import type { QueryClient } from '@tanstack/react-query'; import { queriesMock } from 'app-shared/mocks/queriesMock'; -import { addFeatureFlagToLocalStorage } from 'app-shared/utils/featureToggleUtils'; +import { addFeatureFlagToLocalStorage, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; const mockResource1: Resource = { identifier: 'r1', @@ -93,7 +93,7 @@ describe('ResourcePage', () => { }); it('displays migrate tab in left navigation bar when resource reference is present in resource', async () => { - addFeatureFlagToLocalStorage('resourceMigration'); + addFeatureFlagToLocalStorage(FeatureFlag.ResourceMigration); const getResource = jest .fn() diff --git a/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx b/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx index ec780461b0f..f22bb83b974 100644 --- a/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx +++ b/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx @@ -26,7 +26,7 @@ import { ResourceAccessLists } from '../../components/ResourceAccessLists'; import { AccessListDetail } from '../../components/AccessListDetails'; import { useGetAccessListQuery } from '../../hooks/queries/useGetAccessListQuery'; import { useUrlParams } from '../../hooks/useUrlParams'; -import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; +import { shouldDisplayFeature, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; import { StudioContentMenu } from '@studio/components'; import type { StudioContentMenuButtonTabProps } from '@studio/components'; @@ -162,7 +162,7 @@ export const ResourcePage = (): React.JSX.Element => { * Decide if the migration page should be accessible or not */ const isMigrateEnabled = (): boolean => { - return !!altinn2References && shouldDisplayFeature('resourceMigration'); + return !!altinn2References && shouldDisplayFeature(FeatureFlag.ResourceMigration); }; const aboutPageId = 'about'; diff --git a/frontend/studio-root/app/App.tsx b/frontend/studio-root/app/App.tsx index 6f86bf1ab7e..bb433c3e175 100644 --- a/frontend/studio-root/app/App.tsx +++ b/frontend/studio-root/app/App.tsx @@ -4,10 +4,10 @@ import { Route, Routes } from 'react-router-dom'; import { StudioNotFoundPage } from '@studio/components'; import { Paragraph, Link } from '@digdir/designsystemet-react'; import { useTranslation, Trans } from 'react-i18next'; - import './App.css'; import { PageLayout } from '../pages/PageLayout'; import { ContactPage } from '../pages/Contact/ContactPage'; +import { FlagsPage } from '../pages/FlagsPage'; export const App = (): JSX.Element => { return ( @@ -15,6 +15,7 @@ export const App = (): JSX.Element => { }> } /> + } /> } /> diff --git a/frontend/studio-root/pages/FlagsPage/FlagsPage.module.css b/frontend/studio-root/pages/FlagsPage/FlagsPage.module.css new file mode 100644 index 00000000000..05642c64ac6 --- /dev/null +++ b/frontend/studio-root/pages/FlagsPage/FlagsPage.module.css @@ -0,0 +1,6 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--fds-spacing-3); + padding: var(--fds-spacing-6); +} diff --git a/frontend/studio-root/pages/FlagsPage/FlagsPage.test.tsx b/frontend/studio-root/pages/FlagsPage/FlagsPage.test.tsx new file mode 100644 index 00000000000..29e2f87332f --- /dev/null +++ b/frontend/studio-root/pages/FlagsPage/FlagsPage.test.tsx @@ -0,0 +1,57 @@ +import { FeatureFlag } from 'app-shared/utils/featureToggleUtils'; +import { typedLocalStorage } from '@studio/pure-functions'; // Todo: Move this to a more suitable place: https://github.com/Altinn/altinn-studio/issues/14230 +import type { RenderResult } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import { FlagsPage } from './FlagsPage'; +import React from 'react'; +import { userEvent } from '@testing-library/user-event'; + +const flags: FeatureFlag[] = Object.values(FeatureFlag); + +describe('FlagsPage', () => { + beforeEach(() => typedLocalStorage.removeItem('featureFlags')); + + it('Renders a checkbox for each flag', () => { + renderFlagsPage(); + flags.forEach((flag) => { + expect(screen.getByRole('checkbox', { name: flag })).toBeInTheDocument(); + }); + }); + + it('Renders the chechkboxes as unchecked by default', () => { + renderFlagsPage(); + flags.forEach((flag) => { + expect(screen.getByRole('checkbox', { name: flag })).not.toBeChecked(); + }); + }); + + it('Renders the chechkbox as checked when the corresponding flag is enabled', () => { + const enabledFlag = flags[0]; + typedLocalStorage.setItem('featureFlags', [enabledFlag]); + renderFlagsPage(); + expect(screen.getByRole('checkbox', { name: enabledFlag })).toBeChecked(); + }); + + it('Adds the flag to the list of enabled flags when the user checks the checkbox', async () => { + const user = userEvent.setup(); + renderFlagsPage(); + const flagToEnable = flags[0]; + const checkbox = screen.getByRole('checkbox', { name: flagToEnable }); + await user.click(checkbox); + expect(typedLocalStorage.getItem('featureFlags')).toEqual([flagToEnable]); + }); + + it('Removes the flag from the list of enabled flags when the user unchecks the checkbox', async () => { + const user = userEvent.setup(); + const enabledFlag = flags[0]; + typedLocalStorage.setItem('featureFlags', [enabledFlag]); + renderFlagsPage(); + const checkbox = screen.getByRole('checkbox', { name: enabledFlag }); + await user.click(checkbox); + expect(typedLocalStorage.getItem('featureFlags')).toEqual([]); + }); +}); + +function renderFlagsPage(): RenderResult { + return render(); +} diff --git a/frontend/studio-root/pages/FlagsPage/FlagsPage.tsx b/frontend/studio-root/pages/FlagsPage/FlagsPage.tsx new file mode 100644 index 00000000000..b5c62f72689 --- /dev/null +++ b/frontend/studio-root/pages/FlagsPage/FlagsPage.tsx @@ -0,0 +1,51 @@ +import type { ChangeEvent, ReactElement } from 'react'; +import React, { useCallback, useState } from 'react'; +import { isFeatureActivatedByLocalStorage, FeatureFlag } from 'app-shared/utils/featureToggleUtils'; +import { StudioSwitch, StudioCodeFragment, StudioHeading } from '@studio/components'; +import { setFeatureFlagInLocalStorage } from './setFeatureFlagInLocalStorage'; +import classes from './FlagsPage.module.css'; +import { useTranslation } from 'react-i18next'; + +export function FlagsPage(): ReactElement { + const { t } = useTranslation(); + + return ( +
+ {t('feature_flags.heading')} + +
+ ); +} + +function FlagList(): ReactElement { + return ( + <> + {Object.values(FeatureFlag).map((flag) => { + return ; + })} + + ); +} + +type FeatureFlagProps = { + flagName: FeatureFlag; +}; + +function Flag({ flagName }: FeatureFlagProps): ReactElement { + const [enabled, setEnabled] = useState(isFeatureActivatedByLocalStorage(flagName)); + + const handleToggle = useCallback( + (e: ChangeEvent): void => { + const { checked } = e.target; + setFeatureFlagInLocalStorage(flagName, checked); + setEnabled(checked); + }, + [flagName, setEnabled], + ); + + return ( + + {flagName} + + ); +} diff --git a/frontend/studio-root/pages/FlagsPage/index.ts b/frontend/studio-root/pages/FlagsPage/index.ts new file mode 100644 index 00000000000..dd833255979 --- /dev/null +++ b/frontend/studio-root/pages/FlagsPage/index.ts @@ -0,0 +1 @@ +export * from './FlagsPage'; diff --git a/frontend/studio-root/pages/FlagsPage/setFeatureFlagInLocalStorage.test.ts b/frontend/studio-root/pages/FlagsPage/setFeatureFlagInLocalStorage.test.ts new file mode 100644 index 00000000000..c58aacbe0cf --- /dev/null +++ b/frontend/studio-root/pages/FlagsPage/setFeatureFlagInLocalStorage.test.ts @@ -0,0 +1,20 @@ +import { typedLocalStorage } from '@studio/pure-functions'; // Todo: Move this to a more suitable place: https://github.com/Altinn/altinn-studio/issues/14230 +import { setFeatureFlagInLocalStorage } from './setFeatureFlagInLocalStorage'; +import type { FeatureFlag } from 'app-shared/utils/featureToggleUtils'; + +const testFlag = 'testFeature' as FeatureFlag; // Using casting here instead of a real flag because the list will change over time + +describe('setFeatureFlagInLocalStorage', () => { + beforeEach(() => typedLocalStorage.removeItem('featureFlags')); + + it('Adds the feature flag to the local storage when the state is true', () => { + setFeatureFlagInLocalStorage(testFlag, true); + expect(typedLocalStorage.getItem('featureFlags')).toEqual([testFlag]); + }); + + it('Removes the feature flag from the local storage when the state is false', () => { + typedLocalStorage.setItem('featureFlags', [testFlag]); + setFeatureFlagInLocalStorage(testFlag, false); + expect(typedLocalStorage.getItem('featureFlags')).toEqual([]); + }); +}); diff --git a/frontend/studio-root/pages/FlagsPage/setFeatureFlagInLocalStorage.ts b/frontend/studio-root/pages/FlagsPage/setFeatureFlagInLocalStorage.ts new file mode 100644 index 00000000000..6b0dad1e7f7 --- /dev/null +++ b/frontend/studio-root/pages/FlagsPage/setFeatureFlagInLocalStorage.ts @@ -0,0 +1,14 @@ +import type { FeatureFlag } from 'app-shared/utils/featureToggleUtils'; +import { + addFeatureFlagToLocalStorage, + removeFeatureFlagFromLocalStorage, +} from 'app-shared/utils/featureToggleUtils'; + +export function setFeatureFlagInLocalStorage(flag: FeatureFlag, state: boolean): void { + const changeInLocalStorage = retrieveChangeFunction(state); + return changeInLocalStorage(flag); +} + +function retrieveChangeFunction(state: boolean): (flag: FeatureFlag) => void { + return state ? addFeatureFlagToLocalStorage : removeFeatureFlagFromLocalStorage; +}