Skip to content

Commit ef84ba5

Browse files
persistent feature flags, language and plugin directory support (#3355)
* make feature flags persistent and add change callback add flag to pull language directory dynamically * add static default languages array from bundled directory version * fix tests * rename directory in preparation for adding plugin directory * remove old language directory attempt and clean up * add plugin directory support * add directory.plugin.url to public flags * switch evaluator url source based on if language directory is enabled * add missing plugin directory fields * fix weird build error * revert unintentional change to ChatBox.tsx * fix featureSelector bug unmodified flags will now give their default value when selected * fix formatting * fix conductor's eval take * remove fetchLanguages effect from ControlBarChapterSelect * move packages to sourceacademy namespace --------- Co-authored-by: Martin Henz <[email protected]>
1 parent 731a12c commit ef84ba5

36 files changed

+314
-179
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
"@reduxjs/toolkit": "^1.9.7",
4242
"@sentry/react": "^10.5.0",
4343
"@sourceacademy/c-slang": "^1.0.21",
44-
"@sourceacademy/language-directory": "https://github.com/loyaltypollution/language-directory.git#866cc934372a19e514af0025947ce7f1f5a6e1fc",
44+
"@sourceacademy/conductor": "https://github.com/source-academy/conductor.git#0.2.1",
45+
"@sourceacademy/language-directory": "https://github.com/source-academy/language-directory.git#0.0.4",
46+
"@sourceacademy/plugin-directory": "https://github.com/source-academy/plugin-directory.git#0.0.2",
4547
"@sourceacademy/sharedb-ace": "2.1.1",
4648
"@sourceacademy/sling-client": "^0.1.0",
4749
"@szhsin/react-menu": "^4.0.0",
@@ -54,7 +56,6 @@
5456
"array-move": "^4.0.0",
5557
"browserfs": "^1.4.3",
5658
"classnames": "^2.3.2",
57-
"conductor": "https://github.com/source-academy/conductor.git#0.2.1",
5859
"dayjs": "^1.11.13",
5960
"dompurify": "^3.2.4",
6061
"flexboxgrid": "^6.3.1",

src/commons/application/ApplicationTypes.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { Chapter, Language, type SourceError, type Value, Variant } from 'js-sla
22

33
import type { AchievementState } from '../../features/achievement/AchievementTypes';
44
import type { DashboardState } from '../../features/dashboard/DashboardTypes';
5-
import type { LanguageDirectoryState } from '../../features/languageDirectory/LanguageDirectoryTypes';
5+
import type { LanguageDirectoryState } from '../../features/directory/LanguageDirectoryTypes';
6+
import type { PluginDirectoryState } from '../../features/directory/PluginDirectoryTypes';
67
import type { LeaderboardState } from '../../features/leaderboard/LeaderboardTypes';
78
import type { PlaygroundState } from '../../features/playground/PlaygroundTypes';
89
import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes';
@@ -39,6 +40,7 @@ export type OverallState = {
3940
readonly sideContent: SideContentManagerState;
4041
readonly vscode: VscodeState;
4142
readonly languageDirectory: LanguageDirectoryState;
43+
readonly pluginDirectory: PluginDirectoryState;
4244
};
4345

4446
export type Story = {
@@ -618,7 +620,13 @@ export const defaultVscode: VscodeState = {
618620
export const defaultLanguageDirectory: LanguageDirectoryState = {
619621
selectedLanguageId: null,
620622
selectedEvaluatorId: null,
621-
languages: []
623+
languages: [],
624+
languageMap: {}
625+
};
626+
627+
export const defaultPluginDirectory: PluginDirectoryState = {
628+
plugins: [],
629+
pluginMap: {}
622630
};
623631

624632
export const defaultState: OverallState = {
@@ -634,5 +642,6 @@ export const defaultState: OverallState = {
634642
fileSystem: defaultFileSystem,
635643
sideContent: defaultSideContentManager,
636644
vscode: defaultVscode,
637-
languageDirectory: defaultLanguageDirectory
645+
languageDirectory: defaultLanguageDirectory,
646+
pluginDirectory: defaultPluginDirectory
638647
};

src/commons/application/reducers/RootReducer.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { combineReducers, type Reducer } from '@reduxjs/toolkit';
33
import { FeatureFlagsReducer as featureFlags } from '../../..//commons/featureFlags';
44
import { AchievementReducer as achievement } from '../../../features/achievement/AchievementReducer';
55
import { DashboardReducer as dashboard } from '../../../features/dashboard/DashboardReducer';
6-
import { LanguageDirectoryReducer as languageDirectory } from '../../../features/languageDirectory/LanguageDirectoryReducer';
6+
import { LanguageDirectoryReducer as languageDirectory } from '../../../features/directory/LanguageDirectoryReducer';
7+
import { PluginDirectoryReducer as pluginDirectory } from '../../../features/directory/PluginDirectoryReducer';
78
import { LeaderboardReducer as leaderboard } from '../../../features/leaderboard/LeaderboardReducer';
89
import { PlaygroundReducer as playground } from '../../../features/playground/PlaygroundReducer';
910
import { StoriesReducer as stories } from '../../../features/stories/StoriesReducer';
@@ -29,7 +30,8 @@ const rootReducer: Reducer<OverallState, SourceActionType> = combineReducers({
2930
fileSystem,
3031
sideContent,
3132
vscode,
32-
languageDirectory
33+
languageDirectory,
34+
pluginDirectory
3335
});
3436

3537
export default rootReducer;

src/commons/controlBar/ControlBarChapterSelect.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Button, Menu, MenuItem } from '@blueprintjs/core';
22
import { IconNames } from '@blueprintjs/icons';
33
import { ItemListRenderer, ItemRenderer, Select } from '@blueprintjs/select';
4+
import { IEvaluatorDefinition } from '@sourceacademy/language-directory/dist/types';
45
import { Chapter, Variant } from 'js-slang/dist/types';
5-
import React, { useEffect } from 'react';
6+
import React from 'react';
67
import { useDispatch } from 'react-redux';
78

8-
import { flagLanguageDirectoryEnable } from '../../features/languageDirectory/flagLanguageDirectory';
9-
import LanguageDirectoryActions from '../../features/languageDirectory/LanguageDirectoryActions';
10-
import type { IEvaluatorDefinition } from '../../features/languageDirectory/LanguageDirectoryTypes';
9+
import { flagDirectoryLanguageEnable } from '../../features/directory/flagDirectoryLanguageEnable';
10+
import LanguageDirectoryActions from '../../features/directory/LanguageDirectoryActions';
1111
import { SALanguage } from '../application/ApplicationTypes';
1212
import { useFeature } from '../featureFlags/useFeature';
1313
import { useTypedSelector } from '../utils/Hooks';
@@ -34,17 +34,11 @@ export const ControlBarChapterSelect: React.FC<ControlBarChapterSelectProps> = (
3434
disabled = false
3535
}) => {
3636
const dispatch = useDispatch();
37-
const directoryEnabled = useFeature(flagLanguageDirectoryEnable);
37+
const directoryEnabled = useFeature(flagDirectoryLanguageEnable);
3838
const selectedLanguageId = useTypedSelector(s => s.languageDirectory.selectedLanguageId);
3939
const selectedEvaluatorId = useTypedSelector(s => s.languageDirectory.selectedEvaluatorId);
4040
const dirLanguages = useTypedSelector(s => s.languageDirectory.languages);
4141

42-
useEffect(() => {
43-
if (directoryEnabled && dirLanguages.length === 0) {
44-
dispatch(LanguageDirectoryActions.fetchLanguages());
45-
}
46-
}, [directoryEnabled, dirLanguages.length, dispatch]);
47-
4842
if (!directoryEnabled) {
4943
return (
5044
<LegacyControlBarChapterSelect

src/commons/featureFlags/FeatureFlag.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { SagaIterator } from 'redux-saga';
2+
13
export class FeatureFlag<T> {
24
private readonly _flagName: string;
35
private readonly _defaultValue: T;
46
private readonly _flagDesc?: string;
7+
private readonly _callback?: (newValue: any) => SagaIterator; // using any as the action wipes out type-param
58

69
get flagName(): string {
710
return this._flagName;
@@ -12,10 +15,19 @@ export class FeatureFlag<T> {
1215
get flagDesc(): string | undefined {
1316
return this._flagDesc;
1417
}
18+
onChange(newValue: T) {
19+
return this._callback?.(newValue);
20+
}
1521

16-
constructor(flagName: string, defaultValue: T, flagDesc?: string) {
22+
constructor(
23+
flagName: string,
24+
defaultValue: T,
25+
flagDesc?: string,
26+
callback?: (newValue: T) => SagaIterator
27+
) {
1728
this._flagName = flagName;
1829
this._defaultValue = defaultValue;
1930
this._flagDesc = flagDesc;
31+
this._callback = callback;
2032
}
2133
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { OverallState } from '../application/ApplicationTypes';
22
import { FeatureFlag } from './FeatureFlag';
33

4-
export const featureSelector = (featureFlag: FeatureFlag<any>) => (state: OverallState) =>
5-
state.featureFlags.modifiedFlags[featureFlag.flagName];
4+
export function featureSelector<T>(featureFlag: FeatureFlag<T>) {
5+
return (state: OverallState) =>
6+
(state.featureFlags.modifiedFlags[featureFlag.flagName] || featureFlag.defaultValue) as T;
7+
}

src/commons/featureFlags/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createSlice } from '@reduxjs/toolkit';
2+
import { SagaIterator } from 'redux-saga';
23

34
import { FeatureFlag } from './FeatureFlag';
45

@@ -33,7 +34,8 @@ export const FeatureFlagsReducer = featureFlagsSlice.reducer;
3334
export function createFeatureFlag<T>(
3435
flagName: string,
3536
defaultValue: T,
36-
flagDesc?: string
37+
flagDesc?: string,
38+
callback?: (newValue: T) => SagaIterator
3739
): FeatureFlag<T> {
38-
return new FeatureFlag<T>(flagName, defaultValue, flagDesc);
40+
return new FeatureFlag<T>(flagName, defaultValue, flagDesc, callback);
3941
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { flagConductorEnable } from '../../features/conductor/flagConductorEnable';
22
import { flagConductorEvaluatorUrl } from '../../features/conductor/flagConductorEvaluatorUrl';
3-
import { flagLanguageDirectoryEnable } from '../../features/languageDirectory/flagLanguageDirectory';
3+
import { flagDirectoryLanguageEnable } from '../../features/directory/flagDirectoryLanguageEnable';
4+
import { flagDirectoryLanguageUrl } from '../../features/directory/flagDirectoryLanguageUrl';
5+
import { flagDirectoryPluginUrl } from '../../features/directory/flagDirectoryPluginUrl';
46
import { FeatureFlag } from './FeatureFlag';
57

68
export const publicFlags: FeatureFlag<any>[] = [
79
flagConductorEnable,
810
flagConductorEvaluatorUrl,
9-
flagLanguageDirectoryEnable
11+
flagDirectoryLanguageEnable,
12+
flagDirectoryLanguageUrl,
13+
flagDirectoryPluginUrl
1014
];

src/commons/mocks/StoreMocks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
defaultLanguageDirectory,
1010
defaultLeaderboard,
1111
defaultPlayground,
12+
defaultPluginDirectory,
1213
defaultRouter,
1314
defaultSession,
1415
defaultSideContentManager,
@@ -38,7 +39,8 @@ export function mockInitialStore(
3839
fileSystem: defaultFileSystem,
3940
sideContent: defaultSideContentManager,
4041
vscode: defaultVscode,
41-
languageDirectory: defaultLanguageDirectory
42+
languageDirectory: defaultLanguageDirectory,
43+
pluginDirectory: defaultPluginDirectory
4244
};
4345

4446
const lodashMergeCustomizer = (objValue: any, srcValue: any) => {

src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Position } from '@blueprintjs/core';
2-
import { useEffect, useState } from 'react';
2+
import { useState } from 'react';
33
import { useDispatch } from 'react-redux';
44
import { useFeature } from 'src/commons/featureFlags/useFeature';
55
import SimpleDropdown from 'src/commons/SimpleDropdown';
66
import { useTypedSelector } from 'src/commons/utils/Hooks';
7-
import { flagLanguageDirectoryEnable } from 'src/features/languageDirectory/flagLanguageDirectory';
8-
import LanguageDirectoryActions from 'src/features/languageDirectory/LanguageDirectoryActions';
7+
import { flagDirectoryLanguageEnable } from 'src/features/directory/flagDirectoryLanguageEnable';
8+
import LanguageDirectoryActions from 'src/features/directory/LanguageDirectoryActions';
99

1010
//TODO <remove legacy>: Remove when conductors.languageDirectory is default behaviour
1111
import LegacyNavigationBarLangSelectButton from './LegacyNavigationBarLangSelectButton';
@@ -21,14 +21,8 @@ const NavigationBarLangSelectButton = () => {
2121

2222
const dispatch = useDispatch();
2323
const dirOptions = useDirectoryOptions();
24-
const languagesLoaded = useTypedSelector(s => s.languageDirectory.languages.length > 0);
25-
useEffect(() => {
26-
if (!languagesLoaded) {
27-
dispatch(LanguageDirectoryActions.fetchLanguages());
28-
}
29-
}, [languagesLoaded, dispatch]);
30-
31-
const directoryEnabled = useFeature(flagLanguageDirectoryEnable);
24+
25+
const directoryEnabled = useFeature(flagDirectoryLanguageEnable);
3226
if (!directoryEnabled) {
3327
return <LegacyNavigationBarLangSelectButton />;
3428
}

0 commit comments

Comments
 (0)