Merge branch 'master' into fetch-mock
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
import 'jest-localstorage-mock';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ import { sendEmailVerificationRequest } from 'common/api';
|
|||||||
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
|
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
|
||||||
import { getHandleClickProps } from 'common/accessibility';
|
import { getHandleClickProps } from 'common/accessibility';
|
||||||
import { sleep } from 'utils/sleep';
|
import { sleep } from 'utils/sleep';
|
||||||
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
import TextareaAutosize from 'components/textarea-autosize';
|
||||||
import { Input } from 'components/input';
|
import { Input } from 'components/input';
|
||||||
import { Button } from 'components/button';
|
import { Button } from 'components/button';
|
||||||
import { isJwtExpired } from 'utils/jwt';
|
import { isJwtExpired } from 'utils/jwt';
|
||||||
@@ -69,7 +69,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
|||||||
static emailRegex = /[^@]+@[^.]+\..+/;
|
static emailRegex = /[^@]+@[^.]+\..+/;
|
||||||
|
|
||||||
usernameInputRef = createRef<HTMLInputElement>();
|
usernameInputRef = createRef<HTMLInputElement>();
|
||||||
tokenRef = createRef<TextareaAutosize>();
|
tokenRef = createRef<HTMLTextAreaElement>();
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
usernameValue: '',
|
usernameValue: '',
|
||||||
@@ -82,13 +82,13 @@ export class EmailLoginForm extends Component<Props, State> {
|
|||||||
|
|
||||||
focus = async () => {
|
focus = async () => {
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
|
|
||||||
if (this.usernameInputRef.current) {
|
if (this.usernameInputRef.current) {
|
||||||
this.usernameInputRef.current.focus();
|
this.usernameInputRef.current.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.tokenRef.current?.textareaRef?.current) {
|
|
||||||
this.tokenRef.current.textareaRef.current.select();
|
this.tokenRef.current?.select();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onVerificationSubmit = async (e: Event) => {
|
onVerificationSubmit = async (e: Event) => {
|
||||||
@@ -97,9 +97,8 @@ export class EmailLoginForm extends Component<Props, State> {
|
|||||||
try {
|
try {
|
||||||
await this.props.sendEmailVerification(this.state.usernameValue, this.state.addressValue);
|
await this.props.sendEmailVerification(this.state.usernameValue, this.state.addressValue);
|
||||||
this.setState({ verificationSent: true });
|
this.setState({ verificationSent: true });
|
||||||
setTimeout(() => {
|
await sleep(100);
|
||||||
this.tokenRef.current && this.tokenRef.current.focus();
|
this.tokenRef.current?.focus();
|
||||||
}, 100);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ error: extractErrorMessageFromResponse(e, this.props.intl) });
|
this.setState({ error: extractErrorMessageFromResponse(e, this.props.intl) });
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ import { Input } from 'components/input';
|
|||||||
import { Button } from 'components/button';
|
import { Button } from 'components/button';
|
||||||
import { Dropdown } from 'components/dropdown';
|
import { Dropdown } from 'components/dropdown';
|
||||||
import Preloader from 'components/preloader';
|
import Preloader from 'components/preloader';
|
||||||
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
import TextareaAutosize from 'components/textarea-autosize';
|
||||||
import { isUserAnonymous } from 'utils/isUserAnonymous';
|
import { isUserAnonymous } from 'utils/isUserAnonymous';
|
||||||
import { isJwtExpired } from 'utils/jwt';
|
import { isJwtExpired } from 'utils/jwt';
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { user, anonymousUser } from '__stubs__/user';
|
|||||||
import { StaticStore } from 'common/static-store';
|
import { StaticStore } from 'common/static-store';
|
||||||
import { LS_SAVED_COMMENT_VALUE } from 'common/constants';
|
import { LS_SAVED_COMMENT_VALUE } from 'common/constants';
|
||||||
import * as localStorageModule from 'common/local-storage';
|
import * as localStorageModule from 'common/local-storage';
|
||||||
|
import TextareaAutosize from 'components/textarea-autosize';
|
||||||
|
|
||||||
import { CommentForm, CommentFormProps, messages } from './comment-form';
|
import { CommentForm, CommentFormProps, messages } from './comment-form';
|
||||||
import { SubscribeByEmail } from './__subscribe-by-email';
|
import { SubscribeByEmail } from './__subscribe-by-email';
|
||||||
import TextareaAutosize from './textarea-autosize';
|
|
||||||
import { IntlShape } from 'react-intl';
|
import { IntlShape } from 'react-intl';
|
||||||
|
|
||||||
function createEvent<E extends Event, T = unknown>(type: string, value: T): E {
|
function createEvent<E extends Event, T = unknown>(type: string, value: T): E {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { isUserAnonymous } from 'utils/isUserAnonymous';
|
|||||||
import { sleep } from 'utils/sleep';
|
import { sleep } from 'utils/sleep';
|
||||||
import { replaceSelection } from 'utils/replaceSelection';
|
import { replaceSelection } from 'utils/replaceSelection';
|
||||||
import { Button } from 'components/button';
|
import { Button } from 'components/button';
|
||||||
|
import TextareaAutosize from 'components/textarea-autosize';
|
||||||
import Auth from 'components/auth';
|
import Auth from 'components/auth';
|
||||||
import { getJsonItem, updateJsonItem } from 'common/local-storage';
|
import { getJsonItem, updateJsonItem } from 'common/local-storage';
|
||||||
import { LS_SAVED_COMMENT_VALUE } from 'common/constants';
|
import { LS_SAVED_COMMENT_VALUE } from 'common/constants';
|
||||||
@@ -18,7 +19,6 @@ import { SubscribeByEmail } from './__subscribe-by-email';
|
|||||||
import { SubscribeByRSS } from './__subscribe-by-rss';
|
import { SubscribeByRSS } from './__subscribe-by-rss';
|
||||||
|
|
||||||
import MarkdownToolbar from './markdown-toolbar';
|
import MarkdownToolbar from './markdown-toolbar';
|
||||||
import TextareaAutosize from './textarea-autosize';
|
|
||||||
import { TextExpander } from './text-expander';
|
import { TextExpander } from './text-expander';
|
||||||
|
|
||||||
let textareaId = 0;
|
let textareaId = 0;
|
||||||
@@ -100,7 +100,7 @@ export const messages = defineMessages({
|
|||||||
|
|
||||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
/** reference to textarea element */
|
/** reference to textarea element */
|
||||||
textAreaRef = createRef<TextareaAutosize>();
|
textareaRef = createRef<HTMLTextAreaElement>();
|
||||||
textareaId: string;
|
textareaId: string;
|
||||||
|
|
||||||
constructor(props: CommentFormProps) {
|
constructor(props: CommentFormProps) {
|
||||||
@@ -138,7 +138,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
componentWillReceiveProps(nextProps: CommentFormProps) {
|
componentWillReceiveProps(nextProps: CommentFormProps) {
|
||||||
if (nextProps.value !== this.props.value) {
|
if (nextProps.value !== this.props.value) {
|
||||||
this.setState({ text: nextProps.value || '' });
|
this.setState({ text: nextProps.value || '' });
|
||||||
this.props.autofocus && this.textAreaRef.current && this.textAreaRef.current.focus();
|
|
||||||
}
|
}
|
||||||
if (nextProps.user && !this.props.value) {
|
if (nextProps.user && !this.props.value) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -231,7 +230,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getPreview() {
|
getPreview() {
|
||||||
const text = this.textAreaRef.current ? this.textAreaRef.current.getValue() : this.state.text;
|
const text = this.textareaRef.current?.value ?? this.state.text;
|
||||||
|
|
||||||
if (!text || !text.trim()) return;
|
if (!text || !text.trim()) return;
|
||||||
|
|
||||||
@@ -264,7 +263,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
if (!this.props.user) e.preventDefault();
|
if (!this.props.user) e.preventDefault();
|
||||||
if (!this.props.uploadImage) return;
|
if (!this.props.uploadImage) return;
|
||||||
if (StaticStore.config.max_image_size === 0) return;
|
if (StaticStore.config.max_image_size === 0) return;
|
||||||
if (!this.textAreaRef) return;
|
if (!this.textareaRef.current) return;
|
||||||
if (!e.dataTransfer) return;
|
if (!e.dataTransfer) return;
|
||||||
const items = Array.from(e.dataTransfer.items);
|
const items = Array.from(e.dataTransfer.items);
|
||||||
if (Array.from(items).filter((i) => i.kind === 'file' && ImageMimeRegex.test(i.type)).length === 0) return;
|
if (Array.from(items).filter((i) => i.kind === 'file' && ImageMimeRegex.test(i.type)).length === 0) return;
|
||||||
@@ -295,6 +294,30 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
this.uploadImages(data);
|
this.uploadImages(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** returns selection range of a textarea */
|
||||||
|
getSelection(): [number, number] {
|
||||||
|
const textarea = this.textareaRef.current;
|
||||||
|
|
||||||
|
if (textarea) {
|
||||||
|
return [textarea.selectionStart, textarea.selectionEnd];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No textarea element reference exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** sets selection range of a textarea */
|
||||||
|
setSelection(selection: [number, number]) {
|
||||||
|
const textarea = this.textareaRef.current;
|
||||||
|
|
||||||
|
if (textarea) {
|
||||||
|
textarea.selectionStart = selection[0];
|
||||||
|
textarea.selectionEnd = selection[1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No textarea element reference exists');
|
||||||
|
}
|
||||||
|
|
||||||
/** wrapper with error handling for props.uploadImage */
|
/** wrapper with error handling for props.uploadImage */
|
||||||
uploadImage(file: File): Promise<Image | Error> {
|
uploadImage(file: File): Promise<Image | Error> {
|
||||||
const intl = this.props.intl;
|
const intl = this.props.intl;
|
||||||
@@ -312,15 +335,13 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
async uploadImages(files: File[]) {
|
async uploadImages(files: File[]) {
|
||||||
const intl = this.props.intl;
|
const intl = this.props.intl;
|
||||||
if (!this.props.uploadImage) return;
|
if (!this.props.uploadImage) return;
|
||||||
if (!this.textAreaRef.current) return;
|
if (!this.textareaRef.current) return;
|
||||||
|
|
||||||
/** Human readable image size limit, i.e 5MB */
|
/** Human readable image size limit, i.e 5MB */
|
||||||
const maxImageSizeString = `${(StaticStore.config.max_image_size / 1024 / 1024).toFixed(2)}MB`;
|
const maxImageSizeString = `${(StaticStore.config.max_image_size / 1024 / 1024).toFixed(2)}MB`;
|
||||||
/** upload delay to avoid server rate limiter */
|
/** upload delay to avoid server rate limiter */
|
||||||
const uploadDelay = 5000;
|
const uploadDelay = 5000;
|
||||||
|
|
||||||
const isSelectionSupported = this.textAreaRef.current.isSelectionSupported();
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
errorLock: true,
|
errorLock: true,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -329,43 +350,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
buttonText: intl.formatMessage(messages.uploading),
|
buttonText: intl.formatMessage(messages.uploading),
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: remove legacy code, now we don't support IE
|
|
||||||
// fallback for ie < 9
|
|
||||||
if (!isSelectionSupported) {
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
const isFirst = i === 0;
|
|
||||||
const placeholderStart = this.state.text.length === 0 ? '' : '\n';
|
|
||||||
|
|
||||||
if (file.size > StaticStore.config.max_image_size) {
|
|
||||||
this.appendError(
|
|
||||||
intl.formatMessage(messages.exceededSize, {
|
|
||||||
fileName: file.name,
|
|
||||||
maxImageSize: maxImageSizeString,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
!isFirst && (await sleep(uploadDelay));
|
|
||||||
|
|
||||||
const result = await this.uploadImage(file);
|
|
||||||
|
|
||||||
if (result instanceof Error) {
|
|
||||||
this.appendError(result.message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const markdownString = `${placeholderStart}`;
|
|
||||||
this.setState({
|
|
||||||
text: this.state.text + markdownString,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ errorLock: false, isDisabled: false, buttonText: null });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
const isFirst = i === 0;
|
const isFirst = i === 0;
|
||||||
@@ -375,7 +359,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
})}]()`;
|
})}]()`;
|
||||||
const uploadPlaceholderLength = uploadPlaceholder.length;
|
const uploadPlaceholderLength = uploadPlaceholder.length;
|
||||||
const selection = this.textAreaRef.current.getSelection();
|
const selection = this.getSelection();
|
||||||
/** saved selection in case of error */
|
/** saved selection in case of error */
|
||||||
const originalText = this.state.text;
|
const originalText = this.state.text;
|
||||||
const restoreSelection = async () => {
|
const restoreSelection = async () => {
|
||||||
@@ -384,7 +368,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
});
|
});
|
||||||
/** sleeping awhile so textarea catch state change and its selection */
|
/** sleeping awhile so textarea catch state change and its selection */
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
this.textAreaRef.current!.setSelection(selection);
|
this.setSelection(selection);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (file.size > StaticStore.config.max_image_size) {
|
if (file.size > StaticStore.config.max_image_size) {
|
||||||
@@ -418,7 +402,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
/** sleeping awhile so textarea catch state change and its selection */
|
/** sleeping awhile so textarea catch state change and its selection */
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
const selectionPointer = selection[0] + markdownString.length;
|
const selectionPointer = selection[0] + markdownString.length;
|
||||||
this.textAreaRef.current.setSelection([selectionPointer, selectionPointer]);
|
this.setSelection([selectionPointer, selectionPointer]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ errorLock: false, isDisabled: false, buttonText: null });
|
this.setState({ errorLock: false, isDisabled: false, buttonText: null });
|
||||||
@@ -481,8 +465,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||||||
<TextExpander>
|
<TextExpander>
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
id={this.textareaId}
|
id={this.textareaId}
|
||||||
|
ref={this.textareaRef}
|
||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
ref={this.textAreaRef}
|
|
||||||
className="comment-form__field"
|
className="comment-form__field"
|
||||||
placeholder={placeholderMessage}
|
placeholder={placeholderMessage}
|
||||||
value={text}
|
value={text}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { h, JSX, Component, createRef, RefObject } from 'preact';
|
|
||||||
|
|
||||||
export type TextareaAutosizeProps = {
|
|
||||||
ref?: RefObject<TextareaAutosize>;
|
|
||||||
} & Omit<JSX.HTMLAttributes<HTMLTextAreaElement>, 'ref'>;
|
|
||||||
|
|
||||||
// TODO: rewrite it to functional component and add ref forwarding
|
|
||||||
export default class TextareaAutosize extends Component<TextareaAutosizeProps> {
|
|
||||||
textareaRef = createRef<HTMLTextAreaElement>();
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.autoResize();
|
|
||||||
|
|
||||||
if (this.props.autofocus) this.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: TextareaAutosizeProps) {
|
|
||||||
if (prevProps.value !== this.props.value) {
|
|
||||||
this.autoResize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
focus(): void {
|
|
||||||
setTimeout(() => {
|
|
||||||
const { current: textarea } = this.textareaRef;
|
|
||||||
|
|
||||||
if (textarea) {
|
|
||||||
textarea.focus();
|
|
||||||
textarea.selectionStart = textarea.value.length;
|
|
||||||
textarea.selectionEnd = textarea.value.length;
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** returns whether selectionStart api supported */
|
|
||||||
isSelectionSupported() {
|
|
||||||
const { current: textarea } = this.textareaRef;
|
|
||||||
|
|
||||||
if (textarea) {
|
|
||||||
return 'selectionStart' in textarea;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('No textarea element reference exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** returns selection range of a textarea */
|
|
||||||
getSelection(): [number, number] {
|
|
||||||
const { current: textarea } = this.textareaRef;
|
|
||||||
|
|
||||||
if (textarea) {
|
|
||||||
return [textarea.selectionStart, textarea.selectionEnd];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('No textarea element reference exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** sets selection range of a textarea */
|
|
||||||
setSelection(selection: [number, number]) {
|
|
||||||
const { current: textarea } = this.textareaRef;
|
|
||||||
|
|
||||||
if (textarea) {
|
|
||||||
textarea.selectionStart = selection[0];
|
|
||||||
textarea.selectionEnd = selection[1];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('No textarea element reference exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue() {
|
|
||||||
const { current: textarea } = this.textareaRef;
|
|
||||||
|
|
||||||
return textarea ? textarea.value : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
autoResize() {
|
|
||||||
const { current: textarea } = this.textareaRef;
|
|
||||||
|
|
||||||
if (textarea) {
|
|
||||||
textarea.style.height = '';
|
|
||||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <textarea {...this.props} ref={this.textareaRef} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { h, JSX } from 'preact';
|
||||||
|
import { forwardRef } from 'preact/compat';
|
||||||
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
|
|
||||||
|
export type TextareaAutosizeProps = JSX.HTMLAttributes<HTMLTextAreaElement> & {};
|
||||||
|
|
||||||
|
function autoResize(textarea: HTMLTextAreaElement) {
|
||||||
|
textarea.style.height = '';
|
||||||
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||||
|
({ onInput, value, ...props }, externalRef) => {
|
||||||
|
const localRef = useRef<HTMLTextAreaElement>();
|
||||||
|
const ref = externalRef || localRef;
|
||||||
|
|
||||||
|
const handleInput: JSX.GenericEventHandler<HTMLTextAreaElement> = (evt) => {
|
||||||
|
if (onInput) {
|
||||||
|
return onInput.call(ref.current, evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
autoResize(ref.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
autoResize(ref.current);
|
||||||
|
}, [value, ref]);
|
||||||
|
|
||||||
|
return <textarea {...props} onInput={handleInput} value={value} ref={ref} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TextareaAutosize;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { useCallback, useMemo } from 'preact/compat';
|
import { useMemo } from 'preact/compat';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { BoundActionCreator, BoundActionCreators } from 'utils/actionBinder';
|
import { BoundActionCreator, BoundActionCreators } from 'utils/actionBinder';
|
||||||
|
|
||||||
@@ -19,10 +19,3 @@ export const useActions = <Actions extends { [key: string]: Function }>(
|
|||||||
[dispatch, ...Object.values(actions)]
|
[dispatch, ...Object.values(actions)]
|
||||||
) as any;
|
) as any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAction = <Action extends Function>(action: Action): BoundActionCreator<Action> => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
return useCallback((...args) => dispatch(action(...args)), [dispatch, action]);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
declare module 'jest-localstorage-mock' {
|
||||||
|
export const clear: () => void;
|
||||||
|
}
|
||||||
@@ -17,12 +17,18 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
setupFiles: ['<rootDir>/jest.setup.ts'],
|
setupFiles: ['<rootDir>/jest.setup.ts'],
|
||||||
setupFilesAfterEnv: [
|
setupFilesAfterEnv: [
|
||||||
'jest-localstorage-mock',
|
|
||||||
'<rootDir>/app/__mocks__/fetch.ts',
|
'<rootDir>/app/__mocks__/fetch.ts',
|
||||||
|
'<rootDir>/app/__mocks__/localstorage.ts',
|
||||||
'<rootDir>/app/__stubs__/remark-config.ts',
|
'<rootDir>/app/__stubs__/remark-config.ts',
|
||||||
'<rootDir>/app/__stubs__/static-config.ts',
|
'<rootDir>/app/__stubs__/static-config.ts',
|
||||||
],
|
],
|
||||||
collectCoverageFrom: ['app/**/*.{ts,tsx}', '!**/__mocks__/**', '!**/__stubs__/**', '!app/locales/**'],
|
collectCoverageFrom: [
|
||||||
|
'app/**/*.{ts,tsx}',
|
||||||
|
'!**/__mocks__/**',
|
||||||
|
'!**/__stubs__/**',
|
||||||
|
'!app/locales/**',
|
||||||
|
'!app/utils/loadLocale.ts',
|
||||||
|
],
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
babelConfig: true,
|
babelConfig: true,
|
||||||
|
|||||||
Generated
+7
-1
@@ -1719,6 +1719,12 @@
|
|||||||
"integrity": "sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==",
|
"integrity": "sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@prefresh/babel-plugin": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-fFwyfIHm/B8BBY7HL4j9iJl7KFk/5yVIWE+aozRRPPxI8lRFkyXMAgUFtTSmP3/jiMA6jyOcBeYUhWsyEUynpQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@prefresh/core": {
|
"@prefresh/core": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.3.0.tgz",
|
||||||
@@ -7288,7 +7294,7 @@
|
|||||||
},
|
},
|
||||||
"ignore-walk": {
|
"ignore-walk": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
|
"resolved": false,
|
||||||
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
|
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-react": "^7.12.10",
|
||||||
"@formatjs/cli": "^3.0.1",
|
"@formatjs/cli": "^3.0.1",
|
||||||
"@mavrin/stylelint-declaration-use-css-custom-properties": "1.0.0-alpha.2",
|
"@mavrin/stylelint-declaration-use-css-custom-properties": "1.0.0-alpha.2",
|
||||||
|
"@prefresh/babel-plugin": "^0.4.0",
|
||||||
"@prefresh/webpack": "^3.0.0",
|
"@prefresh/webpack": "^3.0.0",
|
||||||
"@size-limit/file": "^4.9.1",
|
"@size-limit/file": "^4.9.1",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
|
|||||||
+16
-28
@@ -12,7 +12,7 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
|||||||
const babelConfig = require('./.babelrc.js');
|
const babelConfig = require('./.babelrc.js');
|
||||||
|
|
||||||
const NODE_ID = 'remark42';
|
const NODE_ID = 'remark42';
|
||||||
const PUBLIC_PATH = '/web';
|
const PUBLIC_PATH = '/web/';
|
||||||
const PORT = process.env.PORT || 9000;
|
const PORT = process.env.PORT || 9000;
|
||||||
const REMARK_API_BASE_URL = process.env.REMARK_API_BASE_URL || 'http://127.0.0.1:8080';
|
const REMARK_API_BASE_URL = process.env.REMARK_API_BASE_URL || 'http://127.0.0.1:8080';
|
||||||
const DEVSERVER_BASE_PATH = process.env.DEVSERVER_BASE_PATH || 'http://127.0.0.1:9000';
|
const DEVSERVER_BASE_PATH = process.env.DEVSERVER_BASE_PATH || 'http://127.0.0.1:9000';
|
||||||
@@ -78,15 +78,7 @@ module.exports = (_, { mode, analyze }) => {
|
|||||||
publicPath: PUBLIC_PATH,
|
publicPath: PUBLIC_PATH,
|
||||||
};
|
};
|
||||||
|
|
||||||
const optimization = {
|
const getTsRule = (babelConfig = {}) => {
|
||||||
chunkIds: 'named',
|
|
||||||
moduleIds: 'named',
|
|
||||||
splitChunks: {
|
|
||||||
minChunks: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTsRule = (babelEnvConfig = {}) => {
|
|
||||||
return {
|
return {
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
@@ -96,7 +88,7 @@ module.exports = (_, { mode, analyze }) => {
|
|||||||
options: {
|
options: {
|
||||||
exclude,
|
exclude,
|
||||||
cacheDirectory: true,
|
cacheDirectory: true,
|
||||||
...babelEnvConfig,
|
...babelConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -178,13 +170,7 @@ module.exports = (_, { mode, analyze }) => {
|
|||||||
port: PORT,
|
port: PORT,
|
||||||
contentBase: PUBLIC_FOLDER_PATH,
|
contentBase: PUBLIC_FOLDER_PATH,
|
||||||
disableHostCheck: true,
|
disableHostCheck: true,
|
||||||
historyApiFallback: true,
|
|
||||||
quiet: true,
|
|
||||||
inline: true,
|
|
||||||
hot: true,
|
hot: true,
|
||||||
compress: true,
|
|
||||||
clientLogLevel: 'none',
|
|
||||||
overlay: false,
|
|
||||||
stats: 'minimal',
|
stats: 'minimal',
|
||||||
watchOptions: {
|
watchOptions: {
|
||||||
ignored: [PUBLIC_FOLDER_PATH, path.resolve(__dirname, 'node_modules')],
|
ignored: [PUBLIC_FOLDER_PATH, path.resolve(__dirname, 'node_modules')],
|
||||||
@@ -196,7 +182,7 @@ module.exports = (_, { mode, analyze }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
...(isDev ? [new CleanWebpackPlugin(), new RefreshPlugin(), new webpack.HotModuleReplacementPlugin()] : []),
|
...(isDev ? [new CleanWebpackPlugin(), new webpack.HotModuleReplacementPlugin(), new RefreshPlugin()] : []),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.NODE_ENV': JSON.stringify(mode),
|
'process.env.NODE_ENV': JSON.stringify(mode),
|
||||||
'process.env.REMARK_NODE': JSON.stringify(NODE_ID),
|
'process.env.REMARK_NODE': JSON.stringify(NODE_ID),
|
||||||
@@ -211,7 +197,6 @@ module.exports = (_, { mode, analyze }) => {
|
|||||||
entry,
|
entry,
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
resolve,
|
resolve,
|
||||||
optimization,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const legacyConfig = {
|
const legacyConfig = {
|
||||||
@@ -246,7 +231,13 @@ module.exports = (_, { mode, analyze }) => {
|
|||||||
chunkFilename: '[name].mjs',
|
chunkFilename: '[name].mjs',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [getTsRule(babelConfig.env.modern), ...rules],
|
rules: [
|
||||||
|
getTsRule({
|
||||||
|
...babelConfig.env.modern,
|
||||||
|
plugins: [...babelConfig.env.modern.plugins, ...(isDev ? ['@prefresh/babel-plugin'] : [])],
|
||||||
|
}),
|
||||||
|
...rules,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
...plugins,
|
...plugins,
|
||||||
@@ -272,14 +263,6 @@ module.exports = (_, { mode, analyze }) => {
|
|||||||
REMARK_URL,
|
REMARK_URL,
|
||||||
minify: htmlMinifyOptions,
|
minify: htmlMinifyOptions,
|
||||||
}),
|
}),
|
||||||
// new HtmlWebpackPlugin({
|
|
||||||
// template: path.resolve(__dirname, 'templates/comments.ejs'),
|
|
||||||
// filename: 'comments.html',
|
|
||||||
// inject: false,
|
|
||||||
// env: mode,
|
|
||||||
// REMARK_URL,
|
|
||||||
// minify: htmlMinifyOptions,
|
|
||||||
// }),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: path.resolve(__dirname, 'templates/last-comments.ejs'),
|
template: path.resolve(__dirname, 'templates/last-comments.ejs'),
|
||||||
filename: 'last-comments.html',
|
filename: 'last-comments.html',
|
||||||
@@ -320,7 +303,12 @@ module.exports = (_, { mode, analyze }) => {
|
|||||||
devServer,
|
devServer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
return modernConfig;
|
||||||
|
}
|
||||||
|
|
||||||
return [legacyConfig, modernConfig];
|
return [legacyConfig, modernConfig];
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.CUSTOM_PROPERTIES_PATH = CUSTOM_PROPERTIES_PATH;
|
module.exports.CUSTOM_PROPERTIES_PATH = CUSTOM_PROPERTIES_PATH;
|
||||||
module.exports.exclude = exclude;
|
module.exports.exclude = exclude;
|
||||||
|
|||||||
Reference in New Issue
Block a user