refactor: monorepo setup with lerna (#243)
This commit is contained in:
committed by
GitHub
parent
dac29fbf3c
commit
504d95c34f
224
packages/core/src/widgets/code/CodeControl.tsx
Normal file
224
packages/core/src/widgets/code/CodeControl.tsx
Normal file
@ -0,0 +1,224 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { loadLanguage } from '@uiw/codemirror-extensions-langs';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
|
||||
import Outline from '@staticcms/core/components/UI/Outline';
|
||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import transientOptions from '@staticcms/core/lib/util/transientOptions';
|
||||
import languages from './data/languages';
|
||||
import SettingsButton from './SettingsButton';
|
||||
import SettingsPane from './SettingsPane';
|
||||
|
||||
import type {
|
||||
CodeField,
|
||||
ProcessedCodeLanguage,
|
||||
WidgetControlProps,
|
||||
} from '@staticcms/core/interface';
|
||||
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const StyledCodeControlWrapper = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface StyledCodeControlContentProps {
|
||||
$collapsed: boolean;
|
||||
}
|
||||
|
||||
const StyledCodeControlContent = styled(
|
||||
'div',
|
||||
transientOptions,
|
||||
)<StyledCodeControlContentProps>(
|
||||
({ $collapsed }) => `
|
||||
display: block;
|
||||
width: 100%;
|
||||
${
|
||||
$collapsed
|
||||
? `
|
||||
display: none;
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
function valueToOption(val: string | { name: string; label?: string }): {
|
||||
value: string;
|
||||
label: string;
|
||||
} {
|
||||
if (typeof val === 'string') {
|
||||
return { value: val, label: val };
|
||||
}
|
||||
return { value: val.name, label: val.label || val.name };
|
||||
}
|
||||
|
||||
const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, CodeField>> = ({
|
||||
field,
|
||||
onChange,
|
||||
hasErrors,
|
||||
value,
|
||||
t,
|
||||
}) => {
|
||||
const keys = useMemo(() => {
|
||||
const defaults = {
|
||||
code: 'code',
|
||||
lang: 'lang',
|
||||
};
|
||||
|
||||
const keys = field.keys ?? {};
|
||||
return { ...defaults, ...keys };
|
||||
}, [field]);
|
||||
|
||||
const valueIsMap = useMemo(() => Boolean(!field.output_code_only), [field.output_code_only]);
|
||||
|
||||
const [internalValue, setInternalValue] = useState(value ?? '');
|
||||
const [lang, setLang] = useState<ProcessedCodeLanguage | null>(null);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const [hasFocus, setHasFocus] = useState(false);
|
||||
const handleFocus = useCallback(() => {
|
||||
setHasFocus(true);
|
||||
}, []);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setHasFocus(false);
|
||||
}, []);
|
||||
|
||||
const handleCollapseToggle = useCallback(() => {
|
||||
setCollapsed(!collapsed);
|
||||
}, [collapsed]);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(newValue: string | { [key: string]: string } | null | undefined) => {
|
||||
setInternalValue(newValue ?? '');
|
||||
onChange(newValue ?? '');
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (valueIsMap) {
|
||||
handleOnChange({
|
||||
lang: lang?.label ?? '',
|
||||
code: newValue,
|
||||
});
|
||||
}
|
||||
handleOnChange(newValue);
|
||||
},
|
||||
[handleOnChange, lang?.label, valueIsMap],
|
||||
);
|
||||
|
||||
const loadedLangExtension = useMemo(() => {
|
||||
if (!lang) {
|
||||
return null;
|
||||
}
|
||||
return loadLanguage(lang.codemirror_mode as LanguageName);
|
||||
}, [lang]);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
if (!loadedLangExtension) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [loadedLangExtension];
|
||||
}, [loadedLangExtension]);
|
||||
|
||||
const code = useMemo(() => {
|
||||
if (typeof internalValue === 'string') {
|
||||
return internalValue;
|
||||
}
|
||||
|
||||
return internalValue[keys.code];
|
||||
}, [internalValue, keys.code]);
|
||||
|
||||
const [settingsVisible, setSettingsVisible] = useState(false);
|
||||
const showSettings = useCallback(() => {
|
||||
setSettingsVisible(true);
|
||||
}, []);
|
||||
|
||||
const hideSettings = useCallback(() => {
|
||||
setSettingsVisible(false);
|
||||
}, []);
|
||||
|
||||
const uniqueId = useUUID();
|
||||
|
||||
// If `allow_language_selection` is not set, default to true. Otherwise, use its value.
|
||||
const allowLanguageSelection = useMemo(
|
||||
() => Boolean(field.allow_language_selection),
|
||||
[field.allow_language_selection],
|
||||
);
|
||||
|
||||
const availableLanguages = languages.map(language => valueToOption(language.label));
|
||||
|
||||
const handleSetLanguage = useCallback((langIdentifier: string) => {
|
||||
const language = languages.find(language => language.identifiers.includes(langIdentifier));
|
||||
if (language) {
|
||||
setLang(language);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let langIdentifier: string;
|
||||
if (typeof internalValue !== 'string') {
|
||||
langIdentifier = internalValue[keys.lang];
|
||||
} else {
|
||||
langIdentifier = internalValue;
|
||||
}
|
||||
|
||||
if (isEmpty(langIdentifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleSetLanguage(langIdentifier);
|
||||
}, [field.default_language, handleSetLanguage, internalValue, keys.lang, valueIsMap]);
|
||||
|
||||
return (
|
||||
<StyledCodeControlWrapper>
|
||||
{allowLanguageSelection ? (
|
||||
!settingsVisible ? (
|
||||
<SettingsButton onClick={showSettings} />
|
||||
) : (
|
||||
<SettingsPane
|
||||
hideSettings={hideSettings}
|
||||
uniqueId={uniqueId}
|
||||
languages={availableLanguages}
|
||||
language={valueToOption(lang?.label ?? '')}
|
||||
allowLanguageSelection={allowLanguageSelection}
|
||||
onChangeLanguage={handleSetLanguage}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
<ObjectWidgetTopBar
|
||||
key="file-control-top-bar"
|
||||
collapsed={collapsed}
|
||||
onCollapseToggle={handleCollapseToggle}
|
||||
heading={field.label ?? field.name}
|
||||
hasError={hasErrors}
|
||||
t={t}
|
||||
/>
|
||||
<StyledCodeControlContent $collapsed={collapsed}>
|
||||
<CodeMirror
|
||||
value={code}
|
||||
height="auto"
|
||||
minHeight="120px"
|
||||
width="100%"
|
||||
editable={true}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
extensions={extensions}
|
||||
/>
|
||||
</StyledCodeControlContent>
|
||||
<Outline active={hasFocus} hasError={hasErrors} />
|
||||
</StyledCodeControlWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeControl;
|
34
packages/core/src/widgets/code/CodePreview.tsx
Normal file
34
packages/core/src/widgets/code/CodePreview.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import isString from 'lodash/isString';
|
||||
import React from 'react';
|
||||
|
||||
import WidgetPreviewContainer from '@staticcms/core/components/UI/WidgetPreviewContainer';
|
||||
|
||||
import type { CodeField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { FC } from 'react';
|
||||
|
||||
function toValue(value: string | Record<string, string> | undefined | null, field: CodeField) {
|
||||
if (isString(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
return value[field.keys?.code ?? 'code'] ?? '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
const CodePreview: FC<WidgetPreviewProps<string | Record<string, string>, CodeField>> = ({
|
||||
value,
|
||||
field,
|
||||
}) => {
|
||||
return (
|
||||
<WidgetPreviewContainer>
|
||||
<pre>
|
||||
<code>{toValue(value, field)}</code>
|
||||
</pre>
|
||||
</WidgetPreviewContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodePreview;
|
36
packages/core/src/widgets/code/SettingsButton.tsx
Normal file
36
packages/core/src/widgets/code/SettingsButton.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
|
||||
import { zIndex } from '@staticcms/core/components/UI/styles';
|
||||
|
||||
import type { FC, MouseEvent } from 'react';
|
||||
|
||||
const StyledSettingsButton = styled(IconButton)`
|
||||
position: absolute;
|
||||
z-index: ${zIndex.zIndex100};
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
opacity: 0.8;
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
height: auto;
|
||||
color: #000;
|
||||
`;
|
||||
|
||||
export interface SettingsButtonProps {
|
||||
showClose?: boolean;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
const SettingsButton: FC<SettingsButtonProps> = ({ showClose = false, onClick }) => {
|
||||
return (
|
||||
<StyledSettingsButton onClick={onClick}>
|
||||
{showClose ? <CloseIcon /> : <SettingsIcon />}
|
||||
</StyledSettingsButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsButton;
|
131
packages/core/src/widgets/code/SettingsPane.tsx
Normal file
131
packages/core/src/widgets/code/SettingsPane.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import React from 'react';
|
||||
|
||||
import { shadows, zIndex } from '@staticcms/core/components/UI/styles';
|
||||
import SettingsButton from './SettingsButton';
|
||||
|
||||
import type { SelectChangeEvent } from '@mui/material/Select';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const SettingsPaneContainer = styled('div')`
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
bottom: 1px;
|
||||
right: 1px;
|
||||
width: 200px;
|
||||
z-index: ${zIndex.zIndex10};
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
${shadows.drop};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
const SettingsSectionTitle = styled('h3')`
|
||||
font-size: 14px;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface SettingsSelectProps {
|
||||
type: 'language';
|
||||
label: string;
|
||||
uniqueId: string;
|
||||
value: {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
options: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
onChange: (newValue: string) => void;
|
||||
}
|
||||
|
||||
const SettingsSelect: FC<SettingsSelectProps> = ({
|
||||
value,
|
||||
label,
|
||||
options,
|
||||
onChange,
|
||||
uniqueId,
|
||||
type,
|
||||
}) => {
|
||||
const handleChange = (event: SelectChangeEvent<string>) => {
|
||||
onChange(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id={`${uniqueId}-select-${type}-label`}>{label}</InputLabel>
|
||||
<Select
|
||||
labelId={`${uniqueId}-select-${type}-label`}
|
||||
id={`${uniqueId}-select-${type}`}
|
||||
value={value.value}
|
||||
label={label}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{options.map(({ label, value }) =>
|
||||
value ? (
|
||||
<MenuItem key={`${uniqueId}-select-${type}-option-${value}`} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
) : null,
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export interface SettingsPaneProps {
|
||||
hideSettings: () => void;
|
||||
uniqueId: string;
|
||||
languages: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
language: {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
allowLanguageSelection: boolean;
|
||||
onChangeLanguage: (lang: string) => void;
|
||||
}
|
||||
|
||||
const SettingsPane: FC<SettingsPaneProps> = ({
|
||||
hideSettings,
|
||||
uniqueId,
|
||||
languages,
|
||||
language,
|
||||
onChangeLanguage,
|
||||
}) => {
|
||||
return (
|
||||
<SettingsPaneContainer onKeyDown={e => isHotkey('esc', e) && hideSettings()}>
|
||||
<SettingsButton onClick={hideSettings} showClose={true} />
|
||||
<>
|
||||
<SettingsSectionTitle>Field Settings</SettingsSectionTitle>
|
||||
<SettingsSelect
|
||||
type="language"
|
||||
label="Language"
|
||||
uniqueId={uniqueId}
|
||||
value={language}
|
||||
options={languages}
|
||||
onChange={onChangeLanguage}
|
||||
/>
|
||||
</>
|
||||
</SettingsPaneContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPane;
|
6133
packages/core/src/widgets/code/data/languages-raw.yml
Normal file
6133
packages/core/src/widgets/code/data/languages-raw.yml
Normal file
File diff suppressed because it is too large
Load Diff
1508
packages/core/src/widgets/code/data/languages.ts
Normal file
1508
packages/core/src/widgets/code/data/languages.ts
Normal file
File diff suppressed because it is too large
Load Diff
47
packages/core/src/widgets/code/index.ts
Normal file
47
packages/core/src/widgets/code/index.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import controlComponent from './CodeControl';
|
||||
import previewComponent from './CodePreview';
|
||||
import schema from './schema';
|
||||
|
||||
import type { CodeField, WidgetParam } from '@staticcms/core/interface';
|
||||
|
||||
const CodeWidget = (): WidgetParam<string | { [key: string]: string }, CodeField> => {
|
||||
return {
|
||||
name: 'code',
|
||||
controlComponent,
|
||||
previewComponent,
|
||||
options: {
|
||||
schema,
|
||||
getDefaultValue: (
|
||||
defaultValue: string | { [key: string]: string } | null | undefined,
|
||||
field: CodeField,
|
||||
) => {
|
||||
if (field.output_code_only) {
|
||||
return String(defaultValue);
|
||||
}
|
||||
|
||||
const langKey = field.keys?.['lang'] ?? 'lang';
|
||||
const codeKey = field.keys?.['code'] ?? 'code';
|
||||
|
||||
if (typeof defaultValue === 'string') {
|
||||
return {
|
||||
[langKey]: field.default_language ?? '',
|
||||
[codeKey]: defaultValue,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
[langKey]: field.default_language ?? defaultValue?.[langKey] ?? '',
|
||||
[codeKey]: defaultValue?.[codeKey] ?? '',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export * from './SettingsButton';
|
||||
export { default as CodeSettingsButton } from './SettingsButton';
|
||||
export * from './SettingsPane';
|
||||
export { default as CodeSettingsPane } from './SettingsPane';
|
||||
export { controlComponent as CodeControl, previewComponent as CodePreview, schema as CodeSchema };
|
||||
|
||||
export default CodeWidget;
|
38
packages/core/src/widgets/code/languageSelectStyles.ts
Normal file
38
packages/core/src/widgets/code/languageSelectStyles.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { reactSelectStyles, borders } from '@staticcms/core/components/UI/styles';
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { OptionStyleState } from '@staticcms/core/components/UI/styles';
|
||||
|
||||
const languageSelectStyles = {
|
||||
...reactSelectStyles,
|
||||
container: (provided: CSSProperties) => ({
|
||||
...reactSelectStyles.container(provided),
|
||||
'margin-top': '2px',
|
||||
}),
|
||||
control: (provided: CSSProperties) => ({
|
||||
...reactSelectStyles.control(provided),
|
||||
border: borders.textField,
|
||||
padding: 0,
|
||||
fontSize: '13px',
|
||||
minHeight: 'auto',
|
||||
}),
|
||||
dropdownIndicator: (provided: CSSProperties) => ({
|
||||
...reactSelectStyles.dropdownIndicator(provided),
|
||||
padding: '4px',
|
||||
}),
|
||||
option: (provided: CSSProperties, state: OptionStyleState) => ({
|
||||
...reactSelectStyles.option(provided, state),
|
||||
padding: 0,
|
||||
paddingLeft: '8px',
|
||||
}),
|
||||
menu: (provided: CSSProperties) => ({
|
||||
...reactSelectStyles.menu(provided),
|
||||
margin: '2px 0',
|
||||
}),
|
||||
menuList: (provided: CSSProperties) => ({
|
||||
...provided,
|
||||
'max-height': '200px',
|
||||
}),
|
||||
};
|
||||
|
||||
export default languageSelectStyles;
|
14
packages/core/src/widgets/code/schema.ts
Normal file
14
packages/core/src/widgets/code/schema.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export default {
|
||||
properties: {
|
||||
default_language: { type: 'string' },
|
||||
allow_language_selection: { type: 'boolean' },
|
||||
output_code_only: { type: 'boolean' },
|
||||
keys: {
|
||||
type: 'object',
|
||||
properties: { code: { type: 'string' }, lang: { type: 'string' } },
|
||||
},
|
||||
default: {
|
||||
oneOf: [{ type: 'string' }, { type: 'object' }],
|
||||
},
|
||||
},
|
||||
};
|
63
packages/core/src/widgets/code/scripts/process-languages.ts
Normal file
63
packages/core/src/widgets/code/scripts/process-languages.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import uniq from 'lodash/uniq';
|
||||
import path from 'path';
|
||||
|
||||
import type { ProcessedCodeLanguage } from '@staticcms/core/interface';
|
||||
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
|
||||
|
||||
const rawDataPath = '../data/languages-raw.yml';
|
||||
const outputPath = '../data/languages.ts';
|
||||
|
||||
interface CodeLanguage {
|
||||
extensions: string[];
|
||||
aliases: string[];
|
||||
codemirror_mode: LanguageName;
|
||||
codemirror_mime_type: string;
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
const filePath = path.resolve(__dirname, rawDataPath);
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
return yaml.load(fileContent) as Record<string, CodeLanguage>;
|
||||
}
|
||||
|
||||
function outputData(data: ProcessedCodeLanguage[]) {
|
||||
const filePath = path.resolve(__dirname, outputPath);
|
||||
return fs.writeFile(
|
||||
filePath,
|
||||
`import type { ProcessedCodeLanguage } from '@staticcms/core/interface';
|
||||
|
||||
const languages: ProcessedCodeLanguage[] = ${JSON.stringify(data, null, 2)};
|
||||
|
||||
export default languages;
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
function transform(data: Record<string, CodeLanguage>) {
|
||||
return Object.entries(data).reduce((acc, [label, lang]) => {
|
||||
const { extensions = [], aliases = [], codemirror_mode, codemirror_mime_type } = lang;
|
||||
if (codemirror_mode) {
|
||||
const dotlessExtensions = extensions.map(ext => ext.slice(1));
|
||||
const identifiers = uniq(
|
||||
[label.toLowerCase(), ...aliases, ...dotlessExtensions].filter(alias => {
|
||||
if (!alias) {
|
||||
return;
|
||||
}
|
||||
return !/[^a-zA-Z]/.test(alias);
|
||||
}),
|
||||
);
|
||||
acc.push({ label, identifiers, codemirror_mode, codemirror_mime_type });
|
||||
}
|
||||
return acc;
|
||||
}, [] as ProcessedCodeLanguage[]);
|
||||
}
|
||||
|
||||
async function process() {
|
||||
const data = await fetchData();
|
||||
const transformedData = transform(data);
|
||||
return outputData(transformedData);
|
||||
}
|
||||
|
||||
process();
|
Reference in New Issue
Block a user