From d2a59e05963525d85d66aa0bd9e138cc3a0fa064 Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:54:28 +0000 Subject: [PATCH 01/16] Add temporary language-directory dependency --- package.json | 1 + yarn.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/package.json b/package.json index f0972eac14..25699fa6b1 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@reduxjs/toolkit": "^1.9.7", "@sentry/browser": "^8.33.0", "@sourceacademy/c-slang": "^1.0.21", + "@sourceacademy/language-directory": "https://github.com/source-academy/language-directory.git", "@sourceacademy/sharedb-ace": "2.1.1", "@sourceacademy/sling-client": "^0.1.0", "@szhsin/react-menu": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index d6dc1329b4..cc7b5d7f11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3259,6 +3259,13 @@ __metadata: languageName: node linkType: hard +"@sourceacademy/language-directory@https://github.com/source-academy/language-directory.git": + version: 0.0.2 + resolution: "@sourceacademy/language-directory@https://github.com/source-academy/language-directory.git#commit=e2cef7eac0ba31d9f7ceaff4a9d20ed7738f52f2" + checksum: 10c0/cac2185b3d78330de190ac58d786093940982f194991a6088c41b83dfbffbe37191d1746db104542902ce5128e00892ae4051b292df312f40997c8720b88c1d8 + languageName: node + linkType: hard + "@sourceacademy/sharedb-ace@npm:2.1.1": version: 2.1.1 resolution: "@sourceacademy/sharedb-ace@npm:2.1.1" @@ -7545,6 +7552,7 @@ __metadata: "@rsbuild/plugin-svgr": "npm:^1.2.0" "@sentry/browser": "npm:^8.33.0" "@sourceacademy/c-slang": "npm:^1.0.21" + "@sourceacademy/language-directory": "https://github.com/source-academy/language-directory.git" "@sourceacademy/sharedb-ace": "npm:2.1.1" "@sourceacademy/sling-client": "npm:^0.1.0" "@svgr/webpack": "npm:^8.0.0" From 93859d5de1531b0ea2cfba4c259eaa4b70c95c46 Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:55:13 +0000 Subject: [PATCH 02/16] Add static provider for language directory --- src/commons/languageDirectory/provider.ts | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/commons/languageDirectory/provider.ts diff --git a/src/commons/languageDirectory/provider.ts b/src/commons/languageDirectory/provider.ts new file mode 100644 index 0000000000..af4886ed0e --- /dev/null +++ b/src/commons/languageDirectory/provider.ts @@ -0,0 +1,36 @@ +import type { IEvaluatorDefinition, ILanguageDefinition } from '@sourceacademy/language-directory'; +import { getEvaluatorDefinition as getEvaluatorDefinitionFromDirectory, languageMap, languages } from '@sourceacademy/language-directory'; + +export interface LanguageDirectoryProvider { + getLanguages(): Promise; + getLanguageById(languageId: string): Promise; + getEvaluatorDefinition( + languageId: string, + evaluatorId: string + ): Promise; +} + +export class StaticLanguageDirectoryProvider implements LanguageDirectoryProvider { + async getLanguages(): Promise { + return languages; + } + + async getLanguageById(languageId: string): Promise { + return languageMap.get(languageId); + } + + async getEvaluatorDefinition( + languageId: string, + evaluatorId: string + ): Promise { + const language = languageMap.get(languageId); + if (!language) return undefined; + return getEvaluatorDefinitionFromDirectory(language, evaluatorId); + } +} + +export const staticLanguageDirectoryProvider = new StaticLanguageDirectoryProvider(); + +export type { ILanguageDefinition, IEvaluatorDefinition }; + + From 229d689d79fc52cbe411d6bdcf7ee006a0a17f74 Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:03:40 +0000 Subject: [PATCH 03/16] Add language directory flag --- src/commons/application/ApplicationTypes.ts | 10 ++- .../application/reducers/RootReducer.ts | 30 ++++---- src/commons/featureFlags/publicFlags.ts | 8 +- .../NavigationBarLangSelectButton.tsx | 75 ++++++++++++++++--- src/commons/sagas/LanguageDirectorySaga.ts | 33 ++++++++ src/commons/sagas/MainSaga.ts | 28 +++---- .../LanguageDirectoryActions.ts | 10 +++ .../LanguageDirectoryReducer.ts | 22 ++++++ .../LanguageDirectoryTypes.ts | 9 +++ .../flagLanguageDirectory.ts | 10 +++ 10 files changed, 194 insertions(+), 41 deletions(-) create mode 100644 src/commons/sagas/LanguageDirectorySaga.ts create mode 100644 src/features/languageDirectory/LanguageDirectoryActions.ts create mode 100644 src/features/languageDirectory/LanguageDirectoryReducer.ts create mode 100644 src/features/languageDirectory/LanguageDirectoryTypes.ts create mode 100644 src/features/languageDirectory/flagLanguageDirectory.ts diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index d6984afa63..ce915c4376 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -2,6 +2,7 @@ import { Chapter, Language, type SourceError, type Value, Variant } from 'js-sla import type { AchievementState } from '../../features/achievement/AchievementTypes'; import type { DashboardState } from '../../features/dashboard/DashboardTypes'; +import type { LanguageDirectoryState } from '../../features/languageDirectory/LanguageDirectoryTypes'; import type { LeaderboardState } from '../../features/leaderboard/LeaderboardTypes'; import type { PlaygroundState } from '../../features/playground/PlaygroundTypes'; import { PlaybackStatus, RecordingStatus } from '../../features/sourceRecorder/SourceRecorderTypes'; @@ -37,6 +38,7 @@ export type OverallState = { readonly fileSystem: FileSystemState; readonly sideContent: SideContentManagerState; readonly vscode: VscodeState; + readonly languageDirectory: LanguageDirectoryState; }; export type Story = { @@ -613,6 +615,11 @@ export const defaultVscode: VscodeState = { isVscode: false }; +export const defaultLanguageDirectory: LanguageDirectoryState = { + selectedLanguageId: null, + selectedEvaluatorId: null +}; + export const defaultState: OverallState = { router: defaultRouter, achievement: defaultAchievement, @@ -625,5 +632,6 @@ export const defaultState: OverallState = { featureFlags: defaultFeatureFlags, fileSystem: defaultFileSystem, sideContent: defaultSideContentManager, - vscode: defaultVscode + vscode: defaultVscode, + languageDirectory: defaultLanguageDirectory }; diff --git a/src/commons/application/reducers/RootReducer.ts b/src/commons/application/reducers/RootReducer.ts index 4c89cf6377..3d966b120e 100644 --- a/src/commons/application/reducers/RootReducer.ts +++ b/src/commons/application/reducers/RootReducer.ts @@ -1,14 +1,15 @@ -import { combineReducers, Reducer } from '@reduxjs/toolkit'; -import { SourceActionType } from 'src/commons/utils/ActionsHelper'; +import { combineReducers, type Reducer } from '@reduxjs/toolkit'; import { FeatureFlagsReducer as featureFlags } from '../../..//commons/featureFlags'; import { AchievementReducer as achievement } from '../../../features/achievement/AchievementReducer'; import { DashboardReducer as dashboard } from '../../../features/dashboard/DashboardReducer'; +import { LanguageDirectoryReducer as languageDirectory } from '../../../features/languageDirectory/LanguageDirectoryReducer'; import { LeaderboardReducer as leaderboard } from '../../../features/leaderboard/LeaderboardReducer'; import { PlaygroundReducer as playground } from '../../../features/playground/PlaygroundReducer'; import { StoriesReducer as stories } from '../../../features/stories/StoriesReducer'; import { FileSystemReducer as fileSystem } from '../../fileSystem/FileSystemReducer'; import { SideContentReducer as sideContent } from '../../sideContent/SideContentReducer'; +import type { SourceActionType } from '../../utils/ActionsHelper'; import { WorkspaceReducer as workspaces } from '../../workspace/WorkspaceReducer'; import { OverallState } from '../ApplicationTypes'; import { RouterReducer as router } from './CommonsReducer'; @@ -16,18 +17,19 @@ import { SessionsReducer as session } from './SessionsReducer'; import { VscodeReducer as vscode } from './VscodeReducer'; const rootReducer: Reducer = combineReducers({ - router, - achievement, - leaderboard, - dashboard, - playground, - session, - stories, - workspaces, - featureFlags, - fileSystem, - sideContent, - vscode + router, + achievement, + leaderboard, + dashboard, + playground, + session, + stories, + workspaces, + featureFlags, + fileSystem, + sideContent, + vscode, + languageDirectory }); export default rootReducer; diff --git a/src/commons/featureFlags/publicFlags.ts b/src/commons/featureFlags/publicFlags.ts index d5e23f274e..d719986b64 100644 --- a/src/commons/featureFlags/publicFlags.ts +++ b/src/commons/featureFlags/publicFlags.ts @@ -1,5 +1,11 @@ import { flagConductorEnable } from '../../features/conductor/flagConductorEnable'; import { flagConductorEvaluatorUrl } from '../../features/conductor/flagConductorEvaluatorUrl'; +import { flagLanguageDirectoryEnable } from '../../features/languageDirectory/flagLanguageDirectory'; import { FeatureFlag } from './FeatureFlag'; -export const publicFlags: FeatureFlag[] = [flagConductorEnable, flagConductorEvaluatorUrl]; + +export const publicFlags: FeatureFlag[] = [ + flagConductorEnable, + flagConductorEvaluatorUrl, + flagLanguageDirectoryEnable +]; diff --git a/src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx b/src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx index abdc495804..5bd2935751 100644 --- a/src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx +++ b/src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx @@ -1,5 +1,5 @@ import { Position } from '@blueprintjs/core'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { cLanguages, @@ -12,26 +12,77 @@ import { SUPPORTED_LANGUAGES, SupportedLanguage } from 'src/commons/application/ApplicationTypes'; +import { useFeature } from 'src/commons/featureFlags/useFeature'; +import { staticLanguageDirectoryProvider } from 'src/commons/languageDirectory/provider'; import SimpleDropdown from 'src/commons/SimpleDropdown'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import WorkspaceActions from 'src/commons/workspace/WorkspaceActions'; +import { flagLanguageDirectoryEnable } from 'src/features/languageDirectory/flagLanguageDirectory'; +import LanguageDirectoryActions from 'src/features/languageDirectory/LanguageDirectoryActions'; import { playgroundConfigLanguage } from 'src/features/playground/PlaygroundActions'; -// TODO: Hardcoded to use the first sublanguage for each language -const defaultSublanguages: { - [lang in SupportedLanguage]: SALanguage; -} = { - [SupportedLanguage.JAVASCRIPT]: sourceLanguages[0], - [SupportedLanguage.PYTHON]: pyLanguages[0], - [SupportedLanguage.SCHEME]: schemeLanguages[0], - [SupportedLanguage.JAVA]: javaLanguages[0], - [SupportedLanguage.C]: cLanguages[0] -}; +function useDirectoryOptions(enabled: boolean) { + const [options, setOptions] = useState>([]); + useEffect(() => { + if (!enabled) return; + let cancelled = false; + (async () => { + const langs = await staticLanguageDirectoryProvider.getLanguages(); + if (cancelled) return; + setOptions(langs.map(l => ({ value: l.id, label: l.name }))); + })(); + return () => { + cancelled = true; + }; + }, [enabled]); + return options; +} const NavigationBarLangSelectButton = () => { + const [isOpen, setIsOpen] = useState(false); + const selectedDirLanguageId = useTypedSelector(s => s.languageDirectory.selectedLanguageId); + const dispatch = useDispatch(); + const dirOptions = useDirectoryOptions(true); + + const directoryEnabled = useFeature(flagLanguageDirectoryEnable); + if (!directoryEnabled) { + return ; + } + + const selectDirLanguage = (languageId: string) => { + dispatch(LanguageDirectoryActions.setSelectedLanguage(languageId)); + setIsOpen(false); + }; + + return ( + setIsOpen(false), isOpen }} + buttonProps={{ + rightIcon: 'caret-down', + onClick: () => setIsOpen(true), + 'data-testid': 'NavigationBarLangSelectButton' + }} + /> + ); +}; + +const LegacyNavigationBarLangSelectButton = () => { const [isOpen, setIsOpen] = useState(false); const lang = useTypedSelector(store => store.playground.languageConfig.mainLanguage); const dispatch = useDispatch(); + + // Legacy default sublanguage mapping + const defaultSublanguages: { [lang in SupportedLanguage]: SALanguage } = { + [SupportedLanguage.JAVASCRIPT]: sourceLanguages[0], + [SupportedLanguage.PYTHON]: pyLanguages[0], + [SupportedLanguage.SCHEME]: schemeLanguages[0], + [SupportedLanguage.JAVA]: javaLanguages[0], + [SupportedLanguage.C]: cLanguages[0] + }; + const selectLang = (language: SupportedLanguage) => { const { chapter, variant } = defaultSublanguages[language]; dispatch(playgroundConfigLanguage(getLanguageConfig(chapter, variant))); @@ -41,7 +92,7 @@ const NavigationBarLangSelectButton = () => { return ( ({ value: lang, label: lang }))} + options={SUPPORTED_LANGUAGES.map(l => ({ value: l, label: l }))} onClick={selectLang} selectedValue={lang} popoverProps={{ position: Position.BOTTOM_RIGHT, onClose: () => setIsOpen(false), isOpen }} diff --git a/src/commons/sagas/LanguageDirectorySaga.ts b/src/commons/sagas/LanguageDirectorySaga.ts new file mode 100644 index 0000000000..4f55537492 --- /dev/null +++ b/src/commons/sagas/LanguageDirectorySaga.ts @@ -0,0 +1,33 @@ +import type { SagaIterator } from 'redux-saga'; +import { call, put, select, takeEvery } from 'redux-saga/effects'; +import type { OverallState } from 'src/commons/application/ApplicationTypes'; +import { staticLanguageDirectoryProvider } from 'src/commons/languageDirectory/provider'; + +import Actions from '../../features/languageDirectory/LanguageDirectoryActions'; + +function* resolveDefaultEvaluatorSaga(action: ReturnType): SagaIterator { + try { + const { + payload: { languageId, evaluatorId } + } = action; + if (evaluatorId) return; // already explicitly set + const language = yield call(staticLanguageDirectoryProvider.getLanguageById, languageId); + if (!language) return; + const defaultEvaluatorId: string | null = language.evaluators.length > 0 ? language.evaluators[0].id : null; + if (!defaultEvaluatorId) return; + // If state still matches the same language, set evaluator + const currentLanguageId: string | null = yield select( + (s: OverallState) => s.languageDirectory.selectedLanguageId + ); + if (currentLanguageId !== languageId) return; + yield put(Actions.setSelectedEvaluator(defaultEvaluatorId)); + } catch { + // swallow + } +} + +export default function* LanguageDirectorySaga(): SagaIterator { + yield takeEvery(Actions.setSelectedLanguage.type, resolveDefaultEvaluatorSaga); +} + + diff --git a/src/commons/sagas/MainSaga.ts b/src/commons/sagas/MainSaga.ts index f95d60f00e..c6f5e28b6c 100644 --- a/src/commons/sagas/MainSaga.ts +++ b/src/commons/sagas/MainSaga.ts @@ -6,6 +6,7 @@ import Constants from '../utils/Constants'; import AchievementSaga from './AchievementSaga'; import BackendSaga from './BackendSaga'; import GitHubPersistenceSaga from './GitHubPersistenceSaga'; +import LanguageDirectorySaga from './LanguageDirectorySaga'; import LeaderboardSaga from './LeaderboardSaga'; import LoginSaga from './LoginSaga'; import PersistenceSaga from './PersistenceSaga'; @@ -16,17 +17,18 @@ import StoriesSaga from './StoriesSaga'; import WorkspaceSaga from './WorkspaceSaga'; export default function* MainSaga(): SagaIterator { - yield all([ - fork(Constants.useBackend ? BackendSaga : mockBackendSaga), - fork(WorkspaceSaga), - fork(LoginSaga), - fork(PlaygroundSaga), - fork(AchievementSaga), - fork(LeaderboardSaga), - fork(PersistenceSaga), - fork(GitHubPersistenceSaga), - fork(RemoteExecutionSaga), - fork(StoriesSaga), - fork(SideContentSaga) - ]); + yield all([ + fork(Constants.useBackend ? BackendSaga : mockBackendSaga), + fork(WorkspaceSaga), + fork(LoginSaga), + fork(PlaygroundSaga), + fork(AchievementSaga), + fork(LeaderboardSaga), + fork(PersistenceSaga), + fork(GitHubPersistenceSaga), + fork(RemoteExecutionSaga), + fork(StoriesSaga), + fork(SideContentSaga), + fork(LanguageDirectorySaga) + ]); } diff --git a/src/features/languageDirectory/LanguageDirectoryActions.ts b/src/features/languageDirectory/LanguageDirectoryActions.ts new file mode 100644 index 0000000000..99f521ba3a --- /dev/null +++ b/src/features/languageDirectory/LanguageDirectoryActions.ts @@ -0,0 +1,10 @@ +import { createActions } from 'src/commons/redux/utils'; + +export default createActions('conductor/languageDirectory', { + /** Set selected language; evaluatorId optional (defaults to first available) */ + setSelectedLanguage: (languageId: string, evaluatorId?: string) => ({ languageId, evaluatorId }), + /** Set selected evaluator explicitly */ + setSelectedEvaluator: (evaluatorId: string) => ({ evaluatorId }) +}); + + diff --git a/src/features/languageDirectory/LanguageDirectoryReducer.ts b/src/features/languageDirectory/LanguageDirectoryReducer.ts new file mode 100644 index 0000000000..6670f1daa2 --- /dev/null +++ b/src/features/languageDirectory/LanguageDirectoryReducer.ts @@ -0,0 +1,22 @@ +import { createReducer, type Reducer } from '@reduxjs/toolkit'; +// Note: resolver handled in saga; reducer remains synchronous +import type { SourceActionType } from 'src/commons/utils/ActionsHelper'; + +import Actions from './LanguageDirectoryActions'; +import type { LanguageDirectoryState } from './LanguageDirectoryTypes'; +import { defaultLanguageDirectoryState } from './LanguageDirectoryTypes'; + +export const LanguageDirectoryReducer: Reducer = + createReducer(defaultLanguageDirectoryState, builder => { + builder + .addCase(Actions.setSelectedLanguage, (state, action) => { + const { languageId, evaluatorId } = action.payload; + state.selectedLanguageId = languageId; + state.selectedEvaluatorId = evaluatorId ?? null; + }) + .addCase(Actions.setSelectedEvaluator, (state, action) => { + state.selectedEvaluatorId = action.payload.evaluatorId; + }); + }); + + diff --git a/src/features/languageDirectory/LanguageDirectoryTypes.ts b/src/features/languageDirectory/LanguageDirectoryTypes.ts new file mode 100644 index 0000000000..6fe4f6fe05 --- /dev/null +++ b/src/features/languageDirectory/LanguageDirectoryTypes.ts @@ -0,0 +1,9 @@ +export type LanguageDirectoryState = { + readonly selectedLanguageId: string | null; + readonly selectedEvaluatorId: string | null; +}; + +export const defaultLanguageDirectoryState: LanguageDirectoryState = { + selectedLanguageId: null, + selectedEvaluatorId: null +}; diff --git a/src/features/languageDirectory/flagLanguageDirectory.ts b/src/features/languageDirectory/flagLanguageDirectory.ts new file mode 100644 index 0000000000..07614141a0 --- /dev/null +++ b/src/features/languageDirectory/flagLanguageDirectory.ts @@ -0,0 +1,10 @@ +import { createFeatureFlag } from '../../commons/featureFlags'; +import { featureSelector } from '../../commons/featureFlags/featureSelector'; + +export const flagLanguageDirectoryEnable = createFeatureFlag( + 'conductor.language.directory', + false, + 'Enable new language directory powered selection UI and runtime selection.' +); + +export const selectLanguageDirectoryEnable = featureSelector(flagLanguageDirectoryEnable); From 296256fe9e00919eed8049a9b4b306cca0f9b466 Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:18:00 +0000 Subject: [PATCH 04/16] Reroute language-directory for testing --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 25699fa6b1..64cd20daa1 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@reduxjs/toolkit": "^1.9.7", "@sentry/browser": "^8.33.0", "@sourceacademy/c-slang": "^1.0.21", - "@sourceacademy/language-directory": "https://github.com/source-academy/language-directory.git", + "@sourceacademy/language-directory": "https://github.com/loyaltypollution/language-directory.git", "@sourceacademy/sharedb-ace": "2.1.1", "@sourceacademy/sling-client": "^0.1.0", "@szhsin/react-menu": "^4.0.0", @@ -68,7 +68,6 @@ "js-slang": "^1.0.84", "js-yaml": "^4.1.0", "konva": "^9.2.0", - "language-directory": "https://github.com/source-academy/language-directory.git", "lodash": "^4.17.21", "lz-string": "^1.4.4", "mdast-util-from-markdown": "^2.0.0", From d781cc81c5710c2f3513c39d7c078c704a197469 Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:55:26 +0000 Subject: [PATCH 05/16] Remove unused export --- src/features/languageDirectory/LanguageDirectoryTypes.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/features/languageDirectory/LanguageDirectoryTypes.ts b/src/features/languageDirectory/LanguageDirectoryTypes.ts index 6fe4f6fe05..185a268181 100644 --- a/src/features/languageDirectory/LanguageDirectoryTypes.ts +++ b/src/features/languageDirectory/LanguageDirectoryTypes.ts @@ -1,9 +1,4 @@ export type LanguageDirectoryState = { readonly selectedLanguageId: string | null; readonly selectedEvaluatorId: string | null; -}; - -export const defaultLanguageDirectoryState: LanguageDirectoryState = { - selectedLanguageId: null, - selectedEvaluatorId: null -}; +}; \ No newline at end of file From 0b000aa06a044aab1e764985f2a3391bdb72ec3a Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Sat, 16 Aug 2025 12:47:59 +0000 Subject: [PATCH 06/16] Configure temporary yarn directory --- yarn.lock | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index cc7b5d7f11..5d36eb56bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3259,10 +3259,10 @@ __metadata: languageName: node linkType: hard -"@sourceacademy/language-directory@https://github.com/source-academy/language-directory.git": +"@sourceacademy/language-directory@https://github.com/loyaltypollution/language-directory.git": version: 0.0.2 - resolution: "@sourceacademy/language-directory@https://github.com/source-academy/language-directory.git#commit=e2cef7eac0ba31d9f7ceaff4a9d20ed7738f52f2" - checksum: 10c0/cac2185b3d78330de190ac58d786093940982f194991a6088c41b83dfbffbe37191d1746db104542902ce5128e00892ae4051b292df312f40997c8720b88c1d8 + resolution: "@sourceacademy/language-directory@https://github.com/loyaltypollution/language-directory.git#commit=d99bd4163c045c16f93bf9a80afa1eda42fc4b02" + checksum: 10c0/b20111eff59add47bd1c160af64bfa82140c35b7ab1abfd800992f330654c002f997699647b4128fe2e79fcf57b767502aa40366ab6f18ca50ef066263e76423 languageName: node linkType: hard @@ -7552,7 +7552,7 @@ __metadata: "@rsbuild/plugin-svgr": "npm:^1.2.0" "@sentry/browser": "npm:^8.33.0" "@sourceacademy/c-slang": "npm:^1.0.21" - "@sourceacademy/language-directory": "https://github.com/source-academy/language-directory.git" + "@sourceacademy/language-directory": "https://github.com/loyaltypollution/language-directory.git" "@sourceacademy/sharedb-ace": "npm:2.1.1" "@sourceacademy/sling-client": "npm:^0.1.0" "@svgr/webpack": "npm:^8.0.0" @@ -7621,7 +7621,6 @@ __metadata: js-yaml: "npm:^4.1.0" jsdom: "npm:^26.0.0" konva: "npm:^9.2.0" - language-directory: "https://github.com/source-academy/language-directory.git" lodash: "npm:^4.17.21" lz-string: "npm:^1.4.4" mdast-util-from-markdown: "npm:^2.0.0" @@ -9300,13 +9299,6 @@ __metadata: languageName: node linkType: hard -"language-directory@https://github.com/source-academy/language-directory.git": - version: 0.0.1 - resolution: "language-directory@https://github.com/source-academy/language-directory.git#commit=8e406e4b28a7df5e3e66fdf3005092d0cf861143" - checksum: 10c0/2db1dd6a94497232e2a183f10769f9e98658636ad3c0c56ae2d54abd5a414dcfbaa994e651ac94507d129147da523254d81103a75f5e0ad2cbaf2274ee3f146d - languageName: node - linkType: hard - "lcov-parse@npm:^1.0.0": version: 1.0.0 resolution: "lcov-parse@npm:1.0.0" From 8de713d2336d1a359cb65e1a72fd4ef6b0adf28a Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Sat, 16 Aug 2025 12:48:54 +0000 Subject: [PATCH 07/16] Refactor out LegacyButton behaviour --- .../LegacyNavigationBarLangSelectButton.tsx | 55 +++++++++++++ .../NavigationBarLangSelectButton.tsx | 81 ++++--------------- 2 files changed, 70 insertions(+), 66 deletions(-) create mode 100644 src/commons/navigationBar/subcomponents/LegacyNavigationBarLangSelectButton.tsx diff --git a/src/commons/navigationBar/subcomponents/LegacyNavigationBarLangSelectButton.tsx b/src/commons/navigationBar/subcomponents/LegacyNavigationBarLangSelectButton.tsx new file mode 100644 index 0000000000..739e92fa64 --- /dev/null +++ b/src/commons/navigationBar/subcomponents/LegacyNavigationBarLangSelectButton.tsx @@ -0,0 +1,55 @@ +import { Position } from '@blueprintjs/core'; +import { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { + cLanguages, + getLanguageConfig, + javaLanguages, + pyLanguages, + SALanguage, + schemeLanguages, + sourceLanguages, + SUPPORTED_LANGUAGES, + SupportedLanguage} from 'src/commons/application/ApplicationTypes'; +import SimpleDropdown from 'src/commons/SimpleDropdown'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; +import WorkspaceActions from 'src/commons/workspace/WorkspaceActions'; +import { playgroundConfigLanguage } from 'src/features/playground/PlaygroundActions'; + +const LegacyNavigationBarLangSelectButton = () => { + const [isOpen, setIsOpen] = useState(false); + const lang = useTypedSelector(store => store.playground.languageConfig.mainLanguage); + const dispatch = useDispatch(); + + // Legacy default sublanguage mapping + const defaultSublanguages: { [lang in SupportedLanguage]: SALanguage } = { + [SupportedLanguage.JAVASCRIPT]: sourceLanguages[0], + [SupportedLanguage.PYTHON]: pyLanguages[0], + [SupportedLanguage.SCHEME]: schemeLanguages[0], + [SupportedLanguage.JAVA]: javaLanguages[0], + [SupportedLanguage.C]: cLanguages[0] + }; + + const selectLang = (language: SupportedLanguage) => { + const { chapter, variant } = defaultSublanguages[language]; + dispatch(playgroundConfigLanguage(getLanguageConfig(chapter, variant))); + dispatch(WorkspaceActions.chapterSelect(chapter, variant, 'playground')); + setIsOpen(false); + }; + + return ( + ({ value: l, label: l }))} + onClick={selectLang} + selectedValue={lang} + popoverProps={{ position: Position.BOTTOM_RIGHT, onClose: () => setIsOpen(false), isOpen }} + buttonProps={{ + rightIcon: 'caret-down', + onClick: () => setIsOpen(true), + 'data-testid': 'NavigationBarLangSelectButton' + }} + /> + ); +}; + +export default LegacyNavigationBarLangSelectButton; diff --git a/src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx b/src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx index 5bd2935751..b77d78665c 100644 --- a/src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx +++ b/src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx @@ -1,48 +1,33 @@ import { Position } from '@blueprintjs/core'; import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { - cLanguages, - getLanguageConfig, - javaLanguages, - pyLanguages, - SALanguage, - schemeLanguages, - sourceLanguages, - SUPPORTED_LANGUAGES, - SupportedLanguage -} from 'src/commons/application/ApplicationTypes'; import { useFeature } from 'src/commons/featureFlags/useFeature'; -import { staticLanguageDirectoryProvider } from 'src/commons/languageDirectory/provider'; import SimpleDropdown from 'src/commons/SimpleDropdown'; import { useTypedSelector } from 'src/commons/utils/Hooks'; -import WorkspaceActions from 'src/commons/workspace/WorkspaceActions'; import { flagLanguageDirectoryEnable } from 'src/features/languageDirectory/flagLanguageDirectory'; import LanguageDirectoryActions from 'src/features/languageDirectory/LanguageDirectoryActions'; -import { playgroundConfigLanguage } from 'src/features/playground/PlaygroundActions'; +import type { ILanguageDefinition } from 'src/features/languageDirectory/LanguageDirectoryTypes'; -function useDirectoryOptions(enabled: boolean) { - const [options, setOptions] = useState>([]); - useEffect(() => { - if (!enabled) return; - let cancelled = false; - (async () => { - const langs = await staticLanguageDirectoryProvider.getLanguages(); - if (cancelled) return; - setOptions(langs.map(l => ({ value: l.id, label: l.name }))); - })(); - return () => { - cancelled = true; - }; - }, [enabled]); - return options; +// Remove when conductors.languageDirectory is default behaviour +import LegacyNavigationBarLangSelectButton from './LegacyNavigationBarLangSelectButton'; + +function useDirectoryOptions() { + const langs = useTypedSelector(s => s.languageDirectory.languages) as ILanguageDefinition[]; + return langs.map(l => ({ value: l.id, label: l.name })); } const NavigationBarLangSelectButton = () => { const [isOpen, setIsOpen] = useState(false); const selectedDirLanguageId = useTypedSelector(s => s.languageDirectory.selectedLanguageId); + const dispatch = useDispatch(); - const dirOptions = useDirectoryOptions(true); + const dirOptions = useDirectoryOptions(); + const languagesLoaded = useTypedSelector(s => s.languageDirectory.languages.length > 0); + useEffect(() => { + if (!languagesLoaded) { + dispatch(LanguageDirectoryActions.fetchLanguages()); + } + }, [languagesLoaded, dispatch]); const directoryEnabled = useFeature(flagLanguageDirectoryEnable); if (!directoryEnabled) { @@ -69,40 +54,4 @@ const NavigationBarLangSelectButton = () => { ); }; -const LegacyNavigationBarLangSelectButton = () => { - const [isOpen, setIsOpen] = useState(false); - const lang = useTypedSelector(store => store.playground.languageConfig.mainLanguage); - const dispatch = useDispatch(); - - // Legacy default sublanguage mapping - const defaultSublanguages: { [lang in SupportedLanguage]: SALanguage } = { - [SupportedLanguage.JAVASCRIPT]: sourceLanguages[0], - [SupportedLanguage.PYTHON]: pyLanguages[0], - [SupportedLanguage.SCHEME]: schemeLanguages[0], - [SupportedLanguage.JAVA]: javaLanguages[0], - [SupportedLanguage.C]: cLanguages[0] - }; - - const selectLang = (language: SupportedLanguage) => { - const { chapter, variant } = defaultSublanguages[language]; - dispatch(playgroundConfigLanguage(getLanguageConfig(chapter, variant))); - dispatch(WorkspaceActions.chapterSelect(chapter, variant, 'playground')); - setIsOpen(false); - }; - - return ( - ({ value: l, label: l }))} - onClick={selectLang} - selectedValue={lang} - popoverProps={{ position: Position.BOTTOM_RIGHT, onClose: () => setIsOpen(false), isOpen }} - buttonProps={{ - rightIcon: 'caret-down', - onClick: () => setIsOpen(true), - 'data-testid': 'NavigationBarLangSelectButton' - }} - /> - ); -}; - export default NavigationBarLangSelectButton; From fd02fd7320ea2731b75eff1e7aadc386a0dfba47 Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Sat, 16 Aug 2025 12:49:46 +0000 Subject: [PATCH 08/16] Manage language-directory languages in redux --- src/commons/application/ApplicationTypes.ts | 3 +- src/commons/languageDirectory/provider.ts | 36 ------------------ src/commons/sagas/LanguageDirectorySaga.ts | 29 ++++++++------- src/commons/utils/ActionsHelper.ts | 4 +- .../LanguageDirectoryActions.ts | 8 +++- .../LanguageDirectoryReducer.ts | 16 +++++--- .../LanguageDirectoryTypes.ts | 37 ++++++++++++++++++- 7 files changed, 73 insertions(+), 60 deletions(-) delete mode 100644 src/commons/languageDirectory/provider.ts diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index ce915c4376..4524a67eae 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -617,7 +617,8 @@ export const defaultVscode: VscodeState = { export const defaultLanguageDirectory: LanguageDirectoryState = { selectedLanguageId: null, - selectedEvaluatorId: null + selectedEvaluatorId: null, + languages: [] }; export const defaultState: OverallState = { diff --git a/src/commons/languageDirectory/provider.ts b/src/commons/languageDirectory/provider.ts deleted file mode 100644 index af4886ed0e..0000000000 --- a/src/commons/languageDirectory/provider.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { IEvaluatorDefinition, ILanguageDefinition } from '@sourceacademy/language-directory'; -import { getEvaluatorDefinition as getEvaluatorDefinitionFromDirectory, languageMap, languages } from '@sourceacademy/language-directory'; - -export interface LanguageDirectoryProvider { - getLanguages(): Promise; - getLanguageById(languageId: string): Promise; - getEvaluatorDefinition( - languageId: string, - evaluatorId: string - ): Promise; -} - -export class StaticLanguageDirectoryProvider implements LanguageDirectoryProvider { - async getLanguages(): Promise { - return languages; - } - - async getLanguageById(languageId: string): Promise { - return languageMap.get(languageId); - } - - async getEvaluatorDefinition( - languageId: string, - evaluatorId: string - ): Promise { - const language = languageMap.get(languageId); - if (!language) return undefined; - return getEvaluatorDefinitionFromDirectory(language, evaluatorId); - } -} - -export const staticLanguageDirectoryProvider = new StaticLanguageDirectoryProvider(); - -export type { ILanguageDefinition, IEvaluatorDefinition }; - - diff --git a/src/commons/sagas/LanguageDirectorySaga.ts b/src/commons/sagas/LanguageDirectorySaga.ts index 4f55537492..bd95ea6c99 100644 --- a/src/commons/sagas/LanguageDirectorySaga.ts +++ b/src/commons/sagas/LanguageDirectorySaga.ts @@ -1,17 +1,22 @@ -import type { SagaIterator } from 'redux-saga'; -import { call, put, select, takeEvery } from 'redux-saga/effects'; +import { call, put, select } from 'redux-saga/effects'; import type { OverallState } from 'src/commons/application/ApplicationTypes'; -import { staticLanguageDirectoryProvider } from 'src/commons/languageDirectory/provider'; +import { staticLanguageDirectoryProvider } from 'src/features/languageDirectory/LanguageDirectoryTypes'; -import Actions from '../../features/languageDirectory/LanguageDirectoryActions'; +import LanguageDirectoryActions from '../../features/languageDirectory/LanguageDirectoryActions'; +import { combineSagaHandlers } from '../redux/utils'; +import { actions } from '../utils/ActionsHelper'; -function* resolveDefaultEvaluatorSaga(action: ReturnType): SagaIterator { - try { +const LanguageDirectorySaga = combineSagaHandlers({ + [LanguageDirectoryActions.fetchLanguages.type]: function* () { + const langs = yield call(staticLanguageDirectoryProvider.getLanguages.bind(staticLanguageDirectoryProvider)); + yield put(actions.setLanguages(langs)); + }, + [LanguageDirectoryActions.setSelectedLanguage.type]: function* (action) { const { payload: { languageId, evaluatorId } } = action; if (evaluatorId) return; // already explicitly set - const language = yield call(staticLanguageDirectoryProvider.getLanguageById, languageId); + const language = yield call(staticLanguageDirectoryProvider.getLanguageById.bind(staticLanguageDirectoryProvider), languageId); if (!language) return; const defaultEvaluatorId: string | null = language.evaluators.length > 0 ? language.evaluators[0].id : null; if (!defaultEvaluatorId) return; @@ -20,14 +25,10 @@ function* resolveDefaultEvaluatorSaga(action: ReturnType s.languageDirectory.selectedLanguageId ); if (currentLanguageId !== languageId) return; - yield put(Actions.setSelectedEvaluator(defaultEvaluatorId)); - } catch { - // swallow + yield put(actions.setSelectedEvaluator(defaultEvaluatorId)); } -} +}); -export default function* LanguageDirectorySaga(): SagaIterator { - yield takeEvery(Actions.setSelectedLanguage.type, resolveDefaultEvaluatorSaga); -} +export default LanguageDirectorySaga; diff --git a/src/commons/utils/ActionsHelper.ts b/src/commons/utils/ActionsHelper.ts index 23d5412144..f2114dd5f6 100644 --- a/src/commons/utils/ActionsHelper.ts +++ b/src/commons/utils/ActionsHelper.ts @@ -10,6 +10,7 @@ import AchievementActions from '../../features/achievement/AchievementActions'; import DashboardActions from '../../features/dashboard/DashboardActions'; import GitHubActions from '../../features/github/GitHubActions'; import GroundControlActions from '../../features/groundControl/GroundControlActions'; +import LanguageDirectoryActions from '../../features/languageDirectory/LanguageDirectoryActions'; import LeaderboardActions from '../../features/leaderboard/LeaderboardActions'; import PersistenceActions from '../../features/persistence/PersistenceActions'; import PlaygroundActions from '../../features/playground/PlaygroundActions'; @@ -45,7 +46,8 @@ export const actions = { ...SideContentActions, ...VscodeActions, ...SideContentActions, - ...FeatureFlagsActions + ...FeatureFlagsActions, + ...LanguageDirectoryActions }; export type SourceActionType = ActionType; diff --git a/src/features/languageDirectory/LanguageDirectoryActions.ts b/src/features/languageDirectory/LanguageDirectoryActions.ts index 99f521ba3a..ed17ef24d5 100644 --- a/src/features/languageDirectory/LanguageDirectoryActions.ts +++ b/src/features/languageDirectory/LanguageDirectoryActions.ts @@ -1,10 +1,14 @@ import { createActions } from 'src/commons/redux/utils'; -export default createActions('conductor/languageDirectory', { +const LanguageDirectoryActions = createActions('conductor/languageDirectory', { + /** Fetch languages (saga) */ + fetchLanguages: null, + /** Set languages list */ + setLanguages: (languages: any[]) => ({ languages }), /** Set selected language; evaluatorId optional (defaults to first available) */ setSelectedLanguage: (languageId: string, evaluatorId?: string) => ({ languageId, evaluatorId }), /** Set selected evaluator explicitly */ setSelectedEvaluator: (evaluatorId: string) => ({ evaluatorId }) }); - +export default LanguageDirectoryActions; \ No newline at end of file diff --git a/src/features/languageDirectory/LanguageDirectoryReducer.ts b/src/features/languageDirectory/LanguageDirectoryReducer.ts index 6670f1daa2..0cb2ce037e 100644 --- a/src/features/languageDirectory/LanguageDirectoryReducer.ts +++ b/src/features/languageDirectory/LanguageDirectoryReducer.ts @@ -1,14 +1,22 @@ import { createReducer, type Reducer } from '@reduxjs/toolkit'; -// Note: resolver handled in saga; reducer remains synchronous +import { defaultLanguageDirectory } from 'src/commons/application/ApplicationTypes'; import type { SourceActionType } from 'src/commons/utils/ActionsHelper'; import Actions from './LanguageDirectoryActions'; import type { LanguageDirectoryState } from './LanguageDirectoryTypes'; -import { defaultLanguageDirectoryState } from './LanguageDirectoryTypes'; export const LanguageDirectoryReducer: Reducer = - createReducer(defaultLanguageDirectoryState, builder => { + createReducer(defaultLanguageDirectory, builder => { builder + .addCase(Actions.setLanguages, (state, action) => { + state.languages = action.payload.languages as any; + if (state.selectedLanguageId === null && state.languages.length > 0) { + state.selectedLanguageId = state.languages[0].id; + } + if (state.selectedEvaluatorId === null && state.languages.length > 0) { + state.selectedEvaluatorId = state.languages[0].evaluators[0].id; + } + }) .addCase(Actions.setSelectedLanguage, (state, action) => { const { languageId, evaluatorId } = action.payload; state.selectedLanguageId = languageId; @@ -18,5 +26,3 @@ export const LanguageDirectoryReducer: Reducer; + getLanguageById(languageId: string): Promise; + getEvaluatorDefinition( + languageId: string, + evaluatorId: string + ): Promise; +} + +export class StaticLanguageDirectoryProvider implements LanguageDirectoryProvider { + async getLanguages(): Promise { + return languages; + } + + async getLanguageById(languageId: string): Promise { + return languageMap.get(languageId); + } + + async getEvaluatorDefinition( + languageId: string, + evaluatorId: string + ): Promise { + const language = languageMap.get(languageId); + if (!language) return undefined; + return getEvaluatorDefinition(language, evaluatorId); + } +} + +export const staticLanguageDirectoryProvider = new StaticLanguageDirectoryProvider(); + +export type { ILanguageDefinition, IEvaluatorDefinition }; From b8ad43ff855f57f2a4cdaf5d9480bfda80be7e32 Mon Sep 17 00:00:00 2001 From: loyaltypollution <65063925+loyaltypollution@users.noreply.github.com> Date: Sat, 16 Aug 2025 14:54:24 +0000 Subject: [PATCH 09/16] Update ChapterSelect for language directory --- .../controlBar/ControlBarChapterSelect.tsx | 133 ++++++++---------- .../LegacyControlBarChapterSelect.tsx | 111 +++++++++++++++ src/commons/sagas/LanguageDirectorySaga.ts | 34 +++++ .../LanguageDirectoryReducer.ts | 6 - 4 files changed, 206 insertions(+), 78 deletions(-) create mode 100644 src/commons/controlBar/LegacyControlBarChapterSelect.tsx diff --git a/src/commons/controlBar/ControlBarChapterSelect.tsx b/src/commons/controlBar/ControlBarChapterSelect.tsx index c35be486ca..c5dc71d985 100644 --- a/src/commons/controlBar/ControlBarChapterSelect.tsx +++ b/src/commons/controlBar/ControlBarChapterSelect.tsx @@ -1,22 +1,17 @@ -import { Button, Menu, MenuItem, Tooltip } from '@blueprintjs/core'; +import { Button, Menu, MenuItem } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { ItemListRenderer, ItemRenderer, Select } from '@blueprintjs/select'; import { Chapter, Variant } from 'js-slang/dist/types'; -import React from 'react'; +import React, { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; -import { - fullJSLanguage, - fullTSLanguage, - htmlLanguage, - javaLanguages, - pyLanguages, - SALanguage, - schemeLanguages, - sourceLanguages, - styliseSublanguage -} from '../application/ApplicationTypes'; -import Constants from '../utils/Constants'; +import { flagLanguageDirectoryEnable } from '../../features/languageDirectory/flagLanguageDirectory'; +import LanguageDirectoryActions from '../../features/languageDirectory/LanguageDirectoryActions'; +import type { IEvaluatorDefinition } from '../../features/languageDirectory/LanguageDirectoryTypes'; +import { SALanguage } from '../application/ApplicationTypes'; +import { useFeature } from '../featureFlags/useFeature'; import { useTypedSelector } from '../utils/Hooks'; +import { LegacyControlBarChapterSelect } from './LegacyControlBarChapterSelect'; type ControlBarChapterSelectProps = DispatchProps & StateProps; @@ -31,46 +26,6 @@ type StateProps = { disabled?: boolean; }; -const chapterListRenderer: ItemListRenderer = ({ - itemsParentRef, - renderItem, - items -}) => { - const defaultChoices = items.filter(({ variant }) => variant === Variant.DEFAULT); - const variantChoices = items.filter(({ variant }) => variant !== Variant.DEFAULT); - - return ( - - {defaultChoices.map(renderItem)} - {variantChoices.length > 0 && ( - - {variantChoices.map(renderItem)} - - )} - - ); -}; - -const chapterRenderer: (isFolderModeEnabled: boolean) => ItemRenderer = - (isFolderModeEnabled: boolean) => - (lang, { handleClick }) => { - const isDisabled = isFolderModeEnabled && lang.chapter === Chapter.SOURCE_1; - const tooltipContent = isDisabled - ? 'Folder mode makes use of lists which are not available in Source 1. To switch to Source 1, disable Folder mode.' - : undefined; - return ( - - - - ); - }; - -const ChapterSelectComponent = Select.ofType(); - export const ControlBarChapterSelect: React.FC = ({ isFolderModeEnabled, sourceChapter, @@ -78,34 +33,68 @@ export const ControlBarChapterSelect: React.FC = ( handleChapterSelect = () => {}, disabled = false }) => { - const selectedLang = useTypedSelector(store => store.playground.languageConfig.mainLanguage); + const dispatch = useDispatch(); + const directoryEnabled = useFeature(flagLanguageDirectoryEnable); + const selectedLanguageId = useTypedSelector(s => s.languageDirectory.selectedLanguageId); + const selectedEvaluatorId = useTypedSelector(s => s.languageDirectory.selectedEvaluatorId); + const dirLanguages = useTypedSelector(s => s.languageDirectory.languages); + + useEffect(() => { + if (directoryEnabled && dirLanguages.length === 0) { + dispatch(LanguageDirectoryActions.fetchLanguages()); + } + }, [directoryEnabled, dirLanguages.length, dispatch]); - const choices = [ - ...sourceLanguages, - // Full JS/TS version uses eval(), which is a huge security risk, so we only enable - // for public deployments. HTML, while sandboxed, is treated the same way to be safe. - // See https://github.com/source-academy/frontend/pull/2460#issuecomment-1528759912 - ...(Constants.playgroundOnly ? [fullJSLanguage, fullTSLanguage, htmlLanguage] : []), - ...schemeLanguages, - ...pyLanguages, - ...javaLanguages - ]; + if (!directoryEnabled) { + return ; + } + + const EvaluatorSelectComponent = Select.ofType(); + + const currentLanguage = dirLanguages.find(l => l.id === selectedLanguageId); + const evaluators = currentLanguage?.evaluators ?? []; + const selectedEvaluator = evaluators.find(e => e.id === selectedEvaluatorId); + + const evaluatorListRenderer: ItemListRenderer = ({ + itemsParentRef, + renderItem, + items + }) => ( + + {items.map(renderItem)} + + ); + + const evaluatorRenderer: ItemRenderer = (evaluator, { handleClick }) => ( + + ); + + const onSelectEvaluator = (evaluator: IEvaluatorDefinition) => { + dispatch(LanguageDirectoryActions.setSelectedEvaluator(evaluator.id)); + }; return ( - mainLanguage === selectedLang)} - onItemSelect={handleChapterSelect} - itemRenderer={chapterRenderer(isFolderModeEnabled)} - itemListRenderer={chapterListRenderer} +