MM-47095 Migrate AbstractIncomingWebhook to TypeScript (#22654)

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Dismated
2023-04-04 19:28:40 +03:00
коммит произвёл GitHub
родитель 6919761122
Коммит 42ae1536cb
5 изменённых файлов: 148 добавлений и 103 удалений

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React, {ChangeEventHandler} from 'react';
import {Channel} from '@mattermost/types/channels'; import {Channel} from '@mattermost/types/channels';
@@ -10,7 +10,7 @@ import * as Utils from 'utils/utils';
type Props = { type Props = {
channels: Channel[]; channels: Channel[];
onChange?: () => void; onChange?: ChangeEventHandler<HTMLSelectElement>;
value?: string; value?: string;
selectOpen: boolean; selectOpen: boolean;
selectPrivate: boolean; selectPrivate: boolean;

Просмотреть файл

@@ -43,7 +43,7 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
<input <input
className="form-control" className="form-control"
id="displayName" id="displayName"
maxLength="64" maxLength={64}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testIncomingWebhook" value="testIncomingWebhook"
@@ -76,7 +76,7 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
<input <input
className="form-control" className="form-control"
id="description" id="description"
maxLength="500" maxLength={500}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testing" value="testing"
@@ -107,8 +107,8 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
className="col-md-5 col-sm-8" className="col-md-5 col-sm-8"
> >
<Connect(ChannelSelect) <Connect(ChannelSelect)
id="channelId"
onChange={[Function]} onChange={[Function]}
selectDm={false}
selectOpen={true} selectOpen={true}
selectPrivate={true} selectPrivate={true}
value="88cxd9wpzpbpfp8pad78xj75pr" value="88cxd9wpzpbpfp8pad78xj75pr"
@@ -172,7 +172,7 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
<input <input
className="form-control" className="form-control"
id="username" id="username"
maxLength="22" maxLength={22}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -205,7 +205,7 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
<input <input
className="form-control" className="form-control"
id="iconURL" id="iconURL"
maxLength="1024" maxLength={1024}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -304,7 +304,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
<input <input
className="form-control" className="form-control"
id="displayName" id="displayName"
maxLength="64" maxLength={64}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testIncomingWebhook" value="testIncomingWebhook"
@@ -337,7 +337,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
<input <input
className="form-control" className="form-control"
id="description" id="description"
maxLength="500" maxLength={500}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testing" value="testing"
@@ -368,8 +368,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
className="col-md-5 col-sm-8" className="col-md-5 col-sm-8"
> >
<Connect(ChannelSelect) <Connect(ChannelSelect)
id="channelId"
onChange={[Function]} onChange={[Function]}
selectDm={false}
selectOpen={true} selectOpen={true}
selectPrivate={true} selectPrivate={true}
value="88cxd9wpzpbpfp8pad78xj75pr" value="88cxd9wpzpbpfp8pad78xj75pr"
@@ -433,7 +433,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
<input <input
className="form-control" className="form-control"
id="username" id="username"
maxLength="22" maxLength={22}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -466,7 +466,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
<input <input
className="form-control" className="form-control"
id="iconURL" id="iconURL"
maxLength="1024" maxLength={1024}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -565,7 +565,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="displayName" id="displayName"
maxLength="64" maxLength={64}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -598,7 +598,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="description" id="description"
maxLength="500" maxLength={500}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -629,8 +629,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
className="col-md-5 col-sm-8" className="col-md-5 col-sm-8"
> >
<Connect(ChannelSelect) <Connect(ChannelSelect)
id="channelId"
onChange={[Function]} onChange={[Function]}
selectDm={false}
selectOpen={true} selectOpen={true}
selectPrivate={true} selectPrivate={true}
value="" value=""
@@ -694,7 +694,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="username" id="username"
maxLength="22" maxLength={22}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -727,7 +727,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="iconURL" id="iconURL"
maxLength="1024" maxLength={1024}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -829,7 +829,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="displayName" id="displayName"
maxLength="64" maxLength={64}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testIncomingWebhook" value="testIncomingWebhook"
@@ -862,7 +862,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="description" id="description"
maxLength="500" maxLength={500}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testing" value="testing"
@@ -893,8 +893,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
className="col-md-5 col-sm-8" className="col-md-5 col-sm-8"
> >
<Connect(ChannelSelect) <Connect(ChannelSelect)
id="channelId"
onChange={[Function]} onChange={[Function]}
selectDm={false}
selectOpen={true} selectOpen={true}
selectPrivate={true} selectPrivate={true}
value="88cxd9wpzpbpfp8pad78xj75pr" value="88cxd9wpzpbpfp8pad78xj75pr"
@@ -958,7 +958,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="username" id="username"
maxLength="22" maxLength={22}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -1057,7 +1057,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="displayName" id="displayName"
maxLength="64" maxLength={64}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testIncomingWebhook" value="testIncomingWebhook"
@@ -1090,7 +1090,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="description" id="description"
maxLength="500" maxLength={500}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testing" value="testing"
@@ -1121,8 +1121,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
className="col-md-5 col-sm-8" className="col-md-5 col-sm-8"
> >
<Connect(ChannelSelect) <Connect(ChannelSelect)
id="channelId"
onChange={[Function]} onChange={[Function]}
selectDm={false}
selectOpen={true} selectOpen={true}
selectPrivate={true} selectPrivate={true}
value="88cxd9wpzpbpfp8pad78xj75pr" value="88cxd9wpzpbpfp8pad78xj75pr"
@@ -1186,7 +1186,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="iconURL" id="iconURL"
maxLength="1024" maxLength={1024}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -1285,7 +1285,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="displayName" id="displayName"
maxLength="64" maxLength={64}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testIncomingWebhook" value="testIncomingWebhook"
@@ -1318,7 +1318,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="description" id="description"
maxLength="500" maxLength={500}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="testing" value="testing"
@@ -1349,8 +1349,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
className="col-md-5 col-sm-8" className="col-md-5 col-sm-8"
> >
<Connect(ChannelSelect) <Connect(ChannelSelect)
id="channelId"
onChange={[Function]} onChange={[Function]}
selectDm={false}
selectOpen={true} selectOpen={true}
selectPrivate={true} selectPrivate={true}
value="88cxd9wpzpbpfp8pad78xj75pr" value="88cxd9wpzpbpfp8pad78xj75pr"
@@ -1414,7 +1414,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="username" id="username"
maxLength="22" maxLength={22}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""
@@ -1447,7 +1447,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
<input <input
className="form-control" className="form-control"
id="iconURL" id="iconURL"
maxLength="1024" maxLength={1024}
onChange={[Function]} onChange={[Function]}
type="text" type="text"
value="" value=""

Просмотреть файл

@@ -4,10 +4,28 @@
import React from 'react'; import React from 'react';
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import ChannelSelect from 'components/channel_select';
import AbstractIncomingWebhook from 'components/integrations/abstract_incoming_webhook'; import AbstractIncomingWebhook from 'components/integrations/abstract_incoming_webhook';
import {Team} from '@mattermost/types/teams';
describe('components/integrations/AbstractIncomingWebhook', () => { describe('components/integrations/AbstractIncomingWebhook', () => {
const team = {name: 'team_name'}; const team: Team = {id: 'team_id',
create_at: 0,
update_at: 0,
delete_at: 0,
display_name: 'team_name',
name: 'team_name',
description: 'team_description',
email: 'team_email',
type: 'I',
company_name: 'team_company_name',
allowed_domains: 'team_allowed_domains',
invite_id: 'team_invite_id',
allow_open_invite: false,
scheme_id: 'team_scheme_id',
group_constrained: false,
};
const header = {id: 'header_id', defaultMessage: 'Header'}; const header = {id: 'header_id', defaultMessage: 'Header'};
const footer = {id: 'footer_id', defaultMessage: 'Footer'}; const footer = {id: 'footer_id', defaultMessage: 'Footer'};
const loading = {id: 'loading_id', defaultMessage: 'Loading'}; const loading = {id: 'loading_id', defaultMessage: 'Loading'};
@@ -16,6 +34,15 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
display_name: 'testIncomingWebhook', display_name: 'testIncomingWebhook',
channel_id: '88cxd9wpzpbpfp8pad78xj75pr', channel_id: '88cxd9wpzpbpfp8pad78xj75pr',
description: 'testing', description: 'testing',
id: 'test_id',
team_id: 'test_team_id',
create_at: 0,
update_at: 0,
delete_at: 0,
user_id: 'test_user_id',
username: '',
icon_url: '',
channel_locked: false,
}; };
const enablePostUsernameOverride = true; const enablePostUsernameOverride = true;
const enablePostIconOverride = true; const enablePostIconOverride = true;
@@ -104,7 +131,7 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
}; };
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>); const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
wrapper.find('#channelId').simulate('change', evt); wrapper.find(ChannelSelect).simulate('change', evt);
expect(wrapper.state('channelId')).toBe(newChannelId); expect(wrapper.state('channelId')).toBe(newChannelId);
}); });

Просмотреть файл

@@ -1,87 +1,100 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import PropTypes from 'prop-types'; import React, {ChangeEventHandler, FormEvent, MouseEvent, PureComponent} from 'react';
import React from 'react'; import {FormattedMessage, MessageDescriptor} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import BackstageHeader from 'components/backstage/components/backstage_header'; import BackstageHeader from 'components/backstage/components/backstage_header';
import ChannelSelect from 'components/channel_select'; import ChannelSelect from 'components/channel_select';
import FormError from 'components/form_error'; import FormError from 'components/form_error';
import SpinnerButton from 'components/spinner_button'; import SpinnerButton from 'components/spinner_button';
import {Team} from '@mattermost/types/teams';
import {localizeMessage} from 'utils/utils'; import {localizeMessage} from 'utils/utils';
import {IncomingWebhook} from '@mattermost/types/integrations';
export default class AbstractIncomingWebhook extends React.PureComponent { interface State {
static propTypes = { displayName: string;
description: string;
channelId: string;
channelLocked: boolean;
username: string;
iconURL: string;
saving: boolean;
serverError: string;
clientError: JSX.Element | null;
}
/** interface Props {
* The current team
*/
team: PropTypes.object.isRequired,
/** /**
* The header text to render, has id and defaultMessage * The current team
*/ */
header: PropTypes.object.isRequired, team: Team;
/** /**
* The footer text to render, has id and defaultMessage * The header text to render, has id and defaultMessage
*/ */
footer: PropTypes.object.isRequired, header: MessageDescriptor;
/** /**
* The spinner loading text to render, has id and defaultMessage * The footer text to render, has id and defaultMessage
*/ */
loading: PropTypes.object.isRequired, footer: MessageDescriptor;
/** /**
* The server error text after a failed action * The spinner loading text to render, has id and defaultMessage
*/ */
serverError: PropTypes.string.isRequired, loading: MessageDescriptor;
/** /**
* The hook used to set the initial state * The server error text after a failed action
*/ */
initialHook: PropTypes.object, serverError: string;
/** /**
* Whether to allow configuration of the default post username. * The hook used to set the initial state
*/ */
enablePostUsernameOverride: PropTypes.bool.isRequired, initialHook?: IncomingWebhook | Record<string, never>;
/** /**
* Whether to allow configuration of the default post icon. * Whether to allow configuration of the default post username.
*/ */
enablePostIconOverride: PropTypes.bool.isRequired, enablePostUsernameOverride: boolean;
/** /**
* The async function to run when the action button is pressed * Whether to allow configuration of the default post icon.
*/ */
action: PropTypes.func.isRequired, enablePostIconOverride: boolean;
}
constructor(props) { /**
* The async function to run when the action button is pressed
*/
action: (hook: IncomingWebhook) => Promise<void>;
}
export default class AbstractIncomingWebhook extends PureComponent<Props, State> {
constructor(props: Props | Readonly<Props>) {
super(props); super(props);
this.state = this.getStateFromHook(this.props.initialHook || {}); this.state = this.getStateFromHook(this.props.initialHook || {});
} }
getStateFromHook = (hook) => { getStateFromHook = (hook: IncomingWebhook | Record<string, never>) => {
return { return {
displayName: hook.display_name || '', displayName: hook?.display_name || '',
description: hook.description || '', description: hook?.description || '',
channelId: hook.channel_id || '', channelId: hook?.channel_id || '',
channelLocked: hook.channel_locked || false, channelLocked: hook?.channel_locked || false,
username: hook.username || '', username: hook?.username || '',
iconURL: hook.icon_url || '', iconURL: hook?.icon_url || '',
saving: false, saving: false,
serverError: '', serverError: '',
clientError: null, clientError: null,
}; };
} }
handleSubmit = (e) => { handleSubmit = (e: MouseEvent<HTMLElement> | FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (this.state.saving) { if (this.state.saving) {
@@ -91,7 +104,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
this.setState({ this.setState({
saving: true, saving: true,
serverError: '', serverError: '',
clientError: '', clientError: null,
}); });
if (!this.state.channelId) { if (!this.state.channelId) {
@@ -115,50 +128,56 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
description: this.state.description, description: this.state.description,
username: this.state.username, username: this.state.username,
icon_url: this.state.iconURL, icon_url: this.state.iconURL,
id: this.props.initialHook?.id || '',
create_at: this.props.initialHook?.create_at || 0,
update_at: this.props.initialHook?.update_at || 0,
delete_at: this.props.initialHook?.delete_at || 0,
team_id: this.props.initialHook?.team_id || '',
user_id: this.props.initialHook?.user_id || '',
}; };
this.props.action(hook).then(() => this.setState({saving: false})); this.props.action(hook).then(() => this.setState({saving: false}));
} }
updateDisplayName = (e) => { updateDisplayName: ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ this.setState({
displayName: e.target.value, displayName: e.target.value,
}); });
} }
updateDescription = (e) => { updateDescription: ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ this.setState({
description: e.target.value, description: e.target.value,
}); });
} }
updateChannelId = (e) => { updateChannelId: ChangeEventHandler<HTMLSelectElement> = (e) => {
this.setState({ this.setState({
channelId: e.target.value, channelId: e.target.value,
}); });
} }
updateChannelLocked = (e) => { updateChannelLocked: ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ this.setState({
channelLocked: e.target.checked, channelLocked: e.target.checked,
}); });
} }
updateUsername = (e) => { updateUsername: ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ this.setState({
username: e.target.value, username: e.target.value,
}); });
} }
updateIconURL = (e) => { updateIconURL: ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ this.setState({
iconURL: e.target.value, iconURL: e.target.value,
}); });
} }
render() { render() {
var headerToRender = this.props.header; const headerToRender = this.props.header;
var footerToRender = this.props.footer; const footerToRender = this.props.footer;
return ( return (
<div className='backstage-content'> <div className='backstage-content'>
@@ -177,7 +196,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
<div className='backstage-form'> <div className='backstage-form'>
<form <form
className='form-horizontal' className='form-horizontal'
onSubmit={this.handleSubmit} onSubmit={(e) => this.handleSubmit(e)}
> >
<div className='form-group'> <div className='form-group'>
<label <label
@@ -193,7 +212,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
<input <input
id='displayName' id='displayName'
type='text' type='text'
maxLength='64' maxLength={64}
className='form-control' className='form-control'
value={this.state.displayName} value={this.state.displayName}
onChange={this.updateDisplayName} onChange={this.updateDisplayName}
@@ -220,7 +239,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
<input <input
id='description' id='description'
type='text' type='text'
maxLength='500' maxLength={500}
className='form-control' className='form-control'
value={this.state.description} value={this.state.description}
onChange={this.updateDescription} onChange={this.updateDescription}
@@ -245,11 +264,11 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
</label> </label>
<div className='col-md-5 col-sm-8'> <div className='col-md-5 col-sm-8'>
<ChannelSelect <ChannelSelect
id='channelId'
value={this.state.channelId} value={this.state.channelId}
onChange={this.updateChannelId} onChange={this.updateChannelId}
selectOpen={true} selectOpen={true}
selectPrivate={true} selectPrivate={true}
selectDm={false}
/> />
<div className='form__help'> <div className='form__help'>
<FormattedMessage <FormattedMessage
@@ -299,7 +318,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
<input <input
id='username' id='username'
type='text' type='text'
maxLength='22' maxLength={22}
className='form-control' className='form-control'
value={this.state.username} value={this.state.username}
onChange={this.updateUsername} onChange={this.updateUsername}
@@ -328,7 +347,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
<input <input
id='iconURL' id='iconURL'
type='text' type='text'
maxLength='1024' maxLength={1024}
className='form-control' className='form-control'
value={this.state.iconURL} value={this.state.iconURL}
onChange={this.updateIconURL} onChange={this.updateIconURL}
@@ -360,8 +379,8 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
className='btn btn-primary' className='btn btn-primary'
type='submit' type='submit'
spinning={this.state.saving} spinning={this.state.saving}
spinningText={localizeMessage(this.props.loading.id, this.props.loading.defaultMessage)} spinningText={localizeMessage(this.props.loading.id as string, this.props.loading.defaultMessage as string)}
onClick={this.handleSubmit} onClick={(e) => this.handleSubmit(e)}
id='saveWebhook' id='saveWebhook'
> >
<FormattedMessage <FormattedMessage

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {PureComponent, ReactNode} from 'react'; import React, {ButtonHTMLAttributes, PureComponent, ReactNode} from 'react';
import LoadingWrapper from 'components/widgets/loading/loading_wrapper'; import LoadingWrapper from 'components/widgets/loading/loading_wrapper';
@@ -9,10 +9,9 @@ type Props = {
children?: ReactNode; children?: ReactNode;
spinning: boolean; spinning: boolean;
spinningText: ReactNode; spinningText: ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
} }
export default class SpinnerButton extends PureComponent<Props & React.ButtonHTMLAttributes<HTMLButtonElement>> { export default class SpinnerButton extends PureComponent<Props & ButtonHTMLAttributes<HTMLButtonElement>> {
public static defaultProps: Partial<Props> = { public static defaultProps: Partial<Props> = {
spinning: false, spinning: false,
} }