654 lines
21 KiB
TypeScript
654 lines
21 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import {
|
|
Table,
|
|
Button,
|
|
Input,
|
|
Select,
|
|
Tag,
|
|
Space,
|
|
Modal,
|
|
Drawer,
|
|
Form,
|
|
Popconfirm,
|
|
message,
|
|
Row,
|
|
Col,
|
|
Radio,
|
|
Checkbox,
|
|
Divider,
|
|
Grid,
|
|
} from 'antd';
|
|
import {
|
|
PlusOutlined,
|
|
EditOutlined,
|
|
DeleteOutlined,
|
|
SearchOutlined,
|
|
EyeOutlined,
|
|
SettingOutlined,
|
|
SyncOutlined,
|
|
BuildOutlined,
|
|
ExclamationCircleOutlined,
|
|
QrcodeOutlined,
|
|
} from '@ant-design/icons';
|
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
|
import dayjs from 'dayjs';
|
|
import { useOutletContext, useLocation } from 'react-router-dom';
|
|
import { api } from '@/lib/api';
|
|
import { useMkDocsBuild } from '@/hooks/useMkDocsBuild';
|
|
import LandingPageEditor from '@/components/landing-pages/LandingPageEditor';
|
|
import QrCodeModal from '@/components/QrCodeModal';
|
|
import type { LandingPage, EditorMode, LandingPagesListResponse, LandingPagesListParams, AppOutletContext } from '@/types/api';
|
|
|
|
const { TextArea } = Input;
|
|
|
|
const publishedOptions = [
|
|
{ value: 'true', label: 'Published' },
|
|
{ value: 'false', label: 'Draft' },
|
|
];
|
|
|
|
export default function LandingPagesPage() {
|
|
const { building, confirmAndBuild, isSuperAdmin } = useMkDocsBuild();
|
|
const { setPageHeader } = useOutletContext<AppOutletContext>();
|
|
const location = useLocation();
|
|
const [pages, setPages] = useState<LandingPage[]>([]);
|
|
const [pagination, setPagination] = useState({ page: 1, limit: 20, total: 0, totalPages: 0 });
|
|
const [loading, setLoading] = useState(false);
|
|
const screens = Grid.useBreakpoint();
|
|
const isMobile = !screens.md;
|
|
const [syncing, setSyncing] = useState(false);
|
|
const [validating, setValidating] = useState(false);
|
|
const [search, setSearch] = useState('');
|
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
const searchTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
const [publishedFilter, setPublishedFilter] = useState<'true' | 'false' | undefined>();
|
|
const [createDrawerOpen, setCreateDrawerOpen] = useState(false);
|
|
const [settingsDrawerOpen, setSettingsDrawerOpen] = useState(false);
|
|
const [editingPage, setEditingPage] = useState<LandingPage | null>(null);
|
|
const [editingPageId, setEditingPageId] = useState<string | null>(null);
|
|
const [qrPage, setQrPage] = useState<LandingPage | null>(null);
|
|
const [viewCounts, setViewCounts] = useState<Record<string, number>>({});
|
|
const [createForm] = Form.useForm();
|
|
const [settingsForm] = Form.useForm();
|
|
|
|
const handleSearchChange = (value: string) => {
|
|
setSearch(value);
|
|
clearTimeout(searchTimerRef.current);
|
|
searchTimerRef.current = setTimeout(() => setDebouncedSearch(value), 300);
|
|
};
|
|
|
|
useEffect(() => {
|
|
return () => clearTimeout(searchTimerRef.current);
|
|
}, []);
|
|
|
|
// Handle navigation state from command palette — auto-open editor for a page
|
|
useEffect(() => {
|
|
const editPageId = (location.state as { editPageId?: string } | null)?.editPageId;
|
|
if (!editPageId) return;
|
|
setEditingPageId(editPageId);
|
|
window.history.replaceState({}, '');
|
|
}, [location.state]);
|
|
|
|
const fetchPages = useCallback(async (params?: LandingPagesListParams) => {
|
|
setLoading(true);
|
|
try {
|
|
const { data } = await api.get<LandingPagesListResponse>('/pages', {
|
|
params: {
|
|
page: params?.page ?? pagination.page,
|
|
limit: params?.limit ?? pagination.limit,
|
|
search: params?.search ?? (debouncedSearch || undefined),
|
|
published: params?.published ?? publishedFilter,
|
|
},
|
|
});
|
|
setPages(data.pages);
|
|
setPagination(data.pagination);
|
|
} catch {
|
|
message.error('Failed to load pages');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [pagination.page, pagination.limit, debouncedSearch, publishedFilter]);
|
|
|
|
useEffect(() => {
|
|
fetchPages({ page: 1 });
|
|
}, [debouncedSearch, publishedFilter]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
useEffect(() => {
|
|
api.get<Record<string, number>>('/pages/view-counts')
|
|
.then(({ data }) => setViewCounts(data))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const handleTableChange = (pag: TablePaginationConfig) => {
|
|
fetchPages({ page: pag.current ?? 1, limit: pag.pageSize ?? 20 });
|
|
};
|
|
|
|
const handleCreate = async (values: { title: string; description?: string; editorMode?: EditorMode }) => {
|
|
try {
|
|
const { data } = await api.post<LandingPage>('/pages', values);
|
|
message.success('Page created');
|
|
setCreateDrawerOpen(false);
|
|
createForm.resetFields();
|
|
setEditingPageId(data.id);
|
|
} catch (err: unknown) {
|
|
const msg =
|
|
(err as { response?: { data?: { error?: { message?: string } } } })
|
|
?.response?.data?.error?.message || 'Failed to create page';
|
|
message.error(msg);
|
|
}
|
|
};
|
|
|
|
const handleSyncOverrides = async () => {
|
|
setSyncing(true);
|
|
try {
|
|
const { data } = await api.post<{ imported: number; updated: number; stubs: number }>('/pages/sync');
|
|
if (data.imported > 0 || data.updated > 0 || data.stubs > 0) {
|
|
message.success(`Synced: ${data.imported} imported, ${data.updated} updated, ${data.stubs} stubs created`);
|
|
fetchPages();
|
|
} else {
|
|
message.info('No new overrides to sync');
|
|
}
|
|
} catch {
|
|
message.error('Failed to sync overrides');
|
|
} finally {
|
|
setSyncing(false);
|
|
}
|
|
};
|
|
|
|
const handleValidateExports = async () => {
|
|
setValidating(true);
|
|
try {
|
|
const { data } = await api.post<{validated: number; repaired: number; errors: Array<{pageId: string; slug: string; error: string}>}>('/pages/validate');
|
|
if (data.repaired > 0 || data.errors.length > 0) {
|
|
const msg = `Validated ${data.validated} pages: ${data.repaired} repaired`;
|
|
data.errors.length > 0 ? message.warning(`${msg}, ${data.errors.length} errors`) : message.success(msg);
|
|
fetchPages();
|
|
} else {
|
|
message.info(`Validated ${data.validated} pages - all OK`);
|
|
}
|
|
} catch {
|
|
message.error('Failed to validate exports');
|
|
} finally {
|
|
setValidating(false);
|
|
}
|
|
};
|
|
|
|
const handleSettingsSave = async (values: Record<string, unknown>) => {
|
|
if (!editingPage) return;
|
|
try {
|
|
await api.put(`/pages/${editingPage.id}`, values);
|
|
message.success('Page settings updated');
|
|
setSettingsDrawerOpen(false);
|
|
setEditingPage(null);
|
|
settingsForm.resetFields();
|
|
fetchPages();
|
|
} catch (err: unknown) {
|
|
const msg =
|
|
(err as { response?: { data?: { error?: { message?: string } } } })
|
|
?.response?.data?.error?.message || 'Failed to update page';
|
|
message.error(msg);
|
|
}
|
|
};
|
|
|
|
const handleTogglePublished = async (page: LandingPage) => {
|
|
try {
|
|
await api.put(`/pages/${page.id}`, { published: !page.published });
|
|
message.success(page.published ? 'Page unpublished' : 'Page published');
|
|
fetchPages();
|
|
} catch {
|
|
message.error('Failed to update page');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
try {
|
|
await api.delete(`/pages/${id}`);
|
|
message.success('Page deleted');
|
|
fetchPages();
|
|
} catch {
|
|
message.error('Failed to delete page');
|
|
}
|
|
};
|
|
|
|
// Critical MkDocs override pages that need a confirmation before editing
|
|
const CRITICAL_SLUGS = ['main', 'custom'];
|
|
|
|
const handleEditClick = (page: LandingPage) => {
|
|
if (CRITICAL_SLUGS.includes(page.slug)) {
|
|
Modal.confirm({
|
|
title: 'Edit critical MkDocs template?',
|
|
icon: <ExclamationCircleOutlined />,
|
|
content: (
|
|
<div>
|
|
<p>
|
|
<strong>{page.title}</strong> ({page.mkdocsPath || page.slug}) is a critical MkDocs
|
|
override template. Changes to this file affect the entire documentation site.
|
|
</p>
|
|
<p>Are you sure you want to edit it?</p>
|
|
</div>
|
|
),
|
|
okText: 'Edit',
|
|
okType: 'danger',
|
|
cancelText: 'Cancel',
|
|
onOk: () => setEditingPageId(page.id),
|
|
});
|
|
} else {
|
|
setEditingPageId(page.id);
|
|
}
|
|
};
|
|
|
|
const openSettings = (page: LandingPage) => {
|
|
setEditingPage(page);
|
|
settingsForm.setFieldsValue({
|
|
title: page.title,
|
|
description: page.description,
|
|
listed: page.listed ?? false,
|
|
mkdocsPath: page.mkdocsPath,
|
|
mkdocsExportMode: page.mkdocsExportMode,
|
|
mkdocsHideNav: page.mkdocsHideNav,
|
|
mkdocsHideToc: page.mkdocsHideToc,
|
|
mkdocsSkipExport: page.mkdocsSkipExport,
|
|
seoTitle: page.seoTitle,
|
|
seoDescription: page.seoDescription,
|
|
seoImage: page.seoImage,
|
|
});
|
|
setSettingsDrawerOpen(true);
|
|
};
|
|
|
|
const columns: ColumnsType<LandingPage> = [
|
|
{
|
|
title: 'Title',
|
|
dataIndex: 'title',
|
|
key: 'title',
|
|
render: (title: string, record: LandingPage) => (
|
|
<div>
|
|
<span style={{ fontWeight: 500 }}>{title}</span>
|
|
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.45)' }}>/p/{record.slug}</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: 'Editor',
|
|
dataIndex: 'editorMode',
|
|
key: 'editorMode',
|
|
render: (mode: EditorMode) => (
|
|
<Tag color={mode === 'VISUAL' ? 'green' : 'blue'}>
|
|
{mode === 'VISUAL' ? 'Visual' : 'Code'}
|
|
</Tag>
|
|
),
|
|
responsive: ['sm'],
|
|
},
|
|
{
|
|
title: 'Status',
|
|
dataIndex: 'published',
|
|
key: 'published',
|
|
render: (published: boolean, record: LandingPage) => (
|
|
<Space size={4}>
|
|
<Tag color={published ? 'green' : 'default'}>{published ? 'Published' : 'Draft'}</Tag>
|
|
{record.listed && <Tag color="blue">Listed</Tag>}
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: 'MkDocs',
|
|
dataIndex: 'mkdocsPath',
|
|
key: 'mkdocsPath',
|
|
render: (_: string | null, record: LandingPage) => (
|
|
<div>
|
|
<div>{record.mkdocsPath || '--'}</div>
|
|
{record.mkdocsStubPath && (
|
|
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.45)' }}>{record.mkdocsStubPath}</div>
|
|
)}
|
|
</div>
|
|
),
|
|
responsive: ['lg'],
|
|
},
|
|
{
|
|
title: 'Created',
|
|
dataIndex: 'createdAt',
|
|
key: 'createdAt',
|
|
render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
|
|
responsive: ['md'],
|
|
},
|
|
{
|
|
title: 'Updated',
|
|
dataIndex: 'updatedAt',
|
|
key: 'updatedAt',
|
|
render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
|
|
responsive: ['md'],
|
|
},
|
|
{
|
|
title: 'Views (30d)',
|
|
key: 'views',
|
|
render: (_: unknown, record: LandingPage) => viewCounts[record.slug] ?? 0,
|
|
responsive: ['md'],
|
|
},
|
|
{
|
|
title: 'Actions',
|
|
key: 'actions',
|
|
render: (_: unknown, record: LandingPage) => (
|
|
<Space>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => handleEditClick(record)}
|
|
title={record.editorMode === 'CODE' ? 'Edit code' : 'Edit in builder'}
|
|
/>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<SettingOutlined />}
|
|
onClick={() => openSettings(record)}
|
|
title="Page settings"
|
|
/>
|
|
{record.published && (
|
|
<>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<EyeOutlined />}
|
|
onClick={() => window.open(`/p/${record.slug}`, '_blank')}
|
|
title="View page"
|
|
/>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<QrcodeOutlined />}
|
|
onClick={() => setQrPage(record)}
|
|
title="QR code"
|
|
/>
|
|
</>
|
|
)}
|
|
{record.published ? (
|
|
<Popconfirm
|
|
title="Unpublish this page?"
|
|
description="It will no longer be publicly accessible."
|
|
onConfirm={() => handleTogglePublished(record)}
|
|
>
|
|
<Button type="link" size="small">
|
|
Unpublish
|
|
</Button>
|
|
</Popconfirm>
|
|
) : (
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
onClick={() => handleTogglePublished(record)}
|
|
>
|
|
Publish
|
|
</Button>
|
|
)}
|
|
<Popconfirm
|
|
title="Delete this page?"
|
|
description="This action cannot be undone."
|
|
onConfirm={() => handleDelete(record.id)}
|
|
>
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />} title="Delete" />
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
// Set fullBleed when editor is open, title when in list mode
|
|
useEffect(() => {
|
|
if (editingPageId) {
|
|
setPageHeader({ fullBleed: true });
|
|
} else {
|
|
setPageHeader({ title: 'Landing Pages' });
|
|
}
|
|
return () => setPageHeader(null);
|
|
}, [editingPageId, setPageHeader]);
|
|
|
|
// If editing a page, show the editor instead of the list
|
|
if (editingPageId) {
|
|
return (
|
|
<LandingPageEditor
|
|
pageId={editingPageId}
|
|
onClose={() => {
|
|
setEditingPageId(null);
|
|
fetchPages(); // Refresh table data
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const anyDrawerOpen = createDrawerOpen || settingsDrawerOpen;
|
|
const activeDrawerWidth = isMobile ? 0 : (createDrawerOpen ? 520 : settingsDrawerOpen ? 560 : 0);
|
|
|
|
return (
|
|
<>
|
|
<div style={{ marginRight: anyDrawerOpen ? activeDrawerWidth : 0, transition: 'margin-right 0.15s cubic-bezier(0.2, 0, 0, 1)' }}>
|
|
<Row justify="end" align="middle" style={{ marginBottom: 16 }}>
|
|
<Col>
|
|
<Space>
|
|
{isSuperAdmin && (
|
|
<Button
|
|
icon={<BuildOutlined />}
|
|
loading={building}
|
|
onClick={confirmAndBuild}
|
|
>
|
|
Build Site
|
|
</Button>
|
|
)}
|
|
<Button
|
|
icon={<SyncOutlined spin={syncing} />}
|
|
loading={syncing}
|
|
onClick={handleSyncOverrides}
|
|
>
|
|
Sync Overrides
|
|
</Button>
|
|
<Button
|
|
icon={<SyncOutlined spin={validating} />}
|
|
loading={validating}
|
|
onClick={handleValidateExports}
|
|
title="Validate MkDocs export files and repair if missing"
|
|
>
|
|
Validate Exports
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => setCreateDrawerOpen(true)}
|
|
>
|
|
Create Page
|
|
</Button>
|
|
</Space>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
|
|
<Col xs={24} sm={12} md={8}>
|
|
<Input
|
|
placeholder="Search by title or description"
|
|
prefix={<SearchOutlined />}
|
|
value={search}
|
|
onChange={(e) => handleSearchChange(e.target.value)}
|
|
allowClear
|
|
/>
|
|
</Col>
|
|
<Col xs={12} sm={7} md={4}>
|
|
<Select
|
|
placeholder="Status"
|
|
options={publishedOptions}
|
|
value={publishedFilter}
|
|
onChange={setPublishedFilter}
|
|
allowClear
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Table<LandingPage>
|
|
columns={columns}
|
|
dataSource={pages}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={{
|
|
current: pagination.page,
|
|
pageSize: pagination.limit,
|
|
total: pagination.total,
|
|
showSizeChanger: true,
|
|
showTotal: (total) => `${total} pages`,
|
|
}}
|
|
onChange={handleTableChange}
|
|
locale={{ emptyText: 'No landing pages yet. Create your first page to get started.' }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Create Drawer */}
|
|
<Drawer
|
|
title="Create Landing Page"
|
|
open={createDrawerOpen}
|
|
destroyOnHidden
|
|
mask={false}
|
|
width={isMobile ? '100%' : 520}
|
|
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
|
onClose={() => {
|
|
setCreateDrawerOpen(false);
|
|
createForm.resetFields();
|
|
}}
|
|
extra={
|
|
<Space>
|
|
<Button onClick={() => { setCreateDrawerOpen(false); createForm.resetFields(); }}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="primary" onClick={() => createForm.submit()}>
|
|
Create & Edit
|
|
</Button>
|
|
</Space>
|
|
}
|
|
>
|
|
<Form form={createForm} onFinish={handleCreate} layout="vertical" initialValues={{ editorMode: 'VISUAL' }}>
|
|
<Form.Item
|
|
name="title"
|
|
label="Title"
|
|
rules={[{ required: true, message: 'Title is required' }]}
|
|
>
|
|
<Input placeholder="e.g. Join Our Campaign" />
|
|
</Form.Item>
|
|
<Form.Item name="description" label="Description">
|
|
<TextArea rows={3} />
|
|
</Form.Item>
|
|
<Form.Item name="editorMode" label="Editor Mode">
|
|
<Radio.Group>
|
|
<Radio.Button value="VISUAL">Visual Editor</Radio.Button>
|
|
<Radio.Button value="CODE">Code Editor</Radio.Button>
|
|
</Radio.Group>
|
|
</Form.Item>
|
|
</Form>
|
|
</Drawer>
|
|
|
|
{/* QR Code Modal */}
|
|
{qrPage && (
|
|
<QrCodeModal
|
|
open={!!qrPage}
|
|
onClose={() => setQrPage(null)}
|
|
url={`${window.location.origin}/p/${qrPage.slug}`}
|
|
title={qrPage.title}
|
|
/>
|
|
)}
|
|
|
|
{/* Settings Drawer */}
|
|
<Drawer
|
|
title="Page Settings"
|
|
open={settingsDrawerOpen}
|
|
destroyOnHidden
|
|
mask={false}
|
|
width={isMobile ? '100%' : 560}
|
|
rootStyle={{ position: 'absolute', top: 64, height: 'calc(100vh - 64px)' }}
|
|
onClose={() => {
|
|
setSettingsDrawerOpen(false);
|
|
setEditingPage(null);
|
|
settingsForm.resetFields();
|
|
}}
|
|
extra={
|
|
<Space>
|
|
<Button onClick={() => { setSettingsDrawerOpen(false); setEditingPage(null); settingsForm.resetFields(); }}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="primary" onClick={() => settingsForm.submit()}>
|
|
Save
|
|
</Button>
|
|
</Space>
|
|
}
|
|
>
|
|
<Form form={settingsForm} onFinish={handleSettingsSave} layout="vertical">
|
|
<Form.Item
|
|
name="title"
|
|
label="Title"
|
|
rules={[{ required: true, message: 'Title is required' }]}
|
|
>
|
|
<Input placeholder="e.g. Join Our Campaign" />
|
|
</Form.Item>
|
|
<Form.Item name="description" label="Description">
|
|
<TextArea rows={2} />
|
|
</Form.Item>
|
|
<Form.Item name="seoTitle" label="SEO Title">
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="seoDescription" label="SEO Description">
|
|
<TextArea rows={2} />
|
|
</Form.Item>
|
|
<Form.Item name="seoImage" label="SEO Image URL">
|
|
<Input placeholder="https://..." />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="listed"
|
|
valuePropName="checked"
|
|
help="Show this page in the public /pages directory when published."
|
|
>
|
|
<Checkbox>List in Pages Index</Checkbox>
|
|
</Form.Item>
|
|
|
|
<Divider>MkDocs Integration</Divider>
|
|
|
|
<Form.Item
|
|
name="mkdocsSkipExport"
|
|
valuePropName="checked"
|
|
help="When enabled, this page will not be exported to MkDocs even when published. Use for pages that should only be accessible via /p/:slug."
|
|
>
|
|
<Checkbox>Skip MkDocs Export</Checkbox>
|
|
</Form.Item>
|
|
|
|
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.mkdocsSkipExport !== cur.mkdocsSkipExport}>
|
|
{({ getFieldValue }) =>
|
|
!getFieldValue('mkdocsSkipExport') && (
|
|
<>
|
|
<Form.Item name="mkdocsPath" label="Override Path">
|
|
<Input placeholder="e.g. about.html" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="mkdocsExportMode"
|
|
valuePropName="checked"
|
|
getValueFromEvent={(e: { target: { checked: boolean } }) => e.target.checked ? 'STANDALONE' : 'THEMED'}
|
|
getValueProps={(value: string) => ({ checked: value === 'STANDALONE' })}
|
|
help="Publish as a full HTML page with no MkDocs header, footer, or theme (like lander.html)"
|
|
>
|
|
<Checkbox>Full page MkDocs</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.mkdocsExportMode !== cur.mkdocsExportMode}>
|
|
{({ getFieldValue }) =>
|
|
getFieldValue('mkdocsExportMode') !== 'STANDALONE' && (
|
|
<>
|
|
<Form.Item name="mkdocsHideNav" valuePropName="checked">
|
|
<Checkbox>Hide navigation sidebar</Checkbox>
|
|
</Form.Item>
|
|
<Form.Item name="mkdocsHideToc" valuePropName="checked">
|
|
<Checkbox>Hide table of contents</Checkbox>
|
|
</Form.Item>
|
|
</>
|
|
)
|
|
}
|
|
</Form.Item>
|
|
</>
|
|
)
|
|
}
|
|
</Form.Item>
|
|
</Form>
|
|
</Drawer>
|
|
</>
|
|
);
|
|
}
|