Skip to content

Commit b8ad43f

Browse files
Update ChapterSelect for language directory
1 parent fd02fd7 commit b8ad43f

File tree

4 files changed

+206
-78
lines changed

4 files changed

+206
-78
lines changed
Lines changed: 61 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
1-
import { Button, Menu, MenuItem, Tooltip } from '@blueprintjs/core';
1+
import { Button, Menu, MenuItem } from '@blueprintjs/core';
22
import { IconNames } from '@blueprintjs/icons';
33
import { ItemListRenderer, ItemRenderer, Select } from '@blueprintjs/select';
44
import { Chapter, Variant } from 'js-slang/dist/types';
5-
import React from 'react';
5+
import React, { useEffect } from 'react';
6+
import { useDispatch } from 'react-redux';
67

7-
import {
8-
fullJSLanguage,
9-
fullTSLanguage,
10-
htmlLanguage,
11-
javaLanguages,
12-
pyLanguages,
13-
SALanguage,
14-
schemeLanguages,
15-
sourceLanguages,
16-
styliseSublanguage
17-
} from '../application/ApplicationTypes';
18-
import Constants from '../utils/Constants';
8+
import { flagLanguageDirectoryEnable } from '../../features/languageDirectory/flagLanguageDirectory';
9+
import LanguageDirectoryActions from '../../features/languageDirectory/LanguageDirectoryActions';
10+
import type { IEvaluatorDefinition } from '../../features/languageDirectory/LanguageDirectoryTypes';
11+
import { SALanguage } from '../application/ApplicationTypes';
12+
import { useFeature } from '../featureFlags/useFeature';
1913
import { useTypedSelector } from '../utils/Hooks';
14+
import { LegacyControlBarChapterSelect } from './LegacyControlBarChapterSelect';
2015

2116
type ControlBarChapterSelectProps = DispatchProps & StateProps;
2217

@@ -31,81 +26,75 @@ type StateProps = {
3126
disabled?: boolean;
3227
};
3328

34-
const chapterListRenderer: ItemListRenderer<SALanguage> = ({
35-
itemsParentRef,
36-
renderItem,
37-
items
38-
}) => {
39-
const defaultChoices = items.filter(({ variant }) => variant === Variant.DEFAULT);
40-
const variantChoices = items.filter(({ variant }) => variant !== Variant.DEFAULT);
41-
42-
return (
43-
<Menu ulRef={itemsParentRef} style={{ display: 'flex', flexDirection: 'column' }}>
44-
{defaultChoices.map(renderItem)}
45-
{variantChoices.length > 0 && (
46-
<MenuItem key="variant-menu" text="Variants" icon="cog">
47-
{variantChoices.map(renderItem)}
48-
</MenuItem>
49-
)}
50-
</Menu>
51-
);
52-
};
53-
54-
const chapterRenderer: (isFolderModeEnabled: boolean) => ItemRenderer<SALanguage> =
55-
(isFolderModeEnabled: boolean) =>
56-
(lang, { handleClick }) => {
57-
const isDisabled = isFolderModeEnabled && lang.chapter === Chapter.SOURCE_1;
58-
const tooltipContent = isDisabled
59-
? 'Folder mode makes use of lists which are not available in Source 1. To switch to Source 1, disable Folder mode.'
60-
: undefined;
61-
return (
62-
<Tooltip
63-
key={lang.displayName}
64-
content={tooltipContent}
65-
disabled={tooltipContent === undefined}
66-
>
67-
<MenuItem onClick={handleClick} text={lang.displayName} disabled={isDisabled} />
68-
</Tooltip>
69-
);
70-
};
71-
72-
const ChapterSelectComponent = Select.ofType<SALanguage>();
73-
7429
export const ControlBarChapterSelect: React.FC<ControlBarChapterSelectProps> = ({
7530
isFolderModeEnabled,
7631
sourceChapter,
7732
sourceVariant,
7833
handleChapterSelect = () => {},
7934
disabled = false
8035
}) => {
81-
const selectedLang = useTypedSelector(store => store.playground.languageConfig.mainLanguage);
36+
const dispatch = useDispatch();
37+
const directoryEnabled = useFeature(flagLanguageDirectoryEnable);
38+
const selectedLanguageId = useTypedSelector(s => s.languageDirectory.selectedLanguageId);
39+
const selectedEvaluatorId = useTypedSelector(s => s.languageDirectory.selectedEvaluatorId);
40+
const dirLanguages = useTypedSelector(s => s.languageDirectory.languages);
41+
42+
useEffect(() => {
43+
if (directoryEnabled && dirLanguages.length === 0) {
44+
dispatch(LanguageDirectoryActions.fetchLanguages());
45+
}
46+
}, [directoryEnabled, dirLanguages.length, dispatch]);
8247

83-
const choices = [
84-
...sourceLanguages,
85-
// Full JS/TS version uses eval(), which is a huge security risk, so we only enable
86-
// for public deployments. HTML, while sandboxed, is treated the same way to be safe.
87-
// See https://github.com/source-academy/frontend/pull/2460#issuecomment-1528759912
88-
...(Constants.playgroundOnly ? [fullJSLanguage, fullTSLanguage, htmlLanguage] : []),
89-
...schemeLanguages,
90-
...pyLanguages,
91-
...javaLanguages
92-
];
48+
if (!directoryEnabled) {
49+
return <LegacyControlBarChapterSelect
50+
isFolderModeEnabled={isFolderModeEnabled}
51+
sourceChapter={sourceChapter}
52+
sourceVariant={sourceVariant}
53+
handleChapterSelect={handleChapterSelect}
54+
disabled={disabled}
55+
/>;
56+
}
57+
58+
const EvaluatorSelectComponent = Select.ofType<IEvaluatorDefinition>();
59+
60+
const currentLanguage = dirLanguages.find(l => l.id === selectedLanguageId);
61+
const evaluators = currentLanguage?.evaluators ?? [];
62+
const selectedEvaluator = evaluators.find(e => e.id === selectedEvaluatorId);
63+
64+
const evaluatorListRenderer: ItemListRenderer<IEvaluatorDefinition> = ({
65+
itemsParentRef,
66+
renderItem,
67+
items
68+
}) => (
69+
<Menu ulRef={itemsParentRef} style={{ display: 'flex', flexDirection: 'column' }}>
70+
{items.map(renderItem)}
71+
</Menu>
72+
);
73+
74+
const evaluatorRenderer: ItemRenderer<IEvaluatorDefinition> = (evaluator, { handleClick }) => (
75+
<MenuItem key={evaluator.id} onClick={handleClick} text={evaluator.name} />
76+
);
77+
78+
const onSelectEvaluator = (evaluator: IEvaluatorDefinition) => {
79+
dispatch(LanguageDirectoryActions.setSelectedEvaluator(evaluator.id));
80+
};
9381

9482
return (
95-
<ChapterSelectComponent
96-
items={choices.filter(({ mainLanguage }) => mainLanguage === selectedLang)}
97-
onItemSelect={handleChapterSelect}
98-
itemRenderer={chapterRenderer(isFolderModeEnabled)}
99-
itemListRenderer={chapterListRenderer}
83+
<EvaluatorSelectComponent
84+
items={evaluators}
85+
onItemSelect={onSelectEvaluator}
86+
itemRenderer={evaluatorRenderer}
87+
itemListRenderer={evaluatorListRenderer}
10088
filterable={false}
10189
disabled={disabled}
10290
>
10391
<Button
10492
minimal
105-
text={styliseSublanguage(sourceChapter, sourceVariant)}
93+
text={selectedEvaluator ? selectedEvaluator.name : 'Select Evaluator'}
10694
rightIcon={disabled ? null : IconNames.DOUBLE_CARET_VERTICAL}
95+
data-testid="ControlBarEvaluatorSelect"
10796
disabled={disabled}
10897
/>
109-
</ChapterSelectComponent>
98+
</EvaluatorSelectComponent>
11099
);
111100
};
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Button, Menu, MenuItem, Tooltip } from '@blueprintjs/core';
2+
import { IconNames } from '@blueprintjs/icons';
3+
import { ItemListRenderer, ItemRenderer, Select } from '@blueprintjs/select';
4+
import { Chapter, Variant } from 'js-slang/dist/types';
5+
import React from 'react';
6+
7+
import {
8+
fullJSLanguage,
9+
fullTSLanguage,
10+
htmlLanguage,
11+
javaLanguages,
12+
pyLanguages,
13+
SALanguage,
14+
schemeLanguages,
15+
sourceLanguages,
16+
styliseSublanguage
17+
} from '../application/ApplicationTypes';
18+
import Constants from '../utils/Constants';
19+
import { useTypedSelector } from '../utils/Hooks';
20+
21+
type ControlBarChapterSelectProps = DispatchProps & StateProps;
22+
23+
type DispatchProps = {
24+
handleChapterSelect?: (i: SALanguage, e?: React.SyntheticEvent<HTMLElement>) => void;
25+
};
26+
27+
type StateProps = {
28+
isFolderModeEnabled: boolean;
29+
sourceChapter: Chapter;
30+
sourceVariant: Variant;
31+
disabled?: boolean;
32+
};
33+
34+
const chapterListRenderer: ItemListRenderer<SALanguage> = ({
35+
itemsParentRef,
36+
renderItem,
37+
items
38+
}) => {
39+
const defaultChoices = items.filter(({ variant }) => variant === Variant.DEFAULT);
40+
const variantChoices = items.filter(({ variant }) => variant !== Variant.DEFAULT);
41+
42+
return (
43+
<Menu ulRef={itemsParentRef} style={{ display: 'flex', flexDirection: 'column' }}>
44+
{defaultChoices.map(renderItem)}
45+
{variantChoices.length > 0 && (
46+
<MenuItem key="variant-menu" text="Variants" icon="cog">
47+
{variantChoices.map(renderItem)}
48+
</MenuItem>
49+
)}
50+
</Menu>
51+
);
52+
};
53+
54+
const chapterRenderer: (isFolderModeEnabled: boolean) => ItemRenderer<SALanguage> =
55+
(isFolderModeEnabled: boolean) =>
56+
(lang, { handleClick }) => {
57+
const isDisabled = isFolderModeEnabled && lang.chapter === Chapter.SOURCE_1;
58+
const tooltipContent = isDisabled
59+
? 'Folder mode makes use of lists which are not available in Source 1. To switch to Source 1, disable Folder mode.'
60+
: undefined;
61+
return (
62+
<Tooltip
63+
key={lang.displayName}
64+
content={tooltipContent}
65+
disabled={tooltipContent === undefined}
66+
>
67+
<MenuItem onClick={handleClick} text={lang.displayName} disabled={isDisabled} />
68+
</Tooltip>
69+
);
70+
};
71+
72+
const ChapterSelectComponent = Select.ofType<SALanguage>();
73+
74+
export const LegacyControlBarChapterSelect: React.FC<ControlBarChapterSelectProps> = ({
75+
isFolderModeEnabled,
76+
sourceChapter,
77+
sourceVariant,
78+
handleChapterSelect = () => {},
79+
disabled = false
80+
}) => {
81+
const selectedLang = useTypedSelector(store => store.playground.languageConfig.mainLanguage);
82+
83+
const choices = [
84+
...sourceLanguages,
85+
// Full JS/TS version uses eval(), which is a huge security risk, so we only enable
86+
// for public deployments. HTML, while sandboxed, is treated the same way to be safe.
87+
// See https://github.com/source-academy/frontend/pull/2460#issuecomment-1528759912
88+
...(Constants.playgroundOnly ? [fullJSLanguage, fullTSLanguage, htmlLanguage] : []),
89+
...schemeLanguages,
90+
...pyLanguages,
91+
...javaLanguages
92+
];
93+
94+
return (
95+
<ChapterSelectComponent
96+
items={choices.filter(({ mainLanguage }) => mainLanguage === selectedLang)}
97+
onItemSelect={handleChapterSelect}
98+
itemRenderer={chapterRenderer(isFolderModeEnabled)}
99+
itemListRenderer={chapterListRenderer}
100+
filterable={false}
101+
disabled={disabled}
102+
>
103+
<Button
104+
minimal
105+
text={styliseSublanguage(sourceChapter, sourceVariant)}
106+
rightIcon={disabled ? null : IconNames.DOUBLE_CARET_VERTICAL}
107+
disabled={disabled}
108+
/>
109+
</ChapterSelectComponent>
110+
);
111+
};

src/commons/sagas/LanguageDirectorySaga.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import { call, put, select } from 'redux-saga/effects';
22
import type { OverallState } from 'src/commons/application/ApplicationTypes';
3+
import { flagConductorEnable } from 'src/features/conductor/flagConductorEnable';
4+
import { flagConductorEvaluatorUrl } from 'src/features/conductor/flagConductorEvaluatorUrl';
35
import { staticLanguageDirectoryProvider } from 'src/features/languageDirectory/LanguageDirectoryTypes';
46

57
import LanguageDirectoryActions from '../../features/languageDirectory/LanguageDirectoryActions';
68
import { combineSagaHandlers } from '../redux/utils';
79
import { actions } from '../utils/ActionsHelper';
810

911
const LanguageDirectorySaga = combineSagaHandlers({
12+
[LanguageDirectoryActions.setLanguages.type]: function* () {
13+
const state = yield select(
14+
(s: OverallState) => s.languageDirectory
15+
);
16+
if (state.selectedLanguageId === null && state.languages.length > 0) {
17+
yield put(actions.setSelectedLanguage(state.languages[0].id));
18+
}
19+
if (state.selectedEvaluatorId === null && state.languages.length > 0) {
20+
yield put(actions.setSelectedEvaluator(state.languages[0].evaluators[0].id));
21+
}
22+
},
1023
[LanguageDirectoryActions.fetchLanguages.type]: function* () {
1124
const langs = yield call(staticLanguageDirectoryProvider.getLanguages.bind(staticLanguageDirectoryProvider));
1225
yield put(actions.setLanguages(langs));
@@ -26,6 +39,27 @@ const LanguageDirectorySaga = combineSagaHandlers({
2639
);
2740
if (currentLanguageId !== languageId) return;
2841
yield put(actions.setSelectedEvaluator(defaultEvaluatorId));
42+
},
43+
[LanguageDirectoryActions.setSelectedEvaluator.type]: function* (action) {
44+
const {
45+
payload: { evaluatorId }
46+
} = action;
47+
const selectedLanguageId: string | null = yield select(
48+
(s: OverallState) => s.languageDirectory.selectedLanguageId
49+
);
50+
if (!selectedLanguageId) return;
51+
const evaluator = yield call(
52+
staticLanguageDirectoryProvider.getEvaluatorDefinition.bind(
53+
staticLanguageDirectoryProvider
54+
),
55+
selectedLanguageId,
56+
evaluatorId
57+
);
58+
if (!evaluator) return;
59+
yield put(actions.setFlag({ featureFlag: flagConductorEnable, value: true }));
60+
yield put(
61+
actions.setFlag({ featureFlag: flagConductorEvaluatorUrl, value: evaluator.path })
62+
);
2963
}
3064
});
3165

src/features/languageDirectory/LanguageDirectoryReducer.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ export const LanguageDirectoryReducer: Reducer<LanguageDirectoryState, SourceAct
1010
builder
1111
.addCase(Actions.setLanguages, (state, action) => {
1212
state.languages = action.payload.languages as any;
13-
if (state.selectedLanguageId === null && state.languages.length > 0) {
14-
state.selectedLanguageId = state.languages[0].id;
15-
}
16-
if (state.selectedEvaluatorId === null && state.languages.length > 0) {
17-
state.selectedEvaluatorId = state.languages[0].evaluators[0].id;
18-
}
1913
})
2014
.addCase(Actions.setSelectedLanguage, (state, action) => {
2115
const { languageId, evaluatorId } = action.payload;

0 commit comments

Comments
 (0)