Skip to content

Commit b8b91e0

Browse files
author
k.golikov
committed
Json-To-TypeScript: add settings
1 parent 3d8b923 commit b8b91e0

File tree

10 files changed

+296
-52
lines changed

10 files changed

+296
-52
lines changed

src/hooks/useChangeState.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Dispatch, SetStateAction, useCallback } from 'react';
2+
3+
const useChangeState = <S extends object>(setState: Dispatch<SetStateAction<S>>) => {
4+
return useCallback(<K extends keyof S>(key: K, value: S[K]) => {
5+
setState((state) => ({
6+
...state,
7+
[key]: value
8+
}));
9+
}, []);
10+
};
11+
12+
export default useChangeState;

src/hooks/useChangeStateHandler.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react';
2+
import call from '../utils/call';
3+
import { isObjectLike } from 'lodash';
4+
import useChangeState from './useChangeState';
5+
6+
const isChangeEvent = (value: any): value is ChangeEvent<unknown> => {
7+
return isObjectLike(value) && 'target' in value;
8+
};
9+
10+
const useChangeStateHandler = <S extends object>(setState: Dispatch<SetStateAction<S>>) => {
11+
const changeState = useChangeState(setState);
12+
13+
return useCallback(
14+
<K extends keyof S, T extends S[K]>(key: K) => {
15+
return (value: ChangeEvent<{ value: T }> | T) => {
16+
const actualValue = call(() => {
17+
if (isChangeEvent(value)) {
18+
const event = value as ChangeEvent<{ value: T }>;
19+
return event.target.value;
20+
}
21+
22+
return value;
23+
});
24+
25+
changeState(key, actualValue);
26+
};
27+
},
28+
[changeState]
29+
);
30+
};
31+
32+
export default useChangeStateHandler;

src/pages/codeFormatterPage/CodeFormatterPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { editor, languages } from 'monaco-editor';
99
import CopyButton from '../../components/copyButton/CopyButton';
1010
import formatCode from '../../utils/formatCode';
1111
import AppEditor from '../../components/appEditor/AppEditor';
12+
import getLocalStorageKey from '../../utils/getLocalStorageKey';
1213

1314
// interface FormattedLanguage {
1415
// prettierParser: prettier.BuiltInParserName,
@@ -50,7 +51,7 @@ const monacoOptions: editor.IStandaloneEditorConstructionOptions = {
5051

5152
const CodeFormatterPage: FunctionComponent = () => {
5253
const [selectedLanguage, setSelectedLanguage] = useLocalstorageState<MonacoLanguage>(
53-
'mrgrd56:code-formatter/selectedLanguage',
54+
getLocalStorageKey('code-formatter', 'selectedLanguage'),
5455
'typescript'
5556
);
5657
const [code, setCode] = useState<string>('');

src/pages/jsonToTypeScriptPage/JsonToTypeScriptPage.tsx

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
import React, { useState } from 'react';
1+
import React, { useCallback, useMemo, useState } from 'react';
22
import PageContainer from '../../components/pageContainer/PageContainer';
3-
import { Alert, Button, Col, Row, Tooltip } from 'antd';
3+
import { Alert, Button, Col, Popover, Row, Tooltip } from 'antd';
44
import styles from './JsonToTypeScriptPage.module.scss';
55
import classNames from 'classnames';
66
import AppEditor from '../../components/appEditor/AppEditor';
77
import { editor } from 'monaco-editor';
88
import { useDebouncedMemo } from '../../hooks/debouncedMemo';
99
import ExportType from './types/ExportType';
1010
import convertJsonToTypeScript from './utils/convertJsonToTypeScript';
11-
import { camelCase } from 'lodash';
12-
import pascalCase from '../../utils/pascalCase';
1311
import { SettingOutlined } from '@ant-design/icons';
1412
import getErrorMessage from '../../utils/getErrorMessage';
1513
import CopyButton from '../../components/copyButton/CopyButton';
14+
import { useLocalstorageState } from 'rooks';
15+
import getLocalStorageKey from '../../utils/getLocalStorageKey';
16+
import JsonToTypeScriptConversionSelectableOptions, {
17+
NameTransformer
18+
} from './types/JsonToTypeScriptConversionSelectableOptions';
19+
import JsonToTypeScriptConversionOptions from './types/JsonToTypeScriptConversionOptions';
20+
import { camelCase, snakeCase, upperCase } from 'lodash';
21+
import pascalCase from '../../utils/pascalCase';
22+
import JsonToTypeScriptSettings from './components/JsonToTypeScriptSettings';
1623

1724
const jsonEditorOptions: editor.IStandaloneEditorConstructionOptions = {
1825
minimap: { enabled: false }
@@ -23,27 +30,60 @@ const typescriptEditorOptions: editor.IStandaloneEditorConstructionOptions = {
2330
minimap: { enabled: false }
2431
};
2532

33+
const defaultSelectableConversionOptions: JsonToTypeScriptConversionSelectableOptions = {
34+
rootTypeName: 'Root',
35+
exportType: ExportType.ES_MODULE,
36+
isReversedOrder: true,
37+
typeNameTransformer: NameTransformer.PASCAL_CASE,
38+
fieldNameTransformer: NameTransformer.NONE
39+
};
40+
41+
const nameTransformers: Readonly<Record<NameTransformer, (name: string) => string>> = {
42+
[NameTransformer.NONE]: (name) => name,
43+
[NameTransformer.CAMEL_CASE]: camelCase,
44+
[NameTransformer.PASCAL_CASE]: pascalCase,
45+
[NameTransformer.SNAKE_CASE]: snakeCase,
46+
[NameTransformer.SCREAMING_SNAKE_CASE]: (name) => upperCase(snakeCase(name))
47+
};
48+
49+
const getConversionOptions = (
50+
selectableConversionOptions: JsonToTypeScriptConversionSelectableOptions
51+
): JsonToTypeScriptConversionOptions => {
52+
const { fieldNameTransformer, typeNameTransformer, ...restOptions } = selectableConversionOptions;
53+
54+
return {
55+
...restOptions,
56+
fieldNameTransformer: nameTransformers[fieldNameTransformer],
57+
typeNameTransformer: nameTransformers[typeNameTransformer]
58+
};
59+
};
60+
2661
const JsonToTypeScriptPage = () => {
2762
const [json, setJson] = useState<string>('');
28-
2963
const [error, setError] = useState<string>();
64+
const [isSettingsTooltipVisible, setIsSettingsTooltipVisible] = useState<boolean>(false);
65+
const [isSettingsVisible, setIsSettingsVisible] = useState<boolean>(false);
66+
67+
const [selectableConversionOptions, setSelectableConversionOptions] =
68+
useLocalstorageState<JsonToTypeScriptConversionSelectableOptions>(
69+
getLocalStorageKey('json-to-typescript', 'conversionOptions'),
70+
defaultSelectableConversionOptions
71+
);
72+
73+
const conversionOptions = useMemo(() => {
74+
return getConversionOptions(selectableConversionOptions);
75+
}, [selectableConversionOptions]);
3076

3177
const typeScript = useDebouncedMemo(
32-
{ json },
33-
({ json }, noResult) => {
78+
{ json, conversionOptions },
79+
({ json, conversionOptions }, noResult) => {
3480
if (!json?.trim()) {
3581
setError(undefined);
3682
return '';
3783
}
3884

3985
try {
40-
const result = convertJsonToTypeScript(json, {
41-
rootTypeName: 'Root',
42-
exportType: ExportType.ES_MODULE,
43-
isReversedOrder: true,
44-
typeNameTransformer: pascalCase,
45-
fieldNameTransformer: camelCase
46-
});
86+
const result = convertJsonToTypeScript(json, conversionOptions);
4787

4888
setError(undefined);
4989

@@ -54,19 +94,52 @@ const JsonToTypeScriptPage = () => {
5494
return noResult;
5595
}
5696
},
57-
[json],
97+
[json, conversionOptions],
5898
50
5999
);
60100

101+
const handleSettingsClick = useCallback(() => {
102+
setIsSettingsVisible((isVisible) => !isVisible);
103+
setIsSettingsTooltipVisible(false);
104+
}, []);
105+
106+
const handleSettingsTooltipVisibleChange = useCallback(
107+
(value: boolean) => {
108+
if (!isSettingsVisible) {
109+
setIsSettingsTooltipVisible(value);
110+
}
111+
},
112+
[isSettingsVisible]
113+
);
114+
61115
return (
62116
<PageContainer noPadding className={styles.pageContainer}>
63117
<Row className={styles.container}>
64118
<Col xs={12} className={classNames(styles.col, styles.colLeft)}>
65119
<div className={styles.colHeader}>
66120
<h3 className={styles.colTitle}>JSON</h3>
67-
<Tooltip title="Settings" placement="bottom">
68-
<Button type="text" icon={<SettingOutlined />} />
69-
</Tooltip>
121+
<Popover
122+
trigger="click"
123+
visible={isSettingsVisible}
124+
onVisibleChange={setIsSettingsVisible}
125+
content={
126+
<JsonToTypeScriptSettings
127+
options={selectableConversionOptions}
128+
setOptions={setSelectableConversionOptions}
129+
onClose={handleSettingsClick}
130+
/>
131+
}
132+
placement="bottomRight"
133+
>
134+
<Tooltip
135+
title="Settings"
136+
placement="bottomRight"
137+
visible={isSettingsVisible ? false : isSettingsTooltipVisible}
138+
onVisibleChange={handleSettingsTooltipVisibleChange}
139+
>
140+
<Button type="text" icon={<SettingOutlined />} onClick={handleSettingsClick} />
141+
</Tooltip>
142+
</Popover>
70143
</div>
71144
<AppEditor
72145
className={styles.editor}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
.formContainer {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 6px;
5+
6+
width: 280px;
7+
padding-bottom: 5px;
8+
}
9+
10+
.title {
11+
display: flex;
12+
flex-direction: row;
13+
justify-content: space-between;
14+
align-items: center;
15+
16+
margin-bottom: 0.25rem;
17+
18+
.closeButton {
19+
margin-top: -6px;
20+
}
21+
}
22+
23+
.formItem {
24+
display: flex;
25+
flex-direction: row;
26+
align-items: center;
27+
28+
.label {
29+
width: 120px;
30+
}
31+
32+
.input {
33+
flex: 1;
34+
width: 160px;
35+
max-width: 160px;
36+
}
37+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { Dispatch, FunctionComponent, SetStateAction } from 'react';
2+
import JsonToTypeScriptConversionSelectableOptions, {
3+
NameTransformer
4+
} from '../types/JsonToTypeScriptConversionSelectableOptions';
5+
import { Button, Col, Input, Select, Switch } from 'antd';
6+
import useChangeStateHandler from '../../../hooks/useChangeStateHandler';
7+
import styles from './JsonToTypeScriptSettings.module.scss';
8+
import ExportType from '../types/ExportType';
9+
import Text from 'antd/lib/typography/Text';
10+
import classNames from 'classnames';
11+
import { CloseOutlined } from '@ant-design/icons';
12+
13+
interface Props {
14+
options: JsonToTypeScriptConversionSelectableOptions;
15+
setOptions: Dispatch<SetStateAction<JsonToTypeScriptConversionSelectableOptions>>;
16+
onClose?: () => void;
17+
}
18+
19+
const JsonToTypeScriptSettings: FunctionComponent<Props> = ({ options, setOptions, onClose }) => {
20+
const handleOptionChange = useChangeStateHandler(setOptions);
21+
22+
return (
23+
<Col className={styles.formContainer}>
24+
<div className={styles.title}>
25+
<h3 className="mb-0">Settings</h3>
26+
{onClose && (
27+
<Button
28+
size="small"
29+
type="text"
30+
icon={<CloseOutlined />}
31+
className={styles.closeButton}
32+
onClick={onClose}
33+
/>
34+
)}
35+
</div>
36+
<label className={styles.formItem}>
37+
<span className={styles.label}>Root type name</span>
38+
<Input
39+
className={styles.input}
40+
value={options.rootTypeName}
41+
onChange={handleOptionChange('rootTypeName')}
42+
/>
43+
</label>
44+
<label className={styles.formItem}>
45+
<span className={styles.label}>Export type</span>
46+
<Select className={styles.input} value={options.exportType} onChange={handleOptionChange('exportType')}>
47+
<Select.Option key={ExportType.NONE}>None</Select.Option>
48+
<Select.Option key={ExportType.ES_MODULE}>ES Module</Select.Option>
49+
<Select.Option key={ExportType.COMMONJS}>
50+
<Text type="danger">CommonJS</Text>
51+
</Select.Option>
52+
</Select>
53+
</label>
54+
<label className={styles.formItem}>
55+
<span className={styles.label}>Field names</span>
56+
<Select
57+
className={styles.input}
58+
value={options.fieldNameTransformer}
59+
onChange={handleOptionChange('fieldNameTransformer')}
60+
>
61+
<Select.Option key={NameTransformer.NONE}>Not change</Select.Option>
62+
<Select.Option key={NameTransformer.CAMEL_CASE}>camelCase</Select.Option>
63+
<Select.Option key={NameTransformer.PASCAL_CASE}>PascalCase</Select.Option>
64+
<Select.Option key={NameTransformer.SNAKE_CASE}>snake_case</Select.Option>
65+
<Select.Option key={NameTransformer.SCREAMING_SNAKE_CASE}>SCREAMING_SNAKE_CASE</Select.Option>
66+
</Select>
67+
</label>
68+
<label className={styles.formItem}>
69+
<span className={styles.label}>Type names</span>
70+
<Select
71+
className={styles.input}
72+
value={options.typeNameTransformer}
73+
onChange={handleOptionChange('typeNameTransformer')}
74+
>
75+
<Select.Option key={NameTransformer.NONE}>Not change</Select.Option>
76+
<Select.Option key={NameTransformer.CAMEL_CASE}>camelCase</Select.Option>
77+
<Select.Option key={NameTransformer.PASCAL_CASE}>PascalCase</Select.Option>
78+
<Select.Option key={NameTransformer.SNAKE_CASE}>snake_case</Select.Option>
79+
<Select.Option key={NameTransformer.SCREAMING_SNAKE_CASE}>SCREAMING_SNAKE_CASE</Select.Option>
80+
</Select>
81+
</label>
82+
<label className={classNames('mt-1', styles.formItem)}>
83+
<Switch checked={options.isReversedOrder} onChange={handleOptionChange('isReversedOrder')} />
84+
<span className="ms-3">Reverse declarations</span>
85+
</label>
86+
</Col>
87+
);
88+
};
89+
90+
export default JsonToTypeScriptSettings;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import ExportType from './ExportType';
2+
3+
export enum NameTransformer {
4+
NONE = 'NONE',
5+
CAMEL_CASE = 'CAMEL_CASE',
6+
PASCAL_CASE = 'PASCAL_CASE',
7+
SNAKE_CASE = 'SNAKE_CASE',
8+
SCREAMING_SNAKE_CASE = 'SCREAMING_SNAKE_CASE'
9+
}
10+
11+
interface JsonToTypeScriptConversionSelectableOptions {
12+
exportType: ExportType;
13+
isReversedOrder: boolean;
14+
fieldNameTransformer: NameTransformer;
15+
typeNameTransformer: NameTransformer;
16+
rootTypeName: string;
17+
}
18+
19+
export default JsonToTypeScriptConversionSelectableOptions;

src/pages/jsonToTypeScriptPage/types/typescript.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ const stringifyTypeName = (name: string) => {
165165

166166
let result = name;
167167

168-
if (/^\d$/.test(name[0])) {
168+
if (!result?.trim()) {
169+
result = 'Type';
170+
}
171+
172+
if (/^\d$/.test(result[0])) {
169173
result = 'N' + result;
170174
}
171175

0 commit comments

Comments
 (0)