feat: v4.0.0 (#1016)

Co-authored-by: Denys Konovalov <kontakt@denyskon.de>
Co-authored-by: Mathieu COSYNS <64072917+Mathieu-COSYNS@users.noreply.github.com>
This commit is contained in:
Daniel Lautzenheiser
2024-01-03 15:14:09 -05:00
committed by GitHub
parent 682576ffc4
commit 799c7e6936
732 changed files with 48477 additions and 10886 deletions

View File

@ -0,0 +1,4 @@
dist/
bin/
public/
.cache/

View File

@ -0,0 +1,6 @@
{
"arrowParens": "avoid",
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100
}

File diff suppressed because one or more lines are too long

View File

@ -8,33 +8,33 @@
"serve": "vite preview"
},
"dependencies": {
"@babel/eslint-parser": "7.21.3",
"@staticcms/core": "^3.0.0-beta.1",
"babel-loader": "9.1.2",
"@babel/eslint-parser": "7.22.15",
"@staticcms/core": "^4.0.0-beta.0",
"babel-loader": "9.1.3",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@babel/cli": "7.21.0",
"@babel/core": "7.21.4",
"@babel/plugin-syntax-flow": "7.21.4",
"@babel/plugin-transform-react-jsx": "7.21.0",
"@babel/cli": "7.23.0",
"@babel/core": "7.23.0",
"@babel/plugin-syntax-flow": "7.22.5",
"@babel/plugin-transform-react-jsx": "7.22.15",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "10.4.14",
"eslint": "8.39.0",
"eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-cypress": "2.13.3",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2",
"autoprefixer": "10.4.16",
"eslint": "8.50.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-cypress": "2.15.1",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-unicorn": "46.0.1",
"postcss": "8.4.23",
"postcss-scss": "4.0.6",
"prettier": "2.8.8",
"vite": "4.3.9",
"vite-plugin-svgr": "3.2.0",
"webpack": "5.80.0"
"eslint-plugin-unicorn": "48.0.1",
"postcss": "8.4.31",
"postcss-scss": "4.0.9",
"prettier": "3.0.3",
"vite": "4.5.0",
"vite-plugin-svgr": "4.1.0",
"webpack": "5.88.2"
},
"browserslist": {
"production": [

View File

@ -0,0 +1 @@
<svg width="318" height="198" viewBox="0 0 318 198" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>editor</title><defs><rect id="b" width="300" height="180" rx="6"/><filter x="-6%" y="-7.8%" width="112%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.147843071 0" in="shadowBlurOuter1" result="shadowMatrixOuter1"/><feOffset dy="3" in="SourceAlpha" result="shadowOffsetOuter2"/><feGaussianBlur stdDeviation="4.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0974298007 0" in="shadowBlurOuter2" result="shadowMatrixOuter2"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="shadowMatrixOuter2"/></feMerge></filter><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="c"><stop stop-color="#4779DD" offset="0%"/><stop stop-color="#3A69C7" offset="100%"/></linearGradient></defs><g transform="translate(9 6)" fill="none" fill-rule="evenodd"><use fill="#000" filter="url(#a)" xlink:href="#b"/><use fill="#FFF" xlink:href="#b"/><path d="M0 19h149v161H6c-3.3137 0-6-2.6863-6-6V19z" fill="#EFF0F4"/><path fill="#AEB1BD" d="M10 31h18v4H10z"/><path fill="#FFF" d="M10 39h129v12H10z"/><path fill="#D4D6DD" d="M16 43h44v4H16z"/><path fill="#AEB1BD" d="M10 59h10v4H10z"/><path fill="#FFF" d="M10 67h129v12H10z"/><path fill="#D4D6DD" d="M16 71h26v4H16z"/><path fill="#AEB1BD" d="M10 87h35v4H10z"/><path fill="#FFF9E5" d="M10 95h42v27.7686H10z"/><circle fill="#FFC500" cx="31" cy="110.4463" r="7.8099"/><path d="M10 110.1683c4.6101 2.4917 9.3585 3.7375 14.245 3.7375 7.33 0 10.2112-5.0507 15.7083-5.0507 3.6648 0 7.6803 1.5082 12.0467 4.5246v9.3889H10v-12.6003z" fill="#D4D6DD"/><path d="M10 115.257c4.6503-2.702 8.896-4.0531 12.7373-4.0531 5.7618 0 11.322 4.9766 15.525 4.9766 2.8018 0 7.3811-2.196 13.7377-6.588v13.1761H10v-7.5116z" fill="#9AA1AE"/><path fill="#AEB1BD" d="M10 131h10v4H10z"/><path fill="#FFF" d="M10 139h129v41H10z"/><path fill="#D4D6DD" d="M16 145h117v4H16zm0 6h117v4H16zm0 6h83v4H16zm0 10h117v4H16zm0 6h43v4H16z"/><path fill="#9AA1AE" d="M61 172h2v6h-2z"/><path fill="#AEB1BD" d="M179 31h91v8h-91z"/><path fill="#D4D6DD" d="M213 42h24v4h-24z"/><path fill="#FFF9E5" d="M164 56h121v80H164z"/><circle fill="#FFC500" cx="224.5" cy="100.5" r="22.5"/><path d="M164 99.6992c13.2815 7.1784 26.9613 10.7676 41.0393 10.7676 21.117 0 29.4178-14.5508 45.2548-14.5508 10.558 0 22.1266 4.345 34.7059 13.0352V136H164V99.6992z" fill="#D4D6DD"/><path d="M164 114.3594c13.3973-7.7845 25.6291-11.6768 36.6955-11.6768 16.5995 0 32.6183 14.3374 44.7266 14.3374 8.0722 0 21.2648-6.3266 39.5779-18.98V136H164v-21.6406z" fill="#9AA1AE"/><path fill="#D4D6DD" d="M164 145h121v4H164zm0 6h121v4H164zm0 6h85.8376v4H164zm0 10h121v4H164zm0 6h44.4701v4H164z"/><path fill="#9AA1AE" d="M210 172h2v6h-2z"/><path d="M6 0h288c3.3137 0 6 2.6863 6 6v13H0V6c0-3.3137 2.6863-6 6-6z" fill="url(#c)"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1,7 +1,7 @@
backend:
name: test-repo
site_url: 'https://staticcms.org/'
media_folder: assets/uploads
site_url: 'https://example.com'
media_folder: /assets/uploads
media_library:
folder_support: true
locale: en
@ -17,7 +17,7 @@ i18n:
# Optional, defaults to the first item in locales.
# The locale to be used for fields validation and as a baseline for the entry.
defaultLocale: en
default_locale: en
collections:
- name: posts
label: Posts
@ -30,7 +30,6 @@ collections:
summary_fields:
- title
- date
- draft
sortable_fields:
fields:
- title
@ -39,29 +38,25 @@ collections:
field: title
create: true
view_filters:
- label: Posts With Index
field: title
pattern: 'This is post #'
- label: Posts Without Index
field: title
pattern: front matter post
- label: Drafts
field: draft
pattern: true
filters:
- name: posts-with-index
label: Posts With Index
field: title
pattern: 'This is post #'
- name: posts-without-index
label: Posts Without Index
field: title
pattern: front matter post
view_groups:
- label: Year
field: date
pattern: '\d{4}'
- label: Drafts
field: draft
groups:
- name: by-year
label: Year
field: date
pattern: '\d{4}'
fields:
- label: Title
name: title
widget: string
- label: Draft
name: draft
widget: boolean
default: false
- label: Publish Date
name: date
widget: datetime
@ -75,7 +70,7 @@ collections:
- label: Body
name: body
widget: markdown
hint: Main content goes here.
hint: "*Main* __content__ __*goes*__ [here](https://example.com/)."
- name: faq
label: FAQ
folder: _faqs
@ -149,6 +144,19 @@ collections:
widget: boolean
pattern: ['true', 'Must be true']
required: false
- name: prefix
label: With Prefix
widget: boolean
prefix: "I'm a prefix"
- name: suffix
label: With Suffix
widget: boolean
suffix: "I'm a suffix"
- name: prefix_and_suffix
label: With Prefix and Suffix
widget: boolean
prefix: "I'm a prefix"
suffix: "I'm a suffix"
- name: code
label: Code
file: _widgets/code.json
@ -570,6 +578,7 @@ collections:
- label: Type 2 Object
name: type_2_object
widget: object
summary: "{{datetime | date('yyyy-MM-dd')}}"
fields:
- label: Number
name: number
@ -777,6 +786,19 @@ collections:
widget: number
pattern: ['[0-9]{3,}', 'Must be at least 3 digits']
required: false
- name: prefix
label: With Prefix
widget: number
prefix: '$'
- name: suffix
label: With Suffix
widget: number
suffix: '%'
- name: prefix_and_suffix
label: With Prefix and Suffix
widget: number
prefix: '$'
suffix: '%'
- name: object
label: Object
file: _widgets/object.json
@ -1054,6 +1076,19 @@ collections:
widget: string
pattern: ['.{12,}', 'Must have at least 12 characters']
required: false
- name: prefix
label: With Prefix
widget: string
prefix: '$'
- name: suffix
label: With Suffix
widget: string
suffix: '%'
- name: prefix_and_suffix
label: With Prefix and Suffix
widget: string
prefix: '$'
suffix: '%'
- name: text
label: Text
file: _widgets/text.json
@ -1098,11 +1133,6 @@ collections:
file: _data/settings.json
description: General Site Settings
fields:
- label: Number of posts on frontpage
name: front_limit
widget: number
min: 1
max: 10
- label: Global title
name: site_title
widget: string

170
packages/demo/src/cms.js Normal file
View File

@ -0,0 +1,170 @@
import cms, { useMediaAsset } from '@staticcms/core';
import '@staticcms/core/dist/main.css';
import './data';
// Register all the things
cms.init();
const PostPreview = ({ entry, widgetFor }) => {
return h(
'div',
{},
h('div', { className: 'cover' }, h('h1', {}, entry.data.title), widgetFor('image')),
h('p', {}, h('small', {}, 'Written ' + entry.data.date)),
h('div', { className: 'text' }, widgetFor('body')),
);
};
const PostDateFieldPreview = ({ value }) => {
const date = new Date(value);
const month = date.getMonth() + 1;
const day = date.getDate();
return h(
'div',
{},
`${date.getFullYear()}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day}`,
);
};
const GeneralPreview = ({ widgetsFor, entry, collection }) => {
const title = entry.data.site_title;
const posts = entry.data.posts;
const thumb = posts && posts.thumb;
const thumbUrl = useMediaAsset(thumb, collection, undefined, entry);
return h(
'div',
{},
h('h1', {}, title),
h(
'dl',
{},
h('dt', {}, 'Posts on Frontpage'),
h('dd', {}, widgetsFor('posts').widgets.front_limit ?? 0),
h('dt', {}, 'Default Author'),
h('dd', {}, widgetsFor('posts').data?.author ?? 'None'),
h('dt', {}, 'Default Thumbnail'),
h('dd', {}, thumb && h('img', { src: thumbUrl })),
),
);
};
const AuthorsPreview = ({ widgetsFor }) => {
return h(
'div',
{},
h('h1', {}, 'Authors'),
widgetsFor('authors').map(function (author, index) {
return h(
'div',
{ key: index },
h('hr', {}),
h('strong', {}, author.data.name),
author.widgets.description,
);
}),
);
};
const CustomPage = () => {
return h('div', {}, 'I am a custom page!');
};
cms.registerPreviewTemplate('posts', PostPreview);
cms.registerFieldPreview('posts', 'date', PostDateFieldPreview);
cms.registerPreviewTemplate('general', GeneralPreview);
cms.registerPreviewTemplate('authors', AuthorsPreview);
// Pass the name of a registered control to reuse with a new widget preview.
cms.registerWidget('relationKitchenSinkPost', 'relation');
cms.registerAdditionalLink({
id: 'example',
title: 'Example.com',
data: 'https://example.com',
options: {
icon: 'page',
},
});
cms.registerAdditionalLink({
id: 'custom-page',
title: 'Custom Page',
data: CustomPage,
options: {
icon: 'page',
},
});
cms.registerTheme({
name: 'Custom Red Orange',
extends: 'dark',
primary: {
main: '#ff4500',
},
});
cms.registerShortcode('youtube', {
label: 'YouTube',
openTag: '[',
closeTag: ']',
separator: '|',
toProps: args => {
if (args.length > 0) {
return { src: args[0] };
}
return { src: '' };
},
toArgs: ({ src }) => {
return [src];
},
control: ({ src, onChange }) => {
const theme = useTheme();
return h('span', {}, [
h('input', {
key: 'control-input',
value: src,
onChange: event => {
onChange({ src: event.target.value });
},
style: {
width: '100%',
backgroundColor: theme.common.gray,
color: theme.text.primary,
padding: '4px 8px',
},
}),
h(
'iframe',
{
key: 'control-preview',
width: '100%',
height: '315',
src: `https://www.youtube.com/embed/${src}`,
},
'',
),
]);
},
preview: ({ src }) => {
return h(
'span',
{},
h(
'iframe',
{
width: '420',
height: '315',
src: `https://www.youtube.com/embed/${src}`,
},
'',
),
);
},
});

View File

@ -1,300 +0,0 @@
import cms, { useMediaAsset } from "@staticcms/core";
import "@staticcms/core/dist/main.css";
// Register all the things
cms.init();
const PostPreview = ({ entry, widgetFor }) => {
return (
<div>
<div className="cover">
<h1>{entry.data.title}</h1>
{widgetFor("image")}
</div>
<p>
<small>Written {entry.data.date}</small>
</p>
<div className="text">{widgetFor("body")}</div>
</div>
);
};
const PostPreviewCard = ({ entry, theme, hasLocalBackup }) => {
const date = new Date(entry.data.date);
const month = date.getMonth() + 1;
const day = date.getDate();
const image = entry.data.image;
return (
<div style={{ width: "100%" }}>
<div
style={{
width: "100%",
borderTopLeftRadius: "8px",
borderTopRightRadius: "8px",
overflow: "hidden",
height: "140px",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
objectFit: "cover",
backgroundImage: `url('${image}')`,
}}
/>
<div style={{ padding: "16px", width: "100%" }}>
<div
style={{
display: "flex",
width: "100%",
justifyContent: "space-between",
alignItems: "start",
gap: "4px",
color: theme === "dark" ? "white" : "inherit",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "baseline",
gap: "4px",
}}
>
<div
style={{
fontSize: "14px",
fontWeight: 700,
color: "rgb(107, 114, 128)",
lineHeight: "18px",
}}
>
{entry.data.title}
</div>
<span style={{ fontSize: "14px" }}>{`${date.getFullYear()}-${month < 10 ? `0${month}` : month}-${
day < 10 ? `0${day}` : day
}`}</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
whiteSpace: "no-wrap",
gap: "8px",
}}
>
{hasLocalBackup ? (
<div
style={{
border: "2px solid rgb(147, 197, 253)",
borderRadius: "50%",
color: "rgb(147, 197, 253)",
height: "18px",
width: "18px",
fontWeight: "bold",
fontSize: "11px",
display: "flex",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
}}
title="Has local backup"
>
i
</div>
) : null}
<div
style={{
backgroundColor: entry.data.draft === true ? "rgb(37, 99, 235)" : "rgb(22, 163, 74)",
color: "white",
border: "none",
padding: "2px 6px",
textAlign: "center",
textDecoration: "none",
display: "inline-block",
cursor: "pointer",
borderRadius: "4px",
fontSize: "14px",
}}
>
{entry.data.draft === true ? "Draft" : "Published"}
</div>
</div>
</div>
</div>
</div>
);
};
const PostDateFieldPreview = ({ value }) => {
const date = new Date(value);
const month = date.getMonth() + 1;
const day = date.getDate();
return <div>{`${date.getFullYear()}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day}`}</div>;
};
const PostDraftFieldPreview = ({ value }) => {
return (
<div
style={{
backgroundColor: value === true ? "rgb(37 99 235)" : "rgb(22 163 74)",
color: "white",
border: "none",
padding: "2px 6px",
textAlign: "center",
textDecoration: "none",
display: "inline-block",
cursor: "pointer",
borderRadius: "4px",
fontSize: "14px",
lineHeight: "16px",
height: "20px",
}}
>
{value === true ? "Draft" : "Published"}
</div>
);
};
const GeneralPreview = ({ widgetsFor, entry }) => {
const title = entry.data.site_title;
const posts = entry.data.posts;
const thumb = posts && posts.thumb;
const thumbUrl = useMediaAsset(thumb);
return (
<div>
<h1>{title}</h1>
<dl>
<dt>Posts on Frontpage</dt>
<dd>{widgetsFor("posts").widgets.front_limit ?? 0}</dd>
</dl>
<dl>
<dt>Default Author</dt>
<dd>{widgetsFor("posts").data?.author ?? "None"}</dd>
</dl>
<dl>
<dt>Default Thumbnail</dt>
<dd>{thumb && <img src={thumbUrl} />}</dd>
</dl>
</div>
);
};
const AuthorsPreview = ({ widgetsFor }) => {
return (
<div>
<h1>Authors</h1>
{widgetsFor("authors").map((author, index) => (
<div key={index}>
<hr />
<strong>{author.data.name}</strong>
{author.widgets.description}
</div>
))}
</div>
);
};
const RelationKitchenSinkPostPreview = ({ fieldsMetaData }) => {
// When a post is selected from the relation field, all of it's data
// will be available in the field's metadata nested under the collection
// name, and then further nested under the value specified in `value_field`.
// In this case, the post would be nested under "posts" and then under
// the title of the selected post, since our `value_field` in the config
// is "title".
const post = fieldsMetaData && fieldsMetaData.posts.value;
if (!post) {
return null;
}
return (
<div style={{ border: "2px solid #ccc", borderRadius: "8px", padding: "20px" }}>
<h2>Related Post</h2>
<h3>{post.title}</h3>
<img src={post.image} />
<p>{(post.body ?? "").slice(0, 100) + "..."}</p>
</div>
);
};
const CustomPage = () => {
return <div>I am a custom page!</div>;
};
cms.registerPreviewTemplate("posts", PostPreview);
CMS.registerPreviewCard("posts", PostPreviewCard, () => 240);
CMS.registerFieldPreview("posts", "date", PostDateFieldPreview);
CMS.registerFieldPreview("posts", "draft", PostDraftFieldPreview);
cms.registerPreviewTemplate("general", GeneralPreview);
cms.registerPreviewTemplate("authors", AuthorsPreview);
// Pass the name of a registered control to reuse with a new widget preview.
cms.registerWidget("relationKitchenSinkPost", "relation", RelationKitchenSinkPostPreview);
cms.registerAdditionalLink({
id: "docs",
title: "Static CMS Docs",
data: "https://staticcms.org",
options: {
icon: "page",
},
});
cms.registerAdditionalLink({
id: "config",
title: "Demo config.yml",
data: "https://github.com/StaticJsCMS/static-cms/blob/main/packages/demo/public/config.yml",
options: {
icon: "page",
},
});
cms.registerAdditionalLink({
id: "custom-page",
title: "Custom Page",
data: CustomPage,
options: {
icon: "page",
},
});
cms.registerShortcode("youtube", {
label: "YouTube",
openTag: "[",
closeTag: "]",
separator: "|",
toProps: (args) => {
if (args.length > 0) {
return { src: args[0] };
}
return { src: "" };
},
toArgs: ({ src }) => {
return [src];
},
control: ({ src, onChange }) => {
return (
<span>
<input
key="control-input"
value={src}
onChange={(event) => {
onChange({ src: event.target.value });
}}
/>
<iframe key="control-preview" width="420" height="315" src={`https://www.youtube.com/embed/${src}`} />
</span>
);
},
preview: ({ src }) => {
return (
<span>
<iframe width="420" height="315" src={`https://www.youtube.com/embed/${src}`} />
</span>
);
},
});

302
packages/demo/src/data.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Static CMS Demo - Editorial</title>
<link rel="icon" type="image/png" href="/static-cms-icon.png" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<link href="/editorial/config.yml" type="text/yaml" rel="cms-config-url" />
<script type="module" src="/index.jsx"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Static CMS Demo - Simple</title>
<link rel="icon" type="image/png" href="/static-cms-icon.png" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<link href="/simple/config.yml" type="text/yaml" rel="cms-config-url" />
<script type="module" src="/index.jsx"></script>
</body>
</html>

View File

@ -1,17 +1,48 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgrPlugin from "vite-plugin-svgr";
import { defineConfig } from 'vite';
import { resolve } from 'path';
import react from '@vitejs/plugin-react';
import svgrPlugin from 'vite-plugin-svgr';
const root = resolve(__dirname, 'src');
const publicDir = resolve(__dirname, 'public');
const outDir = resolve(__dirname, 'build');
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), svgrPlugin()],
assetsInclude: ["public/**/*"],
root,
publicDir,
plugins: [
react(),
svgrPlugin(),
{
name: 'rewrite-middleware',
configureServer(serve) {
serve.middlewares.use((req, _res, next) => {
if (req.url === '/simple') {
req.url = '/simple/';
} else if (req.url === '/editorial') {
req.url = '/editorial/';
}
next();
});
},
},
],
assetsInclude: ['public/**/*'],
optimizeDeps: {
force: true,
include: ["@staticcms/core"],
include: ['@staticcms/core'],
},
build: {
commonjsOptions: { include: [/core/, /node_modules/] },
outDir: "build",
outDir,
emptyOutDir: true,
rollupOptions: {
input: {
main: resolve(root, 'index.html'),
simple: resolve(root, 'simple', 'index.html'),
editorial: resolve(root, 'editorial', 'index.html'),
},
},
},
});