Feature/improved types (#68)

* v1.0.0-alpha34
This commit is contained in:
Daniel Lautzenheiser 2022-11-05 00:22:38 -04:00 committed by GitHub
parent 6e9e5c53ef
commit 13d30beba0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 791 additions and 196 deletions

View File

@ -15,10 +15,6 @@
content:
'{\n"title": "This is a JSON front matter post",\n"image": "/nf-logo.png",\n"date": "2015-02-15T00:00:00.000Z"\n}\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n',
},
'2015-02-16-this-is-a-toml-frontmatter-post.md': {
content:
'+++\ntitle = "This is a TOML front matter post"\nimage = "/nf-logo.png"\n"date" = "2015-02-16T00:00:00.000Z"\n+++\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n',
},
'2015-02-14-this-is-a-post-with-a-different-extension.other': {
content:
'---\ntitle: This post should not appear because the extension is different\nimage: /nf-logo.png\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n',

View File

@ -1,6 +1,6 @@
{
"name": "@staticcms/core",
"version": "1.0.0-alpha33",
"version": "1.0.0-alpha34",
"license": "MIT",
"description": "Static CMS core application.",
"repository": "https://github.com/StaticJsCMS/static-cms",
@ -27,12 +27,10 @@
"type-check": "tsc --watch"
},
"main": "dist/static-cms-core.js",
"types": "dist/index.d.ts",
"files": [
"src/",
"dist/",
"index.d.ts"
"dist/**/*"
],
"types": "index.d.ts",
"browserslist": [
"last 2 Chrome versions",
"last 2 ChromeAndroid versions",
@ -47,7 +45,6 @@
"@emotion/css": "11.10.0",
"@emotion/react": "11.10.4",
"@emotion/styled": "11.10.4",
"@iarna/toml": "2.2.5",
"@mui/icons-material": "5.10.6",
"@mui/material": "5.10.10",
"@mui/system": "5.4.1",
@ -111,7 +108,6 @@
"semaphore": "1.1.0",
"stream-browserify": "3.0.0",
"symbol-observable": "4.0.0",
"tomlify-j0.4": "3.0.0",
"ts-loader": "9.4.1",
"uploadcare-widget": "3.19.0",
"uploadcare-widget-tab-effects": "1.5.0",

View File

@ -96,54 +96,56 @@ const Sidebar = ({
const additionalLinks = useMemo(() => getAdditionalLinks(), []);
const links = useMemo(
() =>
Object.values(additionalLinks).map(({ id, title, data, options: { iconName } = {} }) => {
let icon: ReactNode = <ArticleIcon />;
if (iconName) {
const StoredIcon = getIcon(iconName);
if (StoredIcon) {
icon = <StoredIcon />;
Object.values(additionalLinks).map(
({ id, title, data, options: { icon: iconName } = {} }) => {
let icon: ReactNode = <ArticleIcon />;
if (iconName) {
const StoredIcon = getIcon(iconName);
if (StoredIcon) {
icon = <StoredIcon />;
}
}
}
const content = (
<>
<StyledListItemIcon>{icon}</StyledListItemIcon>
<ListItemText primary={title} />
</>
);
const content = (
<>
<StyledListItemIcon>{icon}</StyledListItemIcon>
<ListItemText primary={title} />
</>
);
return typeof data === 'string' ? (
<ListItem
key={title}
href={data}
component="a"
disablePadding
target="_blank"
rel="noopener"
sx={{
color: colors.inactive,
'&:hover': {
color: colors.active,
'.MuiListItemIcon-root': {
return typeof data === 'string' ? (
<ListItem
key={title}
href={data}
component="a"
disablePadding
target="_blank"
rel="noopener"
sx={{
color: colors.inactive,
'&:hover': {
color: colors.active,
'.MuiListItemIcon-root': {
color: colors.active,
},
},
},
}}
>
<ListItemButton>{content}</ListItemButton>
</ListItem>
) : (
<ListItem
key={title}
to={`/page/${id}`}
component={NavLink}
disablePadding
activeClassName="sidebar-active"
>
<ListItemButton>{content}</ListItemButton>
</ListItem>
);
}),
}}
>
<ListItemButton>{content}</ListItemButton>
</ListItem>
) : (
<ListItem
key={title}
to={`/page/${id}`}
component={NavLink}
disablePadding
activeClassName="sidebar-active"
>
<ListItemButton>{content}</ListItemButton>
</ListItem>
);
},
),
[additionalLinks],
);

View File

@ -13,8 +13,6 @@ const EditorPreviewContent = ({ previewComponent, previewProps }: EditorPreviewC
let children: ReactNode;
if (!previewComponent) {
children = null;
} else if (React.isValidElement(previewComponent)) {
children = React.cloneElement(previewComponent, previewProps);
} else {
children = React.createElement(previewComponent, previewProps);
}

View File

@ -429,13 +429,14 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
const element = useMemo(() => document.getElementById('cms-root'), []);
const previewProps: Omit<TemplatePreviewProps, 'document' | 'window'> = useMemo(
() => ({
...props,
getAsset: handleGetAsset,
widgetFor,
widgetsFor,
}),
const previewProps = useMemo(
() =>
({
...props,
getAsset: handleGetAsset,
widgetFor,
widgetsFor,
} as Omit<TemplatePreviewProps, 'document' | 'window'>),
[handleGetAsset, props, widgetFor, widgetsFor],
);

View File

@ -1,36 +0,0 @@
import toml from '@iarna/toml';
import moment from 'moment';
import tomlify from 'tomlify-j0.4';
import AssetProxy from '../valueObjects/AssetProxy';
import { sortKeys } from './helpers';
import { FileFormatter } from './FileFormatter';
function outputReplacer(_key: string, value: unknown) {
if (moment.isMoment(value)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return value.format(value._f);
}
if (value instanceof AssetProxy) {
return `${value.path}`;
}
if (typeof value === 'number' && Number.isInteger(value)) {
// Return the string representation of integers so tomlify won't render with tenths (".0")
return value.toString();
}
// Return `false` to use default (`undefined` would delete key).
return false;
}
class TomlFormatter extends FileFormatter {
fromFile(content: string) {
return toml.parse(content);
}
toFile(data: object, sortedKeys: string[] = []): string {
return tomlify.toToml(data as object, { replace: outputReplacer, sort: sortKeys(sortedKeys) });
}
}
export default new TomlFormatter();

View File

@ -1,29 +1,25 @@
import YamlFormatter from './YamlFormatter';
import TomlFormatter from './TomlFormatter';
import JsonFormatter from './JsonFormatter';
import { FrontmatterInfer, frontmatterJSON, frontmatterTOML, frontmatterYAML } from './frontmatter';
import { FrontmatterInfer, frontmatterJSON, frontmatterYAML } from './frontmatter';
import type { Delimiter } from './frontmatter';
import type { Collection, Entry, Format } from '../interface';
import type { FileFormatter } from './FileFormatter';
export const frontmatterFormats = ['yaml-frontmatter', 'toml-frontmatter', 'json-frontmatter'];
export const frontmatterFormats = ['yaml-frontmatter', 'json-frontmatter'];
export const formatExtensions = {
yml: 'yml',
yaml: 'yml',
toml: 'toml',
json: 'json',
frontmatter: 'md',
'json-frontmatter': 'md',
'toml-frontmatter': 'md',
'yaml-frontmatter': 'md',
};
export const extensionFormatters: Record<string, FileFormatter> = {
yml: YamlFormatter,
yaml: YamlFormatter,
toml: TomlFormatter,
json: JsonFormatter,
md: FrontmatterInfer,
markdown: FrontmatterInfer,
@ -34,11 +30,9 @@ function formatByName(name: Format, customDelimiter?: Delimiter): FileFormatter
const fileFormatter: Record<string, FileFormatter> = {
yml: YamlFormatter,
yaml: YamlFormatter,
toml: TomlFormatter,
json: JsonFormatter,
frontmatter: FrontmatterInfer,
'json-frontmatter': frontmatterJSON(customDelimiter),
'toml-frontmatter': frontmatterTOML(customDelimiter),
'yaml-frontmatter': frontmatterYAML(customDelimiter),
};

View File

@ -1,13 +1,11 @@
import matter from 'gray-matter';
import TomlFormatter from './TomlFormatter';
import YamlFormatter from './YamlFormatter';
import JsonFormatter from './JsonFormatter';
import { FileFormatter } from './FileFormatter';
const Languages = {
YAML: 'yaml',
TOML: 'toml',
JSON: 'json',
} as const;
@ -17,13 +15,6 @@ export type Delimiter = string | [string, string];
type Format = { language: Language; delimiters: Delimiter };
const parsers = {
toml: {
parse: (input: string) => TomlFormatter.fromFile(input),
stringify: (metadata: object, opts?: { sortedKeys?: string[] }) => {
const { sortedKeys } = opts || {};
return TomlFormatter.toFile(metadata, sortedKeys);
},
},
json: {
parse: (input: string) => {
let JSONinput = input.trim();
@ -58,14 +49,11 @@ function inferFrontmatterFormat(str: string) {
const lineEnd = str.indexOf('\n');
const firstLine = str.slice(0, lineEnd !== -1 ? lineEnd : 0).trim();
if (firstLine.length > 3 && firstLine.slice(0, 3) === '---') {
// No need to infer, `gray-matter` will handle things like `---toml` for us.
return;
}
switch (firstLine) {
case '---':
return getFormatOpts(Languages.YAML);
case '+++':
return getFormatOpts(Languages.TOML);
case '{':
return getFormatOpts(Languages.JSON);
default:
@ -80,7 +68,6 @@ export function getFormatOpts(format?: Language, customDelimiter?: Delimiter) {
const formats: { [key in Language]: Format } = {
yaml: { language: Languages.YAML, delimiters: '---' },
toml: { language: Languages.TOML, delimiters: '+++' },
json: { language: Languages.JSON, delimiters: ['{', '}'] },
};
@ -143,10 +130,6 @@ export function frontmatterYAML(customDelimiter?: Delimiter) {
return new FrontmatterFormatter(Languages.YAML, customDelimiter);
}
export function frontmatterTOML(customDelimiter?: Delimiter) {
return new FrontmatterFormatter(Languages.TOML, customDelimiter);
}
export function frontmatterJSON(customDelimiter?: Delimiter) {
return new FrontmatterFormatter(Languages.JSON, customDelimiter);
}

View File

@ -10,7 +10,7 @@ export * from './media-libraries';
export * from './locales';
export * from './lib';
export const CMS = {
const CMS = {
...Registry,
init: bootstrap,
};

View File

@ -263,29 +263,30 @@ export type WidgetPreviewComponent<T = unknown, F extends Field = Field> =
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>
| ComponentType<WidgetPreviewProps<T, F>>;
export interface TemplatePreviewProps<T = unknown> {
export type WidgetsFor<P = EntryData> = <K extends keyof P>(
name: K,
) => P[K] extends Array<infer U>
? {
data: U | null;
widgets: Record<keyof U, React.ReactNode>;
}[]
: {
data: P[K] | null;
widgets: Record<keyof P[K], React.ReactNode>;
};
export interface TemplatePreviewProps<T = EntryData> {
collection: Collection;
fields: Field[];
entry: Entry<T>;
document: Document | undefined | null;
window: Window | undefined | null;
getAsset: GetAssetFunction;
widgetFor: (name: string) => ReactNode;
widgetsFor: (name: string) =>
| {
data: EntryData | null;
widgets: Record<string, React.ReactNode>;
}
| {
data: EntryData | null;
widgets: Record<string, React.ReactNode>;
}[];
widgetFor: (name: T extends EntryData ? string : keyof T) => ReactNode;
widgetsFor: WidgetsFor<T>;
}
export type TemplatePreviewComponent =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>
| ComponentType<TemplatePreviewProps>;
export type TemplatePreviewComponent<T = EntryData> = ComponentType<TemplatePreviewProps<T>>;
export interface WidgetOptions<T = unknown, F extends Field = Field> {
validator?: Widget<T, F>['validator'];
@ -752,7 +753,7 @@ export interface EventListener {
}
export interface AdditionalLinkOptions {
iconName?: string;
icon?: string;
}
export interface AdditionalLink {

View File

@ -8,6 +8,7 @@ import type {
Config,
CustomIcon,
Entry,
EntryData,
EventData,
EventListener,
Field,
@ -34,7 +35,7 @@ const eventHandlers = allowedEvents.reduce((acc, e) => {
interface Registry {
backends: Record<string, BackendInitializer>;
templates: Record<string, TemplatePreviewComponent>;
templates: Record<string, TemplatePreviewComponent<EntryData>>;
widgets: Record<string, Widget>;
icons: Record<string, CustomIcon>;
additionalLinks: Record<string, AdditionalLink>;
@ -109,11 +110,11 @@ export function getPreviewStyles() {
/**
* Preview Templates
*/
export function registerPreviewTemplate(name: string, component: TemplatePreviewComponent) {
registry.templates[name] = component;
export function registerPreviewTemplate<T>(name: string, component: TemplatePreviewComponent<T>) {
registry.templates[name] = component as TemplatePreviewComponent<EntryData>;
}
export function getPreviewTemplate(name: string): TemplatePreviewComponent {
export function getPreviewTemplate(name: string): TemplatePreviewComponent<EntryData> {
return registry.templates[name];
}
@ -126,7 +127,7 @@ export function registerWidget(widget: WidgetParam): void;
export function registerWidget<T = unknown>(
name: string,
control: string | Widget<T>['control'],
preview: Widget<T>['preview'],
preview?: Widget<T>['preview'],
options?: WidgetOptions,
): void;
export function registerWidget<T = unknown>(
@ -358,7 +359,6 @@ export function getAdditionalLinks(): Record<string, AdditionalLink> {
}
export function getAdditionalLink(id: string): AdditionalLink | undefined {
console.log('additionalLinks', registry.additionalLinks);
return registry.additionalLinks[id];
}

View File

@ -85,14 +85,33 @@ const DateTimeControl = ({
[timezoneOffset],
);
const inputFormat = useMemo(() => {
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
const formatParts: string[] = [];
if (typeof dateFormat === 'string' && isNotEmpty(dateFormat)) {
formatParts.push(dateFormat);
}
if (typeof timeFormat === 'string' && isNotEmpty(timeFormat)) {
formatParts.push(timeFormat);
}
if (formatParts.length > 0) {
return formatParts.join(' ');
}
}
return "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
}, [dateFormat, timeFormat]);
const defaultValue = useMemo(() => {
const today = field.picker_utc ? localToUTC(new Date()) : new Date();
return field.default === undefined
? format
? formatDate(today, format)
: formatISO(today)
: formatDate(today, inputFormat)
: field.default;
}, [field.default, field.picker_utc, format, localToUTC]);
}, [field.default, field.picker_utc, format, inputFormat, localToUTC]);
const [internalValue, setInternalValue] = useState(value ?? defaultValue);
@ -132,14 +151,12 @@ const DateTimeControl = ({
const dateTimePicker = useMemo(() => {
if (dateFormat && !timeFormat) {
const inputDateFormat = typeof dateFormat === 'string' ? dateFormat : 'MMM d, yyyy';
return (
<MobileDatePicker
key="mobile-date-picker"
inputFormat={inputDateFormat}
inputFormat={inputFormat}
label={label}
value={formatDate(field.picker_utc ? utcDate : dateValue, inputDateFormat)}
value={formatDate(field.picker_utc ? utcDate : dateValue, inputFormat)}
onChange={handleChange}
renderInput={params => (
<TextField
@ -164,14 +181,12 @@ const DateTimeControl = ({
}
if (!dateFormat && timeFormat) {
const inputTimeFormat = typeof timeFormat === 'string' ? timeFormat : 'H:mm';
return (
<TimePicker
key="time-picker"
label={label}
inputFormat={inputTimeFormat}
value={formatDate(field.picker_utc ? utcDate : dateValue, inputTimeFormat)}
inputFormat={inputFormat}
value={formatDate(field.picker_utc ? utcDate : dateValue, inputFormat)}
onChange={handleChange}
renderInput={params => (
<TextField
@ -195,22 +210,6 @@ const DateTimeControl = ({
);
}
let inputFormat = 'MMM d, yyyy H:mm';
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
const formatParts: string[] = [];
if (typeof dateFormat === 'string' && isNotEmpty(dateFormat)) {
formatParts.push(dateFormat);
}
if (typeof timeFormat === 'string' && isNotEmpty(timeFormat)) {
formatParts.push(timeFormat);
}
if (formatParts.length > 0) {
inputFormat = formatParts.join(' ');
}
}
return (
<MobileDateTimePicker
key="mobile-date-time-picker"
@ -244,6 +243,8 @@ const DateTimeControl = ({
field.picker_utc,
handleChange,
hasErrors,
inputFormat,
internalValue,
isDisabled,
label,
t,

File diff suppressed because it is too large Load Diff