kopia lustrzana https://github.com/wagtail/wagtail
Upgrade Storybook setup, with latest release, less boilerplate
rodzic
b3e5f751e4
commit
d149a27392
10
.eslintrc.js
10
.eslintrc.js
|
@ -92,7 +92,15 @@ module.exports = {
|
|||
overrides: [
|
||||
{
|
||||
// Rules we don’t want to enforce for test and tooling code.
|
||||
files: ['*.test.ts', '*.test.tsx', '*.test.js', 'webpack.config.js'],
|
||||
files: [
|
||||
'*.test.ts',
|
||||
'*.test.tsx',
|
||||
'*.test.js',
|
||||
'webpack.config.js',
|
||||
'*.stories.js',
|
||||
'*.stories.tsx',
|
||||
'storybook/**/*',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
|
|
|
@ -3,3 +3,6 @@ graft wagtail
|
|||
prune wagtail/wagtailadmin/static_src
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
||||
global-exclude *.stories.js
|
||||
global-exclude *.stories.tsx
|
||||
recursive-exclude wagtail/ *.md
|
||||
|
|
|
@ -2,18 +2,44 @@ import React, { useRef, useEffect } from 'react';
|
|||
|
||||
import { renderPattern, simulateLoading } from 'storybook-django';
|
||||
|
||||
const getTemplateName = (template?: string, filename?: string) =>
|
||||
const getTemplateName = (template?: string, filename?: string): string =>
|
||||
template ||
|
||||
filename?.replace(/.+\/templates\//, '').replace(/\.stories\..+$/, '.html');
|
||||
filename?.replace(/.+\/templates\//, '').replace(/\.stories\..+$/, '.html') ||
|
||||
'template-not-found';
|
||||
|
||||
type ContextMapping = { [key: string]: any };
|
||||
type TagsMapping = { [key: string]: any };
|
||||
|
||||
interface TemplatePatternProps {
|
||||
element?: 'div' | 'span';
|
||||
// Path to the template file.
|
||||
template?: string;
|
||||
// Path to a Storybook `stories` file, which should be placed next to and named the same as the HTML template.
|
||||
filename?: string;
|
||||
context?: { [key: string]: any };
|
||||
tags?: { [key: string]: any };
|
||||
context?: ContextMapping;
|
||||
tags?: TagsMapping;
|
||||
}
|
||||
|
||||
const PATTERN_LIBRARY_RENDER_URL = '/pattern-library/api/v1/render-pattern';
|
||||
|
||||
/**
|
||||
* Retrieves a template pattern’s HTML (or error response) from the server.
|
||||
*/
|
||||
export const getTemplatePattern = (
|
||||
templateName: string,
|
||||
context: ContextMapping,
|
||||
tags: TagsMapping,
|
||||
callback: (html: string) => void,
|
||||
) =>
|
||||
renderPattern(PATTERN_LIBRARY_RENDER_URL, templateName, context, tags)
|
||||
.catch(callback)
|
||||
.then((res) => res.text())
|
||||
.then(callback);
|
||||
|
||||
/**
|
||||
* Renders one of our Django templates as if it was a React component.
|
||||
* All props are marked as optional, but either template or filename should be provided.
|
||||
*/
|
||||
const TemplatePattern = ({
|
||||
element = 'div',
|
||||
template,
|
||||
|
@ -22,14 +48,13 @@ const TemplatePattern = ({
|
|||
tags = {},
|
||||
}: TemplatePatternProps) => {
|
||||
const ref = useRef(null);
|
||||
const templateName = getTemplateName(template, filename);
|
||||
|
||||
useEffect(() => {
|
||||
renderPattern(window.PATTERN_LIBRARY_API, templateName, context, tags)
|
||||
.catch((err) => simulateLoading(ref.current, err))
|
||||
.then((res) => res.text())
|
||||
.then((html) => simulateLoading(ref.current, html));
|
||||
}, []);
|
||||
const templateName = getTemplateName(template, filename);
|
||||
getTemplatePattern(templateName, context, tags, (html) =>
|
||||
simulateLoading(ref.current, html),
|
||||
);
|
||||
});
|
||||
|
||||
return React.createElement(element, { ref });
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { Meta, Story, Canvas } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Welcome" />
|
||||
|
||||
# Welcome to Wagtail’s design system
|
||||
|
||||
Wagtail’s admin interface is built with reusable UI components, structured as a design system for ease of maintenance and third-party reuse.
|
|
@ -1,12 +1,21 @@
|
|||
module.exports = {
|
||||
stories: ['../../client/**/*.stories.*', '../../wagtail/**/*.stories.*'],
|
||||
addons: ['@storybook/addon-docs'],
|
||||
stories: [
|
||||
'../../client/**/*.stories.mdx',
|
||||
'../../client/**/*.stories.@(js|tsx)',
|
||||
{
|
||||
directory: '../../wagtail/admin/templates/wagtailadmin/shared/',
|
||||
titlePrefix: 'Shared',
|
||||
files: '*.stories.*',
|
||||
},
|
||||
'../../wagtail/**/*.stories.*',
|
||||
],
|
||||
addons: ['@storybook/addon-docs', '@storybook/addon-controls'],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
builder: 'webpack5',
|
||||
},
|
||||
webpackFinal: (config) => {
|
||||
/* eslint-disable no-param-reassign */
|
||||
config.resolve.fallback.crypto = false;
|
||||
|
||||
const rules = [
|
||||
{
|
||||
|
@ -33,6 +42,7 @@ module.exports = {
|
|||
|
||||
config.module.rules = config.module.rules.concat(rules);
|
||||
|
||||
// Allow using path magic variables to reduce boilerplate in stories.
|
||||
config.node = {
|
||||
__filename: true,
|
||||
__dirname: true,
|
||||
|
|
|
@ -5,6 +5,6 @@ const origin = process.env.TEST_ORIGIN ?? 'http://localhost:8000';
|
|||
|
||||
module.exports = middleware.createDjangoAPIMiddleware({
|
||||
origin,
|
||||
// Must match the urls.py pattern for the pattern library.
|
||||
apiPath: '/pattern-library/',
|
||||
// Must match the patterns in urls.py.
|
||||
apiPath: ['/pattern-library/', '/static/wagtailadmin/'],
|
||||
});
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<div data-sprite></div>
|
||||
<script>
|
||||
// Load icon sprite into the DOM, just like in the Wagtail admin.
|
||||
window.fetch('/pattern-library/api/v1/sprite')
|
||||
.then((res) => res.text())
|
||||
.then((html) => document.querySelector('[data-sprite]').innerHTML = html);
|
||||
</script>
|
|
@ -4,4 +4,45 @@ import '../../wagtail/admin/static_src/wagtailadmin/scss/core.scss';
|
|||
import '../../wagtail/admin/static_src/wagtailadmin/scss/sidebar.scss';
|
||||
import './preview.scss';
|
||||
|
||||
window.PATTERN_LIBRARY_API = '/pattern-library/api/v1/render-pattern';
|
||||
export const parameters = {
|
||||
controls: {
|
||||
hideNoControlsWarning: true,
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const cachedIcons = sessionStorage.getItem('WAGTAIL_ICONS');
|
||||
window.WAGTAIL_ICONS = cachedIcons ? JSON.parse(cachedIcons) : [];
|
||||
|
||||
/**
|
||||
* Loads Wagtail’s icon sprite into the DOM, similarly to the admin.
|
||||
*/
|
||||
const loadIconSprite = () => {
|
||||
const PATTERN_LIBRARY_SPRITE_URL = '/pattern-library/api/v1/sprite';
|
||||
|
||||
window
|
||||
.fetch(PATTERN_LIBRARY_SPRITE_URL)
|
||||
.then((res) => res.text())
|
||||
.then((html) => {
|
||||
const sprite = document.createElement('div');
|
||||
sprite.innerHTML = html;
|
||||
const symbols = Array.from(sprite.querySelectorAll('symbol'));
|
||||
const icons = symbols.map((elt) => elt.id.replace('icon-', ''));
|
||||
|
||||
window.WAGTAIL_ICONS = icons;
|
||||
sessionStorage.setItem('WAGTAIL_ICONS', JSON.stringify(icons));
|
||||
|
||||
if (document.body) {
|
||||
document.body.appendChild(sprite);
|
||||
} else {
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
document.body.appendChild(sprite);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
loadIconSprite();
|
||||
|
|
|
@ -5,14 +5,13 @@ body {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
text-transform: initial;
|
||||
}
|
||||
|
||||
// Fix compatibility issue with Wagtail’s tag implementation.
|
||||
// Fix compatibility issue with Wagtail styles.
|
||||
.sbdocs .tag,
|
||||
.sbdocs .tag::before {
|
||||
.sbdocs .tag::before,
|
||||
h1,
|
||||
h2,
|
||||
code,
|
||||
textarea {
|
||||
all: revert;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,5 +3,5 @@ declare module '*.md';
|
|||
declare module '*.html';
|
||||
|
||||
interface Window {
|
||||
PATTERN_LIBRARY_API: string;
|
||||
WAGTAIL_ICONS: string[];
|
||||
}
|
||||
|
|
|
@ -282,6 +282,23 @@ This must be done after every change to the source files. To watch the source fi
|
|||
|
||||
$ npm start
|
||||
|
||||
Using the pattern library
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Wagtail’s UI component library is built with `Storybook <https://storybook.js.org/>`_ and `django-pattern-library <https://github.com/torchbox/django-pattern-library>`_. To run it locally,
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export DJANGO_SETTINGS_MODULE=wagtail.tests.settings_ui
|
||||
$ # Assumes the current environment contains a valid installation of Wagtail for local development.
|
||||
$ ./wagtail/tests/manage.py migrate
|
||||
$ ./wagtail/tests/manage.py createcachetable
|
||||
$ ./wagtail/tests/manage.py runserver 0:8000
|
||||
$ # In a separate terminal:
|
||||
$ npm run storybook
|
||||
|
||||
The last command will start Storybook at ``http://localhost:6006/``. It will proxy specific requests to Django at ``http://localhost:8000`` by default. Use the ``TEST_ORIGIN`` environment variable to use a different port for Django: ``TEST_ORIGIN=http://localhost:9000 npm run storybook``.
|
||||
|
||||
Compiling the documentation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
Plik diff jest za duży
Load Diff
13
package.json
13
package.json
|
@ -50,10 +50,11 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.7",
|
||||
"@storybook/addon-docs": "^6.3.12",
|
||||
"@storybook/builder-webpack5": "^6.3.12",
|
||||
"@storybook/manager-webpack5": "^6.3.12",
|
||||
"@storybook/react": "^6.3.12",
|
||||
"@storybook/addon-controls": "^6.4.19",
|
||||
"@storybook/addon-docs": "^6.4.19",
|
||||
"@storybook/builder-webpack5": "^6.4.19",
|
||||
"@storybook/manager-webpack5": "^6.4.19",
|
||||
"@storybook/react": "^6.4.19",
|
||||
"@types/draft-js": "^0.10.45",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/react": "^16.14.21",
|
||||
|
@ -111,9 +112,9 @@
|
|||
"scripts": {
|
||||
"start": "webpack --config ./client/webpack.config.js --mode development --progress --watch",
|
||||
"build": "webpack --config ./client/webpack.config.js --mode production",
|
||||
"fix:js": "eslint --ext .js,.ts,.tsx --fix ./",
|
||||
"fix:js": "eslint --ext .js,.ts,.tsx --fix .",
|
||||
"format": "prettier --write '**/?(.)*.{css,scss,js,ts,tsx,json,yaml,yml}'",
|
||||
"lint:js": "eslint --ext .js,.ts,.tsx --report-unused-disable-directives ./client",
|
||||
"lint:js": "eslint --ext .js,.ts,.tsx --report-unused-disable-directives .",
|
||||
"lint:css": "stylelint **/*.scss",
|
||||
"lint:format": "prettier --check '**/?(.)*.{css,scss,js,ts,tsx,json,yaml,yml}'",
|
||||
"lint": "npm run lint:js && npm run lint:css && npm run lint:format",
|
||||
|
|
|
@ -10,6 +10,17 @@
|
|||
"allowJs": true,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"files": [
|
||||
"client/src/index.ts",
|
||||
"client/src/custom.d.ts",
|
||||
"client/storybook/stories.d.ts"
|
||||
],
|
||||
"include": ["src", "wagtail"],
|
||||
"exclude": ["mode_modules", "static"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"static",
|
||||
// Files with template syntax.
|
||||
"wagtail/admin/templates/wagtailadmin/edit_handlers/inline_panel.js",
|
||||
"wagtail/contrib/search_promotions/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import TemplatePattern from '../../../../../client/storybook/TemplatePattern';
|
||||
|
||||
import template from './animated_logo.html';
|
||||
|
||||
export default {
|
||||
parameters: {
|
||||
docs: {
|
||||
source: { code: template },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AnimatedLogo = () => <TemplatePattern filename={__filename} />;
|
|
@ -0,0 +1 @@
|
|||
The breadcrumb component is reused across a lot of Wagtail’s headers.
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import TemplatePattern from '../../../../../client/storybook/TemplatePattern';
|
||||
|
||||
import template from './breadcrumb.html';
|
||||
import docs from './breadcrumb.md';
|
||||
|
||||
export default {
|
||||
parameters: {
|
||||
docs: {
|
||||
source: { code: template },
|
||||
extractComponentDescription: () => docs,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => (
|
||||
<TemplatePattern filename={__filename} context={args} />
|
||||
);
|
||||
|
||||
export const Base = Template.bind({});
|
||||
|
||||
Base.args = {
|
||||
pages: [
|
||||
{
|
||||
is_root: true,
|
||||
id: 2,
|
||||
get_admin_display_title: 'First item',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
get_admin_display_title: 'Second item',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
get_admin_display_title: 'Third item',
|
||||
},
|
||||
],
|
||||
trailing_arrow: true,
|
||||
show_header_buttons: false,
|
||||
};
|
|
@ -3,20 +3,16 @@
|
|||
|
||||
Variables accepted by this template:
|
||||
|
||||
title
|
||||
subtitle
|
||||
|
||||
search_url - if present, display a search box. This is a URL route name (taking no parameters) to be used as the action for that search box
|
||||
query_parameters - a query string (without the '?') to be placed after the search URL
|
||||
|
||||
icon - name of an icon to place against the title
|
||||
|
||||
tabbed - if true, add the classname 'tab-merged'
|
||||
merged - if true, add the classname 'merged'
|
||||
|
||||
action_url - if present, display an 'action' button. This is the URL to be used as the link URL for the button
|
||||
action_text - text for the 'action' button
|
||||
action_icon - icon for the 'action' button, default is 'icon-plus'
|
||||
- `title`
|
||||
- `subtitle`
|
||||
- `search_url` - if present, display a search box. This is a URL route name (taking no parameters) to be used as the action for that search box
|
||||
- `query_parameters` - a query string (without the '?') to be placed after the search URL
|
||||
- `icon` - name of an icon to place against the title
|
||||
- `tabbed` - if true, add the classname 'tab-merged'
|
||||
- `merged` - if true, add the classname 'merged'
|
||||
- `action_url` - if present, display an 'action' button. This is the URL to be used as the link URL for the button
|
||||
- `action_text` - text for the 'action' button
|
||||
- `action_icon` - icon for the 'action' button, default is 'icon-plus'
|
||||
|
||||
{% endcomment %}
|
||||
<header class="{% if merged %}merged{% endif %} {% if tabbed %}tab-merged{% endif %} {% if search_form %}hasform{% endif %}">
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import TemplatePattern from '../../../../../client/storybook/TemplatePattern';
|
||||
|
||||
import template from './header.html';
|
||||
|
||||
export default {
|
||||
parameters: {
|
||||
docs: {
|
||||
source: { code: template },
|
||||
// Trial generating documentation from comment within the template. To be replaced by a better pattern.
|
||||
extractComponentDescription: () =>
|
||||
template
|
||||
.match(/{% comment %}\n((.|\n)+){% endcomment %}/m)[1]
|
||||
.replace(/ {4}/g, ''),
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
icon: {
|
||||
options: window.WAGTAIL_ICONS,
|
||||
control: { type: 'select' },
|
||||
description: 'name of an icon to place against the title',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => (
|
||||
<TemplatePattern filename={__filename} context={args} />
|
||||
);
|
||||
|
||||
export const Base = Template.bind({});
|
||||
|
||||
Base.args = {
|
||||
title: 'Calendar',
|
||||
icon: 'date',
|
||||
};
|
||||
|
||||
export const Action = Template.bind({});
|
||||
|
||||
Action.args = {
|
||||
title: 'Users',
|
||||
subtitle: 'Editors',
|
||||
icon: 'user',
|
||||
action_url: '/test/',
|
||||
action_text: 'Add',
|
||||
};
|
|
@ -1,52 +1,49 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { renderPattern } from 'storybook-django';
|
||||
|
||||
const fetchIconTemplate = () =>
|
||||
renderPattern(
|
||||
window.PATTERN_LIBRARY_API,
|
||||
'wagtailadmin/shared/icon.html',
|
||||
{
|
||||
name: '__icon__',
|
||||
},
|
||||
{},
|
||||
).then((res) => res.text());
|
||||
import { getTemplatePattern } from '../../../../../client/storybook/TemplatePattern';
|
||||
|
||||
/**
|
||||
* Displays all icons within our sprite.
|
||||
*/
|
||||
const Icons = () => {
|
||||
const Icons = ({ color }: { color: string }) => {
|
||||
const [template, setTemplate] = useState<string>('');
|
||||
const [icons, setIcons] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const sprite = document.querySelector('[data-sprite]');
|
||||
if (sprite) {
|
||||
const symbols = Array.from(sprite.querySelectorAll('symbol'));
|
||||
setIcons(symbols.map((s) => s.id.replace('icon-', '')));
|
||||
}
|
||||
|
||||
fetchIconTemplate().then((html) => setTemplate(html));
|
||||
getTemplatePattern(
|
||||
'wagtailadmin/shared/icon.html',
|
||||
{ name: '__icon__' },
|
||||
{},
|
||||
(html) => setTemplate(html),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{icons.map((icon) => (
|
||||
<li key={icon}>
|
||||
<>
|
||||
{window.WAGTAIL_ICONS.map((icon) => (
|
||||
<div key={icon}>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: template.replace(/__icon__/g, icon),
|
||||
__html: template
|
||||
.replace(/__icon__/g, icon)
|
||||
.replace(/<svg/, `<svg style="fill: ${color};"`),
|
||||
}}
|
||||
/>
|
||||
<code>{`{% icon name="${icon}" %}`}</code>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Shared / Icons',
|
||||
component: Icons,
|
||||
argTypes: {
|
||||
color: {
|
||||
description: 'Only intended for demo purposes',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const All = () => <Icons />;
|
||||
export const icons = (args) => <Icons {...args} />;
|
||||
|
||||
icons.args = {
|
||||
color: 'currentColor',
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ from wagtail import __semver__
|
|||
class assets_mixin:
|
||||
def compile_assets(self):
|
||||
try:
|
||||
subprocess.check_call(["npm", "run", "dist"])
|
||||
subprocess.check_call(["npm", "run", "build"])
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
print("Error compiling assets: " + str(e)) # noqa
|
||||
raise SystemExit(1)
|
||||
|
|
Ładowanie…
Reference in New Issue