mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-29 08:57:03 +02:00
feat(pages): add LastUpdateAuthor & LastUpdateTime & editUrl (#10032)
Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com> Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
parent
e4ecffe418
commit
d1590e37ac
12 changed files with 254 additions and 8 deletions
|
@ -14,9 +14,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
|
|||
},
|
||||
{
|
||||
"description": "Markdown index page",
|
||||
"editUrl": undefined,
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
},
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/hello/",
|
||||
"source": "@site/src/pages/hello/index.md",
|
||||
"title": "Index",
|
||||
|
@ -25,11 +28,14 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
|
|||
},
|
||||
{
|
||||
"description": "my MDX page",
|
||||
"editUrl": undefined,
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
"description": "my MDX page",
|
||||
"title": "MDX page",
|
||||
},
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/hello/mdxPage",
|
||||
"source": "@site/src/pages/hello/mdxPage.mdx",
|
||||
"title": "MDX page",
|
||||
|
@ -43,9 +49,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
|
|||
},
|
||||
{
|
||||
"description": "translated Markdown page",
|
||||
"editUrl": undefined,
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
},
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/hello/translatedMd",
|
||||
"source": "@site/src/pages/hello/translatedMd.md",
|
||||
"title": undefined,
|
||||
|
@ -74,9 +83,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
|
|||
},
|
||||
{
|
||||
"description": "Markdown index page",
|
||||
"editUrl": undefined,
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
},
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/fr/hello/",
|
||||
"source": "@site/src/pages/hello/index.md",
|
||||
"title": "Index",
|
||||
|
@ -85,11 +97,14 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
|
|||
},
|
||||
{
|
||||
"description": "my MDX page",
|
||||
"editUrl": undefined,
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
"description": "my MDX page",
|
||||
"title": "MDX page",
|
||||
},
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/fr/hello/mdxPage",
|
||||
"source": "@site/src/pages/hello/mdxPage.mdx",
|
||||
"title": "MDX page",
|
||||
|
@ -103,9 +118,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
|
|||
},
|
||||
{
|
||||
"description": "translated Markdown page (fr)",
|
||||
"editUrl": undefined,
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
},
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/fr/hello/translatedMd",
|
||||
"source": "@site/i18n/fr/docusaurus-plugin-content-pages/hello/translatedMd.md",
|
||||
"title": undefined,
|
||||
|
@ -119,3 +137,72 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
|
|||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`docusaurus-plugin-content-pages loads simple pages with last update 1`] = `
|
||||
[
|
||||
{
|
||||
"permalink": "/",
|
||||
"source": "@site/src/pages/index.js",
|
||||
"type": "jsx",
|
||||
},
|
||||
{
|
||||
"permalink": "/typescript",
|
||||
"source": "@site/src/pages/typescript.tsx",
|
||||
"type": "jsx",
|
||||
},
|
||||
{
|
||||
"description": "Markdown index page",
|
||||
"editUrl": "url placeholder",
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
},
|
||||
"lastUpdatedAt": 1539502055000,
|
||||
"lastUpdatedBy": "Author",
|
||||
"permalink": "/hello/",
|
||||
"source": "@site/src/pages/hello/index.md",
|
||||
"title": "Index",
|
||||
"type": "mdx",
|
||||
"unlisted": false,
|
||||
},
|
||||
{
|
||||
"description": "my MDX page",
|
||||
"editUrl": "url placeholder",
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
"description": "my MDX page",
|
||||
"title": "MDX page",
|
||||
},
|
||||
"lastUpdatedAt": 1539502055000,
|
||||
"lastUpdatedBy": "Author",
|
||||
"permalink": "/hello/mdxPage",
|
||||
"source": "@site/src/pages/hello/mdxPage.mdx",
|
||||
"title": "MDX page",
|
||||
"type": "mdx",
|
||||
"unlisted": false,
|
||||
},
|
||||
{
|
||||
"permalink": "/hello/translatedJs",
|
||||
"source": "@site/src/pages/hello/translatedJs.js",
|
||||
"type": "jsx",
|
||||
},
|
||||
{
|
||||
"description": "translated Markdown page",
|
||||
"editUrl": "url placeholder",
|
||||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
},
|
||||
"lastUpdatedAt": 1539502055000,
|
||||
"lastUpdatedBy": "Author",
|
||||
"permalink": "/hello/translatedMd",
|
||||
"source": "@site/src/pages/hello/translatedMd.md",
|
||||
"title": undefined,
|
||||
"type": "mdx",
|
||||
"unlisted": false,
|
||||
},
|
||||
{
|
||||
"permalink": "/hello/world",
|
||||
"source": "@site/src/pages/hello/world.js",
|
||||
"type": "jsx",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -46,4 +46,24 @@ describe('docusaurus-plugin-content-pages', () => {
|
|||
|
||||
expect(pagesMetadata).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('loads simple pages with last update', async () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
const context = await loadContext({siteDir});
|
||||
const plugin = pluginContentPages(
|
||||
context,
|
||||
validateOptions({
|
||||
validate: normalizePluginOptions,
|
||||
options: {
|
||||
path: 'src/pages',
|
||||
editUrl: () => 'url placeholder',
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const pagesMetadata = await plugin.loadContent!();
|
||||
|
||||
expect(pagesMetadata).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
FrontMatterTOCHeadingLevels,
|
||||
ContentVisibilitySchema,
|
||||
URISchema,
|
||||
FrontMatterLastUpdateSchema,
|
||||
} from '@docusaurus/utils-validation';
|
||||
import type {PageFrontMatter} from '@docusaurus/plugin-content-pages';
|
||||
|
||||
|
@ -24,6 +25,7 @@ const PageFrontMatterSchema = Joi.object<PageFrontMatter>({
|
|||
wrapperClassName: Joi.string(),
|
||||
hide_table_of_contents: Joi.boolean(),
|
||||
...FrontMatterTOCHeadingLevels,
|
||||
last_update: FrontMatterLastUpdateSchema,
|
||||
}).concat(ContentVisibilitySchema);
|
||||
|
||||
export function validatePageFrontMatter(frontMatter: {
|
||||
|
|
|
@ -23,6 +23,9 @@ import {
|
|||
parseMarkdownFile,
|
||||
isUnlisted,
|
||||
isDraft,
|
||||
readLastUpdateData,
|
||||
getEditUrl,
|
||||
posixPath,
|
||||
} from '@docusaurus/utils';
|
||||
import {validatePageFrontMatter} from './frontMatter';
|
||||
import type {LoadContext, Plugin, RouteMetadata} from '@docusaurus/types';
|
||||
|
@ -45,7 +48,8 @@ export default function pluginContentPages(
|
|||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<LoadedContent | null> {
|
||||
const {siteConfig, siteDir, generatedFilesDir, localizationDir} = context;
|
||||
const {siteConfig, siteDir, generatedFilesDir, localizationDir, i18n} =
|
||||
context;
|
||||
|
||||
const contentPaths: PagesContentPaths = {
|
||||
contentPath: path.resolve(siteDir, options.path),
|
||||
|
@ -73,7 +77,7 @@ export default function pluginContentPages(
|
|||
},
|
||||
|
||||
async loadContent() {
|
||||
const {include} = options;
|
||||
const {include, editUrl} = options;
|
||||
|
||||
if (!(await fs.pathExists(contentPaths.contentPath))) {
|
||||
return null;
|
||||
|
@ -120,6 +124,50 @@ export default function pluginContentPages(
|
|||
});
|
||||
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
|
||||
|
||||
const pagesDirPath = await getFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
relativeSource,
|
||||
);
|
||||
|
||||
const pagesSourceAbsolute = path.join(pagesDirPath, relativeSource);
|
||||
|
||||
function getPagesEditUrl() {
|
||||
const pagesPathRelative = path.relative(
|
||||
pagesDirPath,
|
||||
path.resolve(pagesSourceAbsolute),
|
||||
);
|
||||
|
||||
if (typeof editUrl === 'function') {
|
||||
return editUrl({
|
||||
pagesDirPath: posixPath(path.relative(siteDir, pagesDirPath)),
|
||||
pagesPath: posixPath(pagesPathRelative),
|
||||
permalink,
|
||||
locale: i18n.currentLocale,
|
||||
});
|
||||
} else if (typeof editUrl === 'string') {
|
||||
const isLocalized =
|
||||
pagesDirPath === contentPaths.contentPathLocalized;
|
||||
const fileContentPath =
|
||||
isLocalized && options.editLocalizedFiles
|
||||
? contentPaths.contentPathLocalized
|
||||
: contentPaths.contentPath;
|
||||
|
||||
const contentPathEditUrl = normalizeUrl([
|
||||
editUrl,
|
||||
posixPath(path.relative(siteDir, fileContentPath)),
|
||||
]);
|
||||
|
||||
return getEditUrl(pagesPathRelative, contentPathEditUrl);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lastUpdatedData = await readLastUpdateData(
|
||||
source,
|
||||
options,
|
||||
frontMatter.last_update,
|
||||
);
|
||||
|
||||
if (isDraft({frontMatter})) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -132,6 +180,9 @@ export default function pluginContentPages(
|
|||
title: frontMatter.title ?? contentTitle,
|
||||
description: frontMatter.description ?? excerpt,
|
||||
frontMatter,
|
||||
lastUpdatedBy: lastUpdatedData.lastUpdatedBy,
|
||||
lastUpdatedAt: lastUpdatedData.lastUpdatedAt,
|
||||
editUrl: getPagesEditUrl(),
|
||||
unlisted,
|
||||
};
|
||||
}
|
||||
|
@ -160,12 +211,12 @@ export default function pluginContentPages(
|
|||
const {addRoute, createData} = actions;
|
||||
|
||||
function createPageRouteMetadata(metadata: Metadata): RouteMetadata {
|
||||
const lastUpdatedAt =
|
||||
metadata.type === 'mdx' ? metadata.lastUpdatedAt : undefined;
|
||||
|
||||
return {
|
||||
sourceFilePath: aliasedSitePathToRelativePath(metadata.source),
|
||||
// TODO add support for last updated date in the page plugin
|
||||
// at least for Markdown files
|
||||
// lastUpdatedAt: metadata.lastUpdatedAt,
|
||||
lastUpdatedAt: undefined,
|
||||
lastUpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
RehypePluginsSchema,
|
||||
AdmonitionsSchema,
|
||||
RouteBasePathSchema,
|
||||
URISchema,
|
||||
} from '@docusaurus/utils-validation';
|
||||
import {GlobExcludeDefault} from '@docusaurus/utils';
|
||||
import type {OptionValidationContext} from '@docusaurus/types';
|
||||
|
@ -27,6 +28,9 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
|||
beforeDefaultRehypePlugins: [],
|
||||
beforeDefaultRemarkPlugins: [],
|
||||
admonitions: true,
|
||||
showLastUpdateTime: false,
|
||||
showLastUpdateAuthor: false,
|
||||
editLocalizedFiles: false,
|
||||
};
|
||||
|
||||
const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||
|
@ -44,6 +48,12 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
|
|||
DEFAULT_OPTIONS.beforeDefaultRemarkPlugins,
|
||||
),
|
||||
admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
|
||||
showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime),
|
||||
showLastUpdateAuthor: Joi.bool().default(
|
||||
DEFAULT_OPTIONS.showLastUpdateAuthor,
|
||||
),
|
||||
editUrl: Joi.alternatives().try(URISchema, Joi.function()),
|
||||
editLocalizedFiles: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedFiles),
|
||||
});
|
||||
|
||||
export function validateOptions({
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
declare module '@docusaurus/plugin-content-pages' {
|
||||
import type {MDXOptions} from '@docusaurus/mdx-loader';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {FrontMatterLastUpdate, LastUpdateData} from '@docusaurus/utils';
|
||||
|
||||
export type Assets = {
|
||||
image?: string;
|
||||
|
@ -20,6 +21,10 @@ declare module '@docusaurus/plugin-content-pages' {
|
|||
include: string[];
|
||||
exclude: string[];
|
||||
mdxPageComponent: string;
|
||||
showLastUpdateTime: boolean;
|
||||
showLastUpdateAuthor: boolean;
|
||||
editUrl?: string | EditUrlFunction;
|
||||
editLocalizedFiles?: boolean;
|
||||
};
|
||||
|
||||
export type Options = Partial<PluginOptions>;
|
||||
|
@ -35,6 +40,7 @@ declare module '@docusaurus/plugin-content-pages' {
|
|||
readonly toc_max_heading_level?: number;
|
||||
readonly draft?: boolean;
|
||||
readonly unlisted?: boolean;
|
||||
readonly last_update?: FrontMatterLastUpdate;
|
||||
};
|
||||
|
||||
export type JSXPageMetadata = {
|
||||
|
@ -43,16 +49,31 @@ declare module '@docusaurus/plugin-content-pages' {
|
|||
source: string;
|
||||
};
|
||||
|
||||
export type MDXPageMetadata = {
|
||||
export type MDXPageMetadata = LastUpdateData & {
|
||||
type: 'mdx';
|
||||
permalink: string;
|
||||
source: string;
|
||||
frontMatter: PageFrontMatter & {[key: string]: unknown};
|
||||
editUrl?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
unlisted: boolean;
|
||||
};
|
||||
|
||||
export type EditUrlFunction = (editUrlParams: {
|
||||
/**
|
||||
* The root content directory containing this post file, relative to the
|
||||
* site path. Usually the same as `options.path` but can be localized
|
||||
*/
|
||||
pagesDirPath: string;
|
||||
/** Path to this pages file, relative to `pagesDirPath`. */
|
||||
pagesPath: string;
|
||||
/** @see {@link PagesPostMetadata.permalink} */
|
||||
permalink: string;
|
||||
/** Locale name. */
|
||||
locale: string;
|
||||
}) => string | undefined;
|
||||
|
||||
export type Metadata = JSXPageMetadata | MDXPageMetadata;
|
||||
|
||||
export type LoadedContent = Metadata[];
|
||||
|
|
|
@ -18,12 +18,21 @@ import TOC from '@theme/TOC';
|
|||
import Unlisted from '@theme/Unlisted';
|
||||
import type {Props} from '@theme/MDXPage';
|
||||
|
||||
import EditMetaRow from '@theme/EditMetaRow';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function MDXPage(props: Props): JSX.Element {
|
||||
const {content: MDXPageContent} = props;
|
||||
const {
|
||||
metadata: {title, description, frontMatter, unlisted},
|
||||
metadata: {
|
||||
title,
|
||||
editUrl,
|
||||
description,
|
||||
frontMatter,
|
||||
unlisted,
|
||||
lastUpdatedBy,
|
||||
lastUpdatedAt,
|
||||
},
|
||||
assets,
|
||||
} = MDXPageContent;
|
||||
const {
|
||||
|
@ -33,6 +42,8 @@ export default function MDXPage(props: Props): JSX.Element {
|
|||
} = frontMatter;
|
||||
const image = assets.image ?? frontMatter.image;
|
||||
|
||||
const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy);
|
||||
|
||||
return (
|
||||
<HtmlClassNameProvider
|
||||
className={clsx(
|
||||
|
@ -55,6 +66,17 @@ export default function MDXPage(props: Props): JSX.Element {
|
|||
<MDXPageContent />
|
||||
</MDXContent>
|
||||
</article>
|
||||
{canDisplayEditMetaRow && (
|
||||
<EditMetaRow
|
||||
className={clsx(
|
||||
'margin-top--sm',
|
||||
ThemeClassNames.pages.pageFooterEditMetaRow,
|
||||
)}
|
||||
editUrl={editUrl}
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
lastUpdatedBy={lastUpdatedBy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!hideTableOfContents && MDXPageContent.toc.length > 0 && (
|
||||
<div className="col col--2">
|
||||
|
|
|
@ -76,4 +76,7 @@ export const ThemeClassNames = {
|
|||
blogFooterTagsRow: 'theme-blog-footer-tags-row',
|
||||
blogFooterEditMetaRow: 'theme-blog-footer-edit-meta-row',
|
||||
},
|
||||
pages: {
|
||||
pageFooterEditMetaRow: 'theme-pages-footer-edit-meta-row',
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -224,6 +224,7 @@ opensearch
|
|||
opensearchdescription
|
||||
opensource
|
||||
optimizt
|
||||
Orama
|
||||
orama
|
||||
Orta
|
||||
orta
|
||||
|
|
|
@ -90,6 +90,10 @@ export const dogfoodingPluginInstances: PluginConfig[] = [
|
|||
id: 'pages-tests',
|
||||
path: '_dogfooding/_pages tests',
|
||||
routeBasePath: '/tests/pages',
|
||||
showLastUpdateTime: true,
|
||||
showLastUpdateAuthor: true,
|
||||
editUrl: ({pagesPath}) =>
|
||||
`https://github.com/facebook/docusaurus/edit/main/website/_dogfooding/_pages tests/${pagesPath}`,
|
||||
} satisfies PageOptions,
|
||||
],
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ Accepted fields:
|
|||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | `string` | `'src/pages'` | Path to data on filesystem relative to site dir. Components in this directory will be automatically converted to pages. |
|
||||
| `editUrl` | <code>string \| [EditUrlFn](#EditUrlFn)</code> | `undefined` | **Only for Markdown pages**. Base URL to edit your site. The final URL is computed by `editUrl + relativePostPath`. Using a function allows more nuanced control for each file. Omitting this variable entirely will disable edit links. |
|
||||
| `editLocalizedFiles` | `boolean` | `false` | **Only for Markdown pages**. The edit URL will target the localized file, instead of the original unlocalized file. Ignored when `editUrl` is a function. |
|
||||
| `routeBasePath` | `string` | `'/'` | URL route for the pages section of your site. **DO NOT** include a trailing slash. |
|
||||
| `include` | `string[]` | `['**/*.{js,jsx,ts,tsx,md,mdx}']` | Matching files will be included and processed. |
|
||||
| `exclude` | `string[]` | _See example configuration_ | No route will be created for matching files. |
|
||||
|
@ -42,11 +44,26 @@ Accepted fields:
|
|||
| `rehypePlugins` | `[]` | `any[]` | Rehype plugins passed to MDX. |
|
||||
| `beforeDefaultRemarkPlugins` | `any[]` | `[]` | Custom Remark plugins passed to MDX before the default Docusaurus Remark plugins. |
|
||||
| `beforeDefaultRehypePlugins` | `any[]` | `[]` | Custom Rehype plugins passed to MDX before the default Docusaurus Rehype plugins. |
|
||||
| `showLastUpdateAuthor` | `boolean` | `false` | **Only for Markdown pages**. Whether to display the author who last updated the page. |
|
||||
| `showLastUpdateTime` | `boolean` | `false` | **Only for Markdown pages**. Whether to display the last date the page post was updated. This requires access to git history during the build, so will not work correctly with shallow clones (a common default for CI systems). With GitHub `actions/checkout`, use`fetch-depth: 0`. |
|
||||
|
||||
```mdx-code-block
|
||||
</APITable>
|
||||
```
|
||||
|
||||
### Types {#types}
|
||||
|
||||
#### `EditUrlFn` {#EditUrlFn}
|
||||
|
||||
```ts
|
||||
type EditUrlFunction = (params: {
|
||||
blogDirPath: string;
|
||||
blogPath: string;
|
||||
permalink: string;
|
||||
locale: string;
|
||||
}) => string | undefined;
|
||||
```
|
||||
|
||||
### Example configuration {#ex-config}
|
||||
|
||||
You can configure this plugin through preset options or plugin options.
|
||||
|
|
|
@ -464,6 +464,14 @@ export default async function createConfigAsync() {
|
|||
} satisfies BlogOptions,
|
||||
pages: {
|
||||
remarkPlugins: [npm2yarn],
|
||||
editUrl: ({locale, pagesPath}) => {
|
||||
if (locale !== defaultLocale) {
|
||||
return `https://crowdin.com/project/docusaurus-v2/${locale}`;
|
||||
}
|
||||
return `https://github.com/facebook/docusaurus/edit/main/website/src/pages/${pagesPath}`;
|
||||
},
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
} satisfies PageOptions,
|
||||
theme: {
|
||||
customCss: [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue