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

View File

@ -96,7 +96,8 @@ const Sidebar = ({
const additionalLinks = useMemo(() => getAdditionalLinks(), []); const additionalLinks = useMemo(() => getAdditionalLinks(), []);
const links = useMemo( const links = useMemo(
() => () =>
Object.values(additionalLinks).map(({ id, title, data, options: { iconName } = {} }) => { Object.values(additionalLinks).map(
({ id, title, data, options: { icon: iconName } = {} }) => {
let icon: ReactNode = <ArticleIcon />; let icon: ReactNode = <ArticleIcon />;
if (iconName) { if (iconName) {
const StoredIcon = getIcon(iconName); const StoredIcon = getIcon(iconName);
@ -143,7 +144,8 @@ const Sidebar = ({
<ListItemButton>{content}</ListItemButton> <ListItemButton>{content}</ListItemButton>
</ListItem> </ListItem>
); );
}), },
),
[additionalLinks], [additionalLinks],
); );

View File

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

View File

@ -429,13 +429,14 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
const element = useMemo(() => document.getElementById('cms-root'), []); const element = useMemo(() => document.getElementById('cms-root'), []);
const previewProps: Omit<TemplatePreviewProps, 'document' | 'window'> = useMemo( const previewProps = useMemo(
() => ({ () =>
({
...props, ...props,
getAsset: handleGetAsset, getAsset: handleGetAsset,
widgetFor, widgetFor,
widgetsFor, widgetsFor,
}), } as Omit<TemplatePreviewProps, 'document' | 'window'>),
[handleGetAsset, props, widgetFor, widgetsFor], [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 YamlFormatter from './YamlFormatter';
import TomlFormatter from './TomlFormatter';
import JsonFormatter from './JsonFormatter'; import JsonFormatter from './JsonFormatter';
import { FrontmatterInfer, frontmatterJSON, frontmatterTOML, frontmatterYAML } from './frontmatter'; import { FrontmatterInfer, frontmatterJSON, frontmatterYAML } from './frontmatter';
import type { Delimiter } from './frontmatter'; import type { Delimiter } from './frontmatter';
import type { Collection, Entry, Format } from '../interface'; import type { Collection, Entry, Format } from '../interface';
import type { FileFormatter } from './FileFormatter'; import type { FileFormatter } from './FileFormatter';
export const frontmatterFormats = ['yaml-frontmatter', 'toml-frontmatter', 'json-frontmatter']; export const frontmatterFormats = ['yaml-frontmatter', 'json-frontmatter'];
export const formatExtensions = { export const formatExtensions = {
yml: 'yml', yml: 'yml',
yaml: 'yml', yaml: 'yml',
toml: 'toml',
json: 'json', json: 'json',
frontmatter: 'md', frontmatter: 'md',
'json-frontmatter': 'md', 'json-frontmatter': 'md',
'toml-frontmatter': 'md',
'yaml-frontmatter': 'md', 'yaml-frontmatter': 'md',
}; };
export const extensionFormatters: Record<string, FileFormatter> = { export const extensionFormatters: Record<string, FileFormatter> = {
yml: YamlFormatter, yml: YamlFormatter,
yaml: YamlFormatter, yaml: YamlFormatter,
toml: TomlFormatter,
json: JsonFormatter, json: JsonFormatter,
md: FrontmatterInfer, md: FrontmatterInfer,
markdown: FrontmatterInfer, markdown: FrontmatterInfer,
@ -34,11 +30,9 @@ function formatByName(name: Format, customDelimiter?: Delimiter): FileFormatter
const fileFormatter: Record<string, FileFormatter> = { const fileFormatter: Record<string, FileFormatter> = {
yml: YamlFormatter, yml: YamlFormatter,
yaml: YamlFormatter, yaml: YamlFormatter,
toml: TomlFormatter,
json: JsonFormatter, json: JsonFormatter,
frontmatter: FrontmatterInfer, frontmatter: FrontmatterInfer,
'json-frontmatter': frontmatterJSON(customDelimiter), 'json-frontmatter': frontmatterJSON(customDelimiter),
'toml-frontmatter': frontmatterTOML(customDelimiter),
'yaml-frontmatter': frontmatterYAML(customDelimiter), 'yaml-frontmatter': frontmatterYAML(customDelimiter),
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff