This commit is contained in:
seb
2026-10-30 04:16:56 +01:00
parent 8b680b5a1a
commit dba9ae5cf0
10 changed files with 306 additions and 44 deletions

View File

@@ -12,4 +12,10 @@ export const thumb = {
return `/api/thumb/${encodeURIComponent(fullHash)}`; return `/api/thumb/${encodeURIComponent(fullHash)}`;
}, },
// #endregion // #endregion
// #region GET /api/smallthumb/:fullHash — smallUrl
smallUrl(fullHash) {
return `/api/smallthumb/${encodeURIComponent(fullHash)}`;
},
// #endregion
}; };

View File

@@ -31,7 +31,7 @@ class ArticleNameDialog extends Component {
super(props); super(props);
this.state = { this.state = {
mode: SINGLE_MODE, mode: SINGLE_MODE,
name: '', name: props.initialName ?? '',
bulkText: '', bulkText: '',
busy: false, busy: false,
error: null, error: null,
@@ -46,7 +46,7 @@ class ArticleNameDialog extends Component {
if (this.props.open && !prevProps.open) { if (this.props.open && !prevProps.open) {
this.setState({ this.setState({
mode: SINGLE_MODE, mode: SINGLE_MODE,
name: '', name: this.props.initialName ?? '',
bulkText: '', bulkText: '',
busy: false, busy: false,
error: null, error: null,

View File

@@ -9,14 +9,17 @@ import {
class ArticlePhoto extends Component { class ArticlePhoto extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { thumbLoaded: false }; this.state = { thumbLoaded: false, smallThumbError: false };
this.handleThumbLoad = this.handleThumbLoad.bind(this); this.handleThumbLoad = this.handleThumbLoad.bind(this);
this.handleThumbError = this.handleThumbError.bind(this); this.handleThumbError = this.handleThumbError.bind(this);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.photo?.fullHash !== this.props.photo?.fullHash) { if (
this.setState({ thumbLoaded: false }); prevProps.photo?.fullHash !== this.props.photo?.fullHash ||
prevProps.useSmallThumb !== this.props.useSmallThumb
) {
this.setState({ thumbLoaded: false, smallThumbError: false });
} }
} }
@@ -25,8 +28,12 @@ class ArticlePhoto extends Component {
} }
handleThumbError() { handleThumbError() {
if (this.props.useSmallThumb && !this.state.smallThumbError) {
this.setState({ smallThumbError: true, thumbLoaded: false });
} else {
this.setState({ thumbLoaded: false }); this.setState({ thumbLoaded: false });
} }
}
render() { render() {
const { const {
@@ -34,9 +41,11 @@ class ArticlePhoto extends Component {
size = 200, size = 200,
fill = false, fill = false,
objectFit = fill ? 'cover' : 'contain', objectFit = fill ? 'cover' : 'contain',
useSmallThumb = false,
sx, sx,
...rest
} = this.props; } = this.props;
const { thumbLoaded } = this.state; const { thumbLoaded, smallThumbError } = this.state;
const fullHash = photo?.fullHash; const fullHash = photo?.fullHash;
const hasPreview = photo?.thumbHash != null; const hasPreview = photo?.thumbHash != null;
const aspectRatio = const aspectRatio =
@@ -44,8 +53,15 @@ class ArticlePhoto extends Component {
? `${photo.thumbW} / ${photo.thumbH}` ? `${photo.thumbW} / ${photo.thumbH}`
: '1 / 1'; : '1 / 1';
const imgSrc = fullHash
? useSmallThumb && !smallThumbError
? thumb.smallUrl(fullHash)
: thumb.url(fullHash)
: null;
return ( return (
<Box <Box
{...rest}
sx={{ sx={{
position: 'relative', position: 'relative',
...(fill ...(fill
@@ -78,10 +94,10 @@ class ArticlePhoto extends Component {
<ThumbHashImage thumbHash={photo.thumbHash} /> <ThumbHashImage thumbHash={photo.thumbHash} />
</Box> </Box>
) : null} ) : null}
{fullHash ? ( {imgSrc ? (
<Box <Box
component="img" component="img"
src={thumb.url(fullHash)} src={imgSrc}
alt="" alt=""
onLoad={this.handleThumbLoad} onLoad={this.handleThumbLoad}
onError={this.handleThumbError} onError={this.handleThumbError}

View File

@@ -1,16 +1,27 @@
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import Box from '@mui/material/Box';
import { PanelTile } from 'dashgui/panels'; import { PanelTile } from 'dashgui/panels';
import { primaryPhoto } from '../../utils/articleDetail'; import { sortedPhotos, primaryPhoto } from '../../utils/articleDetail';
import ArticlePhoto from './ArticlePhoto'; import ArticlePhoto from './ArticlePhoto';
import ArticlePhotosDialog from './ArticlePhotosDialog'; import ArticlePhotosDialog from './ArticlePhotosDialog';
class ArticlePhotoTile extends PureComponent { class ArticlePhotoTile extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { editOpen: false }; this.state = {
editOpen: false,
selectedHash: null,
};
this.handleEditOpen = this.handleEditOpen.bind(this); this.handleEditOpen = this.handleEditOpen.bind(this);
this.handleEditClose = this.handleEditClose.bind(this); this.handleEditClose = this.handleEditClose.bind(this);
this.handleSelectPhoto = this.handleSelectPhoto.bind(this);
}
componentDidUpdate(prevProps) {
if (prevProps.article?.sku !== this.props.article?.sku) {
this.setState({ selectedHash: null });
}
} }
handleEditOpen() { handleEditOpen() {
@@ -21,6 +32,10 @@ class ArticlePhotoTile extends PureComponent {
this.setState({ editOpen: false }); this.setState({ editOpen: false });
} }
handleSelectPhoto(fullHash) {
this.setState({ selectedHash: fullHash });
}
render() { render() {
const { const {
article: articleData, article: articleData,
@@ -30,8 +45,14 @@ class ArticlePhotoTile extends PureComponent {
sx, sx,
color, color,
} = this.props; } = this.props;
const { editOpen } = this.state; const { editOpen, selectedHash } = this.state;
const photo = primaryPhoto(articleData);
const photos = sortedPhotos(articleData);
const primary = primaryPhoto(articleData);
const activePhoto =
(selectedHash && photos.find((p) => p.fullHash === selectedHash)) ||
primary;
const otherPhotos = photos.filter((p) => p.fullHash !== activePhoto?.fullHash);
return ( return (
<> <>
@@ -47,7 +68,86 @@ class ArticlePhotoTile extends PureComponent {
busy: false, busy: false,
}} }}
> >
<ArticlePhoto photo={photo} /> {otherPhotos.length === 0 ? (
<ArticlePhoto photo={activePhoto} />
) : (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'flex-start',
gap: 1.5,
}}
>
<ArticlePhoto photo={activePhoto} />
{/* Smallthumbs of all other images beside the main image */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 0.75,
maxHeight: 200,
overflowY: 'auto',
overflowX: 'hidden',
mt: '32px',
py: 0.25,
px: 0.25,
flexShrink: 0,
scrollbarWidth: 'thin',
'&::-webkit-scrollbar': { width: 4 },
'&::-webkit-scrollbar-thumb': {
bgcolor: 'action.hover',
borderRadius: 2,
},
}}
>
{otherPhotos.map((p) => (
<Box
key={p.fullHash}
component="button"
type="button"
onClick={(e) => {
e.stopPropagation();
this.handleSelectPhoto(p.fullHash);
}}
sx={{
p: 0,
m: 0,
width: 40,
height: 40,
flexShrink: 0,
cursor: 'pointer',
borderRadius: 1,
overflow: 'hidden',
border: '2px solid transparent',
bgcolor: 'transparent',
outline: 'none',
display: 'block',
transition: 'all 0.15s ease',
'&:hover': {
transform: 'scale(1.08)',
borderColor: 'primary.main',
boxShadow: 2,
},
'&:focus-visible': {
borderColor: 'primary.main',
},
}}
>
<ArticlePhoto
photo={p}
size={40}
useSmallThumb
objectFit="cover"
sx={{ width: '100%', height: '100%', borderRadius: 1 }}
/>
</Box>
))}
</Box>
</Box>
)}
</PanelTile> </PanelTile>
<ArticlePhotosDialog <ArticlePhotosDialog
open={editOpen} open={editOpen}

View File

@@ -43,14 +43,12 @@ export default class CategoryPickerDialog extends PureComponent {
} }
componentDidMount() { componentDidMount() {
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
this._unsub = categoryAutomat.subscribe((s) => { this._unsub = categoryAutomat.subscribe((s) => {
this.setState(() => ({ this.setState({
categories: this.props.categories ?? s.categories, categories: s.categories,
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId, rootCategoryId: s.rootCategoryId ?? this.props.rootCategoryId,
})); });
}); });
}
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -64,10 +62,12 @@ export default class CategoryPickerDialog extends PureComponent {
} else if (this.state.rootCategoryId != null) { } else if (this.state.rootCategoryId != null) {
nextSelectedId = this.state.rootCategoryId; nextSelectedId = this.state.rootCategoryId;
} }
this.setState({ selectedId: nextSelectedId }); this.setState({
} selectedId: nextSelectedId,
categories: categoryAutomat.state.categories ?? this.props.categories,
if ( rootCategoryId: categoryAutomat.state.rootCategoryId ?? this.props.rootCategoryId,
});
} else if (
this.props.categories !== prevProps.categories || this.props.categories !== prevProps.categories ||
this.props.rootCategoryId !== prevProps.rootCategoryId this.props.rootCategoryId !== prevProps.rootCategoryId
) { ) {

View File

@@ -123,6 +123,7 @@ export default class ArticleViewLists extends PureComponent {
this.state = { this.state = {
attributes: initialAttributes, attributes: initialAttributes,
attributesStatus: companyAutomat.state.attributesStatus || 'idle',
assignedAttributes: [], assignedAttributes: [],
allArticles: [], allArticles: [],
busy: false, busy: false,
@@ -170,10 +171,11 @@ export default class ArticleViewLists extends PureComponent {
return { return {
company: state.current, company: state.current,
attributes: state.attributes, attributes: state.attributes,
attributesStatus: state.attributesStatus,
selectedAttributeId: nextSelectedId, selectedAttributeId: nextSelectedId,
}; };
}); });
}, ['current', 'attributes']); }, ['current', 'attributes', 'attributesStatus']);
this.unsubArtikel = artikelAutomat.subscribe(() => { this.unsubArtikel = artikelAutomat.subscribe(() => {
if (this._cancelled) return; if (this._cancelled) return;
@@ -1602,7 +1604,7 @@ export default class ArticleViewLists extends PureComponent {
} }
renderStandardView(selectableAttributes, assignedAttributeIds) { renderStandardView(selectableAttributes, assignedAttributeIds) {
const { busy, activeCreatorMode, selectedAttributeId } = this.state; const { busy, activeCreatorMode, selectedAttributeId, attributes, attributesStatus } = this.state;
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{activeCreatorMode === 'parent' ? ( {activeCreatorMode === 'parent' ? (
@@ -1624,7 +1626,36 @@ export default class ArticleViewLists extends PureComponent {
</Button> </Button>
</Box> </Box>
{selectableAttributes.length === 0 ? ( {attributesStatus === 'loading' ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
<CircularProgress size={14} />
<Typography variant="caption" color="text.secondary">
Merkmale werden geladen…
</Typography>
</Box>
) : !attributes || attributes.length === 0 ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, py: 0.5 }}>
<Typography variant="caption" color="text.secondary">
Im System sind noch keine Merkmale angelegt.
</Typography>
<Link
component={RouterLink}
to="/artikel/merkmale"
variant="caption"
sx={{
color: 'primary.main',
fontWeight: 600,
textDecoration: 'none',
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
'&:hover': { textDecoration: 'underline' },
}}
>
Zu den Merkmalen →
</Link>
</Box>
) : selectableAttributes.length === 0 ? (
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen. Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen.
</Typography> </Typography>

View File

@@ -1,7 +1,7 @@
import { Component, PureComponent } from 'react'; import { Component, PureComponent } from 'react';
import { EditablePanelTile } from 'dashgui/panels'; import { EditablePanelTile } from 'dashgui/panels';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { getCategoryState } from '../../../automats/categoryAutomat'; import categoryAutomat, { getCategoryState } from '../../../automats/categoryAutomat';
import artikelAutomat from '../../../automats/artikelAutomat'; import artikelAutomat from '../../../automats/artikelAutomat';
import { getCategoryBreadcrumbs, categoryDisplayName } from '../../../utils/categoryUtils'; import { getCategoryBreadcrumbs, categoryDisplayName } from '../../../utils/categoryUtils';
import { Box, Chip, IconButton, Typography } from '@mui/material'; import { Box, Chip, IconButton, Typography } from '@mui/material';
@@ -92,10 +92,29 @@ function categoryLabels(categoryIds, categories, rootCategoryId) {
.filter((c) => c.label); .filter((c) => c.label);
} }
class CategoryPills extends Component { class CategoryPills extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { pickerOpen: false, targetPillId: null }; const catState = getCategoryState();
this.state = {
pickerOpen: false,
targetPillId: null,
categories: catState?.categories ?? categoryAutomat.state.categories,
rootCategoryId: catState?.rootCategoryId ?? categoryAutomat.state.rootCategoryId,
};
}
componentDidMount() {
this._unsubCat = categoryAutomat.subscribe((s) => {
this.setState({
categories: s.categories,
rootCategoryId: s.rootCategoryId,
});
});
}
componentWillUnmount() {
this._unsubCat?.();
} }
handleRemoveCategory = (removeId) => { handleRemoveCategory = (removeId) => {
@@ -135,8 +154,7 @@ class CategoryPills extends Component {
render() { render() {
const { categoryIds, editMode } = this.props; const { categoryIds, editMode } = this.props;
const { pickerOpen, targetPillId } = this.state; const { pickerOpen, targetPillId, categories, rootCategoryId } = this.state;
const { categories, rootCategoryId } = getCategoryState() ?? {};
const labels = categoryLabels(categoryIds, categories, rootCategoryId); const labels = categoryLabels(categoryIds, categories, rootCategoryId);
const targetNode = categories && targetPillId != null ? categories[String(targetPillId)] : null; const targetNode = categories && targetPillId != null ? categories[String(targetPillId)] : null;

View File

@@ -1,4 +1,6 @@
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import Link from '@mui/material/Link';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
@@ -562,6 +564,27 @@ export default class ArticleAttributesTab extends PureComponent {
</Tooltip> </Tooltip>
</Box> </Box>
{merkmalOptions.length === 0 && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
<Typography variant="caption" color="text.secondary">
Im System sind noch keine Merkmale angelegt.
</Typography>
<Link
component={RouterLink}
to="/artikel/merkmale"
variant="caption"
sx={{
color: 'primary.main',
fontWeight: 600,
textDecoration: 'none',
'&:hover': { textDecoration: 'underline' },
}}
>
Zu den Merkmalen →
</Link>
</Box>
)}
{/* Unified Assigned Merkmale List */} {/* Unified Assigned Merkmale List */}
{assignedGroups.length > 0 && ( {assignedGroups.length > 0 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>

View File

@@ -416,15 +416,13 @@ export default class CategoryTree extends PureComponent {
} }
componentDidMount() { componentDidMount() {
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
this._unsub = categoryAutomat.subscribe((s) => { this._unsub = categoryAutomat.subscribe((s) => {
this.setState({ this.setState({
categories: this.props.categories ?? s.categories, categories: s.categories,
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId, rootCategoryId: s.rootCategoryId ?? this.props.rootCategoryId,
}); });
}); });
} }
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if ( if (
@@ -463,7 +461,14 @@ export default class CategoryTree extends PureComponent {
name, name,
}); });
enqueueSnackbar(`Kategorie „${name}“ angelegt.`, { variant: 'success' }); enqueueSnackbar(`Kategorie „${name}“ angelegt.`, { variant: 'success' });
this.setState({ addDialogOpen: false }); const nextCategories = categoryAutomat.state.categories ?? {
...(this.state.categories || {}),
[String(created.id)]: created,
};
this.setState({
addDialogOpen: false,
categories: nextCategories,
});
this.props.onSelect?.(created.id, created); this.props.onSelect?.(created.id, created);
} catch (err) { } catch (err) {
notifyError(err); notifyError(err);
@@ -489,12 +494,19 @@ export default class CategoryTree extends PureComponent {
if (!name || !renameNode) return; if (!name || !renameNode) return;
this.setState({ isSubmitting: true }); this.setState({ isSubmitting: true });
try { try {
await categoryAutomat.actions.renameCategory({ const updated = await categoryAutomat.actions.renameCategory({
categoryId: renameNode.id, categoryId: renameNode.id,
name, name,
}); });
enqueueSnackbar(`Kategorie in „${name}“ umbenannt.`, { variant: 'success' }); enqueueSnackbar(`Kategorie in „${name}“ umbenannt.`, { variant: 'success' });
this.setState({ renameDialogOpen: false }); const nextCategories = categoryAutomat.state.categories ?? {
...(this.state.categories || {}),
[String(updated.id)]: updated,
};
this.setState({
renameDialogOpen: false,
categories: nextCategories,
});
} catch (err) { } catch (err) {
notifyError(err); notifyError(err);
} finally { } finally {
@@ -524,7 +536,10 @@ export default class CategoryTree extends PureComponent {
if (this.props.selectedId != null && String(this.props.selectedId) === String(deleteNode.id)) { if (this.props.selectedId != null && String(this.props.selectedId) === String(deleteNode.id)) {
this.props.onSelect?.(deleteNode.parentId ?? rootCategoryId); this.props.onSelect?.(deleteNode.parentId ?? rootCategoryId);
} }
this.setState({ deleteDialogOpen: false }); this.setState({
deleteDialogOpen: false,
categories: categoryAutomat.state.categories,
});
} catch (err) { } catch (err) {
notifyError(err); notifyError(err);
} finally { } finally {

View File

@@ -20,6 +20,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import articleSearchAutomat from '../automats/articleSearchAutomat'; import articleSearchAutomat from '../automats/articleSearchAutomat';
import articleIndexAutomat from '../automats/articleIndexAutomat';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import { formatPrice, getArticleGross } from '../utils/price'; import { formatPrice, getArticleGross } from '../utils/price';
import { thumb } from '../api'; import { thumb } from '../api';
@@ -50,6 +51,7 @@ import navigationAutomat, {
import categoryAutomat from '../automats/categoryAutomat'; import categoryAutomat from '../automats/categoryAutomat';
import companyAutomat from '../automats/companyAutomat'; import companyAutomat from '../automats/companyAutomat';
import { BrandLabel, BrandThumb } from '../components/brand'; import { BrandLabel, BrandThumb } from '../components/brand';
import { ArticleNameDialog } from '../components/article';
import { SCRIM_LABEL } from '../colors'; import { SCRIM_LABEL } from '../colors';
// #endregion // #endregion
@@ -282,6 +284,7 @@ function initialSearchState(searchParams) {
categoryFilterOpen: false, categoryFilterOpen: false,
brandFilterOpen: false, brandFilterOpen: false,
supplierFilterOpen: false, supplierFilterOpen: false,
newArticleOpen: false,
error: automatResult.error, error: automatResult.error,
categoryFacets: automatResult.categoryFacets, categoryFacets: automatResult.categoryFacets,
brandFacets: automatResult.brandFacets, brandFacets: automatResult.brandFacets,
@@ -354,6 +357,42 @@ class ArtikelSearchPage extends Component {
})); }));
}; };
handleOpenNewArticle = () => {
this.setState({ newArticleOpen: true });
};
handleCloseNewArticle = () => {
this.setState({ newArticleOpen: false });
};
handleCreateArticle = async ({ names }) => {
const filterParam = this.props.searchParams.get('filter') ?? '';
const categories = parseFilter(filterParam);
const parentId =
categories[0] ||
categoryAutomat.state.rootCategoryId ||
companyAutomat.state.current?.rootCategoryId ||
companyAutomat.state.current?.rootcategory_id ||
1;
const list = (names ?? []).filter(Boolean);
if (list.length <= 1) {
const created = await categoryAutomat.actions.createArticle({
parentId,
name: list[0] ?? '',
});
void articleIndexAutomat.actions.reload().catch(() => {});
if (created) {
this.props.navigate(artikelDetailPath(created));
}
return;
}
const createdList = await categoryAutomat.actions.createArticles({ parentId, names: list });
void articleIndexAutomat.actions.reload().catch(() => {});
if (createdList?.[0]) {
this.props.navigate(artikelDetailPath(createdList[0]));
}
};
handleOpenArticle = (row) => { handleOpenArticle = (row) => {
this.persistQuery(); this.persistQuery();
this.props.navigate(artikelDetailPath(row)); this.props.navigate(artikelDetailPath(row));
@@ -548,6 +587,7 @@ class ArtikelSearchPage extends Component {
categoryFilterOpen, categoryFilterOpen,
brandFilterOpen, brandFilterOpen,
supplierFilterOpen, supplierFilterOpen,
newArticleOpen,
error, error,
totalCount, totalCount,
categoryFacets, categoryFacets,
@@ -696,6 +736,13 @@ class ArtikelSearchPage extends Component {
> >
Lieferanten Lieferanten
</Button> </Button>
<Button
variant="contained"
onClick={this.handleOpenNewArticle}
sx={{ height: 40, whiteSpace: 'nowrap', flexShrink: 0 }}
>
Neuer Artikel
</Button>
{categories.map((catId) => { {categories.map((catId) => {
const label = getCategoryBreadcrumbLabel( const label = getCategoryBreadcrumbLabel(
@@ -1047,6 +1094,12 @@ class ArtikelSearchPage extends Component {
</PanelTile> </PanelTile>
</Box> </Box>
</Box> </Box>
<ArticleNameDialog
open={newArticleOpen}
initialName={input}
onClose={this.handleCloseNewArticle}
onSubmit={this.handleCreateArticle}
/>
</> </>
); );
} }