Skip to content

chore(website): [playground] inline visual editor instead of showing modal #6813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 3, 2023
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ module.exports = {
'import/no-default-export': 'off',
'react/jsx-no-target-blank': 'off',
'react/no-unescaped-entities': 'off',
'react-hooks/exhaustive-deps': 'off', // TODO: enable it later
'react-hooks/exhaustive-deps': 'warn', // TODO: enable it later
},
settings: {
react: {
Expand Down
7 changes: 6 additions & 1 deletion packages/website/src/components/Playground.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@
}

.tabCode {
height: calc(100% - 32px);
height: calc(100% - 41px);
}

.hidden {
display: none;
visibility: hidden;
}

@media only screen and (max-width: 996px) {
Expand Down
75 changes: 42 additions & 33 deletions packages/website/src/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ import Loader from './layout/Loader';
import OptionsSelector from './OptionsSelector';
import styles from './Playground.module.css';
import ConditionalSplitPane from './SplitPane/ConditionalSplitPane';
import type {
ConfigModel,
ErrorGroup,
RuleDetails,
SelectedRange,
TabType,
} from './types';
import type { ErrorGroup, RuleDetails, SelectedRange, TabType } from './types';

function Playground(): JSX.Element {
const [state, setState] = useHashState({
Expand All @@ -47,19 +41,10 @@ function Playground(): JSX.Element {
const [selectedRange, setSelectedRange] = useState<SelectedRange>();
const [position, setPosition] = useState<number>();
const [activeTab, setTab] = useState<TabType>('code');
const [showModal, setShowModal] = useState<TabType | false>(false);
const [esQueryFilter, setEsQueryFilter] = useState<ESQuery.Selector>();
const [esQueryError, setEsQueryError] = useState<Error>();

const updateModal = useCallback(
(config?: Partial<ConfigModel>) => {
if (config) {
setState(config);
}
setShowModal(false);
},
[setState],
);
const [visualEslintRc, setVisualEslintRc] = useState(false);
const [visualTSConfig, setVisualTSConfig] = useState(false);

const onLoaded = useCallback(
(ruleNames: RuleDetails[], tsVersions: readonly string[]): void => {
Expand All @@ -70,6 +55,22 @@ function Playground(): JSX.Element {
[],
);

const activeVisualEditor = !isLoading
? visualEslintRc && activeTab === 'eslintrc'
? 'eslintrc'
: visualTSConfig && activeTab === 'tsconfig'
? 'tsconfig'
: undefined
: undefined;

const onVisualEditor = useCallback((tab: TabType): void => {
if (tab === 'tsconfig') {
setVisualTSConfig(val => !val);
} else if (tab === 'eslintrc') {
setVisualEslintRc(val => !val);
}
}, []);

const astToShow =
state.showAST === 'ts'
? tsAst
Expand All @@ -81,19 +82,6 @@ function Playground(): JSX.Element {

return (
<div className={styles.codeContainer}>
{ruleNames.length > 0 && (
<ConfigEslint
isOpen={showModal === 'eslintrc'}
ruleOptions={ruleNames}
config={state.eslintrc}
onClose={updateModal}
/>
)}
<ConfigTypeScript
isOpen={showModal === 'tsconfig'}
config={state.tsconfig}
onClose={updateModal}
/>
<div className={styles.codeBlocks}>
<ConditionalSplitPane
split="vertical"
Expand Down Expand Up @@ -124,9 +112,30 @@ function Playground(): JSX.Element {
active={activeTab}
change={setTab}
showVisualEditor={activeTab !== 'code'}
showModal={(): void => setShowModal(activeTab)}
showModal={onVisualEditor}
/>
<div className={styles.tabCode}>
{(activeVisualEditor === 'eslintrc' && (
<ConfigEslint
className={styles.tabCode}
ruleOptions={ruleNames}
config={state.eslintrc}
onChange={setState}
/>
)) ||
(activeVisualEditor === 'tsconfig' && (
<ConfigTypeScript
className={styles.tabCode}
config={state.tsconfig}
onChange={setState}
/>
))}
<div
key="monacoEditor"
className={clsx(
styles.tabCode,
!!activeVisualEditor && styles.hidden,
)}
>
<EditorEmbed />
</div>
<LoadingEditor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
background: var(--ifm-color-emphasis-100);
}

.searchResult:nth-child(even):hover,
.searchResult:hover {
background: var(--ifm-color-emphasis-200);
}
Expand All @@ -57,6 +58,9 @@
column-gap: 0.5rem;
margin-bottom: 0.5rem;
justify-content: flex-end;
position: sticky;
top: 0;
left: 0;
}

.textarea {
Expand Down
209 changes: 88 additions & 121 deletions packages/website/src/components/config/ConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import Dropdown from '@site/src/components/inputs/Dropdown';
import Modal from '@site/src/components/layout/Modal';
import clsx from 'clsx';
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';

import useFocus from '../hooks/useFocus';
import Checkbox from '../inputs/Checkbox';
import Dropdown from '../inputs/Dropdown';
import Text from '../inputs/Text';
import styles from './ConfigEditor.module.css';

Expand All @@ -26,50 +24,8 @@ export type ConfigEditorValues = Record<string, unknown>;
export interface ConfigEditorProps {
readonly options: ConfigOptionsType[];
readonly values: ConfigEditorValues;
readonly isOpen: boolean;
readonly header: string;
readonly onClose: (config: ConfigEditorValues) => void;
}

function reducerObject(
state: ConfigEditorValues,
action:
| { type: 'init'; config?: ConfigEditorValues }
| {
type: 'set';
name: string;
value: unknown;
}
| {
type: 'toggle';
checked: boolean;
default: unknown[] | undefined;
name: string;
},
): ConfigEditorValues {
switch (action.type) {
case 'init': {
return action.config ?? {};
}
case 'set': {
const newState = { ...state };
if (action.value === '') {
delete newState[action.name];
} else {
newState[action.name] = action.value;
}
return newState;
}
case 'toggle': {
const newState = { ...state };
if (action.checked) {
newState[action.name] = action.default ? action.default[0] : true;
} else if (action.name in newState) {
delete newState[action.name];
}
return newState;
}
}
readonly onChange: (config: ConfigEditorValues) => void;
readonly className?: string;
}

function filterConfig(
Expand All @@ -88,93 +44,104 @@ function isDefault(value: unknown, defaults?: unknown[]): boolean {
return defaults ? defaults.includes(value) : value === true;
}

function ConfigEditor(props: ConfigEditorProps): JSX.Element {
const { onClose: onCloseProps, isOpen, values } = props;
const [filter, setFilter] = useState<string>('');
const [config, setConfig] = useReducer(reducerObject, {});
const [filterInput, setFilterFocus] = useFocus<HTMLInputElement>();
interface ConfigEditorFieldProps {
readonly item: ConfigOptionsField;
readonly value: unknown;
readonly onChange: (name: string, value: unknown) => void;
}

function ConfigEditorField({
item,
value,
onChange,
}: ConfigEditorFieldProps): JSX.Element {
return (
<label className={styles.searchResult}>
<span className={styles.searchResultDescription}>
<span className={styles.searchResultName}>{item.key}</span>
{item.label && <br />}
{item.label && <span> {item.label}</span>}
</span>
{(item.type === 'boolean' && (
<Checkbox
name={`config_${item.key}`}
value={item.key}
indeterminate={Boolean(value) && !isDefault(value, item.defaults)}
checked={Boolean(value)}
onChange={(checked): void =>
onChange(item.key, checked ? item.defaults?.[0] ?? true : undefined)
}
/>
)) ||
(item.type === 'string' && item.enum && (
<Dropdown
name={`config_${item.key}`}
value={String(value)}
options={item.enum}
onChange={(value): void => onChange(item.key, value)}
/>
))}
</label>
);
}

const onClose = useCallback(() => {
onCloseProps(config);
}, [onCloseProps, config]);
function ConfigEditor({
onChange: onChangeProp,
values,
options,
className,
}: ConfigEditorProps): JSX.Element {
const [filter, setFilter] = useState<string>('');

useEffect(() => {
setConfig({ type: 'init', config: values });
}, [values]);
const filteredOptions = useMemo(() => {
return filterConfig(options, filter);
}, [options, filter]);

useEffect(() => {
if (isOpen) {
setFilterFocus();
}
}, [isOpen, setFilterFocus]);
const onChange = useCallback(
(name: string, value: unknown): void => {
const newConfig = { ...values };
if (value === '' || value == null) {
delete newConfig[name];
} else {
newConfig[name] = value;
}
onChangeProp(newConfig);
},
[values, onChangeProp],
);

return (
<Modal header={props.header} isOpen={isOpen} onClose={onClose}>
<div
className={clsx(
'thin-scrollbar',
styles.searchResultContainer,
className,
)}
>
<div className={styles.searchBar}>
<Text
ref={filterInput}
type="search"
name="config-filter"
value={filter}
onChange={setFilter}
/>
</div>
<div className={clsx('thin-scrollbar', styles.searchResultContainer)}>
{filterConfig(props.options, filter).map(group => (
<div key={group.heading}>
<h3 className={styles.searchResultGroup}>{group.heading}</h3>
<div>
{group.fields.map(item => (
<label className={styles.searchResult} key={item.key}>
<span className={styles.searchResultDescription}>
<span className={styles.searchResultName}>{item.key}</span>
{item.label && (
<>
<br />
<span> {item.label}</span>
</>
)}
</span>
{item.type === 'boolean' && (
<Checkbox
name={`config_${item.key}`}
value={item.key}
indeterminate={
Boolean(config[item.key]) &&
!isDefault(config[item.key], item.defaults)
}
checked={Boolean(config[item.key])}
onChange={(checked, name): void =>
setConfig({
type: 'toggle',
checked,
default: item.defaults,
name,
})
}
/>
)}
{item.type === 'string' && item.enum && (
<Dropdown
name={`config_${item.key}`}
value={String(config[item.key])}
options={item.enum}
onChange={(value): void => {
setConfig({
type: 'set',
value,
name: item.key,
});
}}
/>
)}
</label>
))}
</div>
{filteredOptions.map(group => (
<div key={group.heading}>
<h3 className={styles.searchResultGroup}>{group.heading}</h3>
<div>
{group.fields.map(item => (
<ConfigEditorField
key={item.key}
item={item}
value={values[item.key]}
onChange={onChange}
/>
))}
</div>
))}
</div>
</Modal>
</div>
))}
</div>
);
}

Expand Down
Loading