Improve component encapsulation

This commit is contained in:
dominik-zeglen 2020-11-05 14:56:29 +01:00
parent 44df6225ad
commit dfc063caa1
13 changed files with 77 additions and 501 deletions

View file

@ -1,8 +1,10 @@
import { CategoryFragment } from "@saleor/fragments/types/CategoryFragment";
import { content } from "../storybook/stories/components/RichTextEditor";
import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json";
import { CategoryDetails_category } from "./types/CategoryDetails";
const content = richTextEditorFixtures.richTextEditor;
export const categories: CategoryFragment[] = [
{
__typename: "Category",

View file

@ -1,7 +1,9 @@
import { content } from "../storybook/stories/components/RichTextEditor";
import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json";
import { CollectionDetails_collection } from "./types/CollectionDetails";
import { CollectionList_collections_edges_node } from "./types/CollectionList";
const content = richTextEditorFixtures.richTextEditor;
export const collections: CollectionList_collections_edges_node[] = [
{
__typename: "Collection",

View file

@ -1,104 +0,0 @@
import Button from "@material-ui/core/Button";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Grow from "@material-ui/core/Grow";
import IconButton from "@material-ui/core/IconButton";
import Paper from "@material-ui/core/Paper";
import Popper from "@material-ui/core/Popper";
import { makeStyles } from "@material-ui/core/styles";
import DeleteIcon from "@material-ui/icons/Delete";
import { ContentState } from "draft-js";
import React from "react";
import { FormattedMessage } from "react-intl";
interface ImageEntityProps {
children: React.ReactNode;
contentState: ContentState;
entityKey: string;
onEdit: (entityKey: string) => void;
onRemove: (entityKey: string) => void;
}
const useStyles = makeStyles(
theme => ({
anchor: {
display: "inline-block"
},
container: {
alignItems: "center",
display: "flex"
},
image: { maxWidth: "100%" },
inline: {
display: "inline-block"
},
root: {
alignItems: "center",
display: "flex",
minHeight: 72,
padding: theme.spacing(1.5)
}
}),
{ name: "ImageEntity" }
);
const ImageEntity: React.FC<ImageEntityProps> = props => {
const { contentState, entityKey, onEdit, onRemove } = props;
const classes = useStyles(props);
const [isOpened, setOpenStatus] = React.useState(false);
const anchor = React.useRef<HTMLDivElement>();
const disable = () => setOpenStatus(false);
const toggle = () => setOpenStatus(!isOpened);
return (
<>
<div className={classes.anchor} ref={anchor}>
<Popper
open={isOpened}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement
}}
>
<Paper className={classes.root}>
<ClickAwayListener onClickAway={disable} mouseEvent="onClick">
<div className={classes.container}>
<Button
onClick={() => {
disable();
onEdit(entityKey);
}}
color="primary"
>
<FormattedMessage
defaultMessage="Replace"
description="replace image, button"
/>
</Button>
<IconButton onClick={() => onRemove(entityKey)}>
<DeleteIcon color="primary" />
</IconButton>
</div>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
<img
className={classes.image}
src={contentState.getEntity(entityKey).getData().href}
onClick={toggle}
/>
</>
);
};
export default ImageEntity;

View file

@ -1,107 +0,0 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import TextField from "@material-ui/core/TextField";
import { buttonMessages } from "@saleor/intl";
import { AtomicBlockUtils, EditorState, EntityInstance } from "draft-js";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Form from "../Form";
interface ImageSourceProps {
editorState: EditorState;
entity?: EntityInstance;
entityKey?: string;
entityType: {
type: string;
};
onComplete: (updateState: EditorState) => void;
onClose: () => void;
}
const ImageSource: React.FC<ImageSourceProps> = ({
editorState,
entity,
entityKey,
entityType,
onComplete,
onClose
}) => {
const intl = useIntl();
const initial = entity ? entity.getData().href : "";
const handleSubmit = (href: string) => {
if (href) {
const content = editorState.getCurrentContent();
if (entity) {
const nextContent = content.mergeEntityData(entityKey, { href });
const nextState = EditorState.push(
editorState,
nextContent,
"apply-entity"
);
onComplete(nextState);
} else {
const contentWithEntity = content.createEntity(
entityType.type,
"IMMUTABLE",
{ href }
);
const nextState = AtomicBlockUtils.insertAtomicBlock(
editorState,
contentWithEntity.getLastCreatedEntityKey(),
" "
);
onComplete(nextState);
}
} else {
onComplete(editorState);
}
};
return (
<Dialog onClose={onClose} open={true} fullWidth maxWidth="sm">
<Form
initial={{ href: initial }}
onSubmit={({ href }) => handleSubmit(href)}
>
{({ data, change, submit }) => (
<>
<DialogTitle>
<FormattedMessage
defaultMessage="Add Image Link"
description="dialog header"
/>
</DialogTitle>
<DialogContent>
<TextField
name="href"
fullWidth
label={intl.formatMessage({
defaultMessage: "Image URL"
})}
value={data.href}
onChange={change}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.cancel} />
</Button>
<Button onClick={submit} color="primary" variant="contained">
<FormattedMessage {...buttonMessages.save} />
</Button>
</DialogActions>
</>
)}
</Form>
</Dialog>
);
};
export default ImageSource;

View file

@ -1,121 +0,0 @@
import Button from "@material-ui/core/Button";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Grow from "@material-ui/core/Grow";
import IconButton from "@material-ui/core/IconButton";
import Paper from "@material-ui/core/Paper";
import Popper from "@material-ui/core/Popper";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import { buttonMessages } from "@saleor/intl";
import { ContentState } from "draft-js";
import React from "react";
import { FormattedMessage } from "react-intl";
import Link from "../Link";
interface LinkEntityProps {
children: React.ReactNode;
contentState: ContentState;
entityKey: string;
onEdit: (entityKey: string) => void;
onRemove: (entityKey: string) => void;
}
const useStyles = makeStyles(
theme => ({
anchor: {
display: "inline-block"
},
container: {
alignItems: "center",
display: "flex"
},
inline: {
display: "inline-block"
},
popover: {
zIndex: 1
},
root: {
alignItems: "center",
display: "flex",
minHeight: 72,
padding: theme.spacing(1.5, 1.5, 1.5, 3)
},
separator: {
backgroundColor: theme.palette.grey[300],
display: "inline-block",
height: 30,
marginLeft: theme.spacing(2),
marginRight: theme.spacing(),
width: 1
}
}),
{ name: "LinkEntity" }
);
const LinkEntity: React.FC<LinkEntityProps> = props => {
const { children, contentState, entityKey, onEdit, onRemove } = props;
const classes = useStyles(props);
const [isOpened, setOpenStatus] = React.useState(false);
const anchor = React.useRef<HTMLDivElement>();
const disable = () => setOpenStatus(false);
const toggle = () => setOpenStatus(!isOpened);
return (
<>
<div className={classes.anchor} ref={anchor}>
<Popper
className={classes.popover}
open={isOpened}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement
}}
>
<Paper className={classes.root}>
<ClickAwayListener onClickAway={disable} mouseEvent="onClick">
<div className={classes.container}>
<Typography className={classes.inline} variant="body1">
{contentState.getEntity(entityKey).getData().url}
</Typography>
<span className={classes.separator} />
<Button
onClick={() => {
disable();
onEdit(entityKey);
}}
color="primary"
>
<FormattedMessage {...buttonMessages.edit} />
</Button>
<IconButton onClick={() => onRemove(entityKey)}>
<DeleteIcon color="primary" />
</IconButton>
</div>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
<Link
href={contentState.getEntity(entityKey).getData().url}
onClick={toggle}
>
{children}
</Link>
</>
);
};
export default LinkEntity;

View file

@ -1,98 +0,0 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import TextField from "@material-ui/core/TextField";
import { buttonMessages } from "@saleor/intl";
import { EditorState, EntityInstance, RichUtils } from "draft-js";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Form from "../Form";
interface LinkSourceProps {
editorState: EditorState;
entity?: EntityInstance;
entityType: {
type: string;
};
onComplete: (updateState: EditorState) => void;
onClose: () => void;
}
const LinkSource: React.FC<LinkSourceProps> = ({
editorState,
entity,
entityType,
onComplete,
onClose
}) => {
const intl = useIntl();
const initial = entity ? entity.getData().url : "";
const handleSubmit = (url: string) => {
if (url) {
const content = editorState.getCurrentContent();
const contentWithEntity = content.createEntity(
entityType.type,
"MUTABLE",
{ url }
);
const entityKey = contentWithEntity.getLastCreatedEntityKey();
const newEditorState = EditorState.set(editorState, {
currentContent: contentWithEntity
});
const nextState = RichUtils.toggleLink(
newEditorState,
newEditorState.getSelection(),
entityKey
);
onComplete(nextState);
} else {
onComplete(editorState);
}
};
return (
<Dialog onClose={onClose} open={true} fullWidth maxWidth="sm">
<Form
initial={{ url: initial }}
onSubmit={({ url }) => handleSubmit(url)}
>
{({ data, change, submit }) => (
<>
<DialogTitle>
<FormattedMessage
defaultMessage="Add or Edit Link"
description="button"
/>
</DialogTitle>
<DialogContent>
<TextField
name="url"
fullWidth
label={intl.formatMessage({
defaultMessage: "URL Linked"
})}
value={data.url}
onChange={change}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.cancel} />
</Button>
<Button onClick={submit} color="primary" variant="contained">
<FormattedMessage {...buttonMessages.save} />
</Button>
</DialogActions>
</>
)}
</Form>
</Dialog>
);
};
export default LinkSource;

View file

@ -0,0 +1,28 @@
import { OutputData } from "@editorjs/editorjs";
import RichTextEditor from "@saleor/components/RichTextEditor";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import * as fixtures from "./fixtures.json";
import { RichTextEditorProps } from "./RichTextEditor";
export const data: OutputData = fixtures.richTextEditor;
const props: RichTextEditorProps = {
data,
disabled: false,
error: false,
helperText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
label: "Content",
name: "content",
onChange: () => undefined
};
storiesOf("Generics / Rich text editor", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => <RichTextEditor {...props} />)
.add("disabled", () => <RichTextEditor {...props} disabled={true} />)
.add("error", () => <RichTextEditor {...props} error={true} />);

View file

@ -2,6 +2,10 @@ import EditorJS, { OutputData } from "@editorjs/editorjs";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import Quote from "@editorjs/quote";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import InputLabel from "@material-ui/core/InputLabel";
import OutlinedInput from "@material-ui/core/OutlinedInput";
import { makeStyles } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator";
import Typography from "@material-ui/core/Typography";
@ -32,21 +36,6 @@ const useStyles = makeStyles(
};
return {
error: {
color: theme.palette.error.main
},
helperText: {
marginTop: theme.spacing(0.75)
},
label: {
color: theme.palette.text.secondary,
position: "absolute",
top: theme.spacing(1),
transition: theme.transitions.duration.short + "ms"
},
labelActive: {
color: theme.palette.primary.main
},
root: {
"& .cdx-quote__text": {
minHeight: 24
@ -99,13 +88,14 @@ const useStyles = makeStyles(
"& a": {
color: theme.palette.primary.light
},
"&:hover": {
"&:not($rootDisabled):hover": {
borderColor: theme.palette.primary.main
},
border: `1px solid ${fade(theme.palette.text.secondary, 0.4)}`,
borderRadius: 4,
boxShadow: `inset 0 0 0 0 ${theme.palette.primary.main}`,
fontSize: theme.typography.body1.fontSize,
minHeight: 56,
padding: theme.spacing(3, 2),
paddingBottom: theme.spacing(),
paddingLeft: 10,
@ -114,6 +104,12 @@ const useStyles = makeStyles(
},
rootActive: {
boxShadow: `inset 0px 0px 0 2px ${theme.palette.primary.main}`
},
rootDisabled: {
...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"]
},
rootError: {
borderColor: theme.palette.error.main
}
};
},
@ -122,9 +118,11 @@ const useStyles = makeStyles(
const RichTextEditor: React.FC<RichTextEditorProps> = ({
data,
disabled,
error,
helperText,
label,
name,
onChange,
onReady
}) => {
@ -144,6 +142,7 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
onChange(savedData);
},
onReady,
readOnly: disabled,
tools: {
header: {
class: Header,
@ -172,39 +171,36 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
[data === undefined]
);
React.useEffect(() => editor.current?.destroy, []);
React.useEffect(() => {
if (editor.current?.readOnly) {
editor.current.readOnly.toggle(disabled);
}
}, [disabled]);
return (
<div>
<FormControl
data-test="richTextEditor"
data-test-id={name}
disabled={disabled}
error={error}
fullWidth
variant="outlined"
>
<InputLabel focused={true} shrink={true}>
{label}
</InputLabel>
<div
className={classNames(classes.root, {
[classes.rootActive]: isFocused
[classes.rootActive]: isFocused,
[classes.rootDisabled]: disabled,
[classes.rootError]: error
})}
ref={editorContainer}
data-test="richTextEditor"
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
>
<Typography
className={classNames(classes.label, {
[classes.labelActive]: isFocused
})}
variant="caption"
>
{label}
</Typography>
</div>
{helperText && (
<Typography
className={classNames({
[classes.error]: error,
[classes.helperText]: true
})}
variant="caption"
>
{helperText}
</Typography>
)}
</div>
/>
<FormHelperText>{helperText}</FormHelperText>
</FormControl>
);
};

View file

@ -1,7 +1,9 @@
import { content } from "../storybook/stories/components/RichTextEditor";
import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json";
import { PageDetails_page } from "./types/PageDetails";
import { PageList_pages_edges_node } from "./types/PageList";
const content = richTextEditorFixtures.richTextEditor;
export const pageList: PageList_pages_edges_node[] = [
{
__typename: "Page",

View file

@ -5,11 +5,13 @@ import {
} from "@saleor/types/globalTypes";
import { warehouseList } from "@saleor/warehouses/fixtures";
import { content } from "../storybook/stories/components/RichTextEditor";
import * as richTextEditorFixtures from "../components/RichTextEditor/fixtures.json";
import { ProductDetails_product } from "./types/ProductDetails";
import { ProductList_products_edges_node } from "./types/ProductList";
import { ProductVariantCreateData_product } from "./types/ProductVariantCreateData";
const content = richTextEditorFixtures.richTextEditor;
export const product: (
placeholderImage: string
) => ProductDetails_product &

View file

@ -35,7 +35,6 @@ function loadStories() {
require("./stories/components/Percent");
require("./stories/components/PhoneField");
require("./stories/components/PriceField");
require("./stories/components/RichTextEditor");
require("./stories/components/SaveButtonBar");
require("./stories/components/SaveFilterTabDialog");
require("./stories/components/SingleSelectField");

View file

@ -1,25 +0,0 @@
import { OutputData } from "@editorjs/editorjs";
import RichTextEditor from "@saleor/components/RichTextEditor";
import { storiesOf } from "@storybook/react";
import React from "react";
import CardDecorator from "../../CardDecorator";
import Decorator from "../../Decorator";
import * as fixtures from "./fixtures.json";
export const content: OutputData = fixtures.richTextEditor;
storiesOf("Generics / Rich text editor", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => (
<RichTextEditor
disabled={false}
error={false}
helperText="Lorem ipsum dolor sit amet, consectetur adipiscing elit"
initial={content}
label="Content"
name="content"
onChange={() => undefined}
/>
));