refactor: monorepo setup with lerna (#243)

This commit is contained in:
Daniel Lautzenheiser
2022-12-15 13:44:49 -05:00
committed by GitHub
parent dac29fbf3c
commit 504d95c34f
706 changed files with 16571 additions and 142 deletions

View 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;

View 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;

View 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;

View 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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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' }],
},
},
};

View 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();