Onboarding: Allow users to navigate backwards in steps (https://github.com/woocommerce/woocommerce-admin/pull/3154)
* Add label wrapper to stepper and style for both orientations * Add onClick event to steps * Add onClick to stepper examples * Allow returning to previous steps on step click * Allow navigating to previously completed steps * Mark previous steps complete
This commit is contained in:
parent
e1e64b9241
commit
4f79cdea93
|
@ -6,6 +6,11 @@ import { Component } from '@wordpress/element';
|
||||||
import { filter } from 'lodash';
|
import { filter } from 'lodash';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce dependencies
|
||||||
|
*/
|
||||||
|
import { updateQueryString } from '@woocommerce/navigation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
@ -14,9 +19,26 @@ import HeaderLogo from './header-logo';
|
||||||
|
|
||||||
export default class ProfileWizardHeader extends Component {
|
export default class ProfileWizardHeader extends Component {
|
||||||
renderStepper() {
|
renderStepper() {
|
||||||
const steps = filter( this.props.steps, step => !! step.label );
|
const { currentStep, steps } = this.props;
|
||||||
return <Stepper steps={ steps } currentStep={ this.props.currentStep } />;
|
const visibleSteps = filter( steps, step => !! step.label );
|
||||||
|
const currentStepIndex = visibleSteps.findIndex( step => step.key === currentStep );
|
||||||
|
|
||||||
|
visibleSteps.map( ( step, index ) => {
|
||||||
|
const previousStep = visibleSteps[ index - 1 ];
|
||||||
|
|
||||||
|
if ( index < currentStepIndex ) {
|
||||||
|
step.isComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! previousStep || previousStep.isComplete ) {
|
||||||
|
step.onClick = key => updateQueryString( { step: key } );
|
||||||
|
}
|
||||||
|
return step;
|
||||||
|
} );
|
||||||
|
|
||||||
|
return <Stepper steps={ visibleSteps } currentStep={ currentStep } />;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const currentStep = this.props.steps.find( s => s.key === this.props.currentStep );
|
const currentStep = this.props.steps.find( s => s.key === this.props.currentStep );
|
||||||
const showStepper = ! currentStep || ! currentStep.label ? false : true;
|
const showStepper = ! currentStep || ! currentStep.label ? false : true;
|
||||||
|
|
|
@ -27,44 +27,6 @@ import Theme from './steps/theme';
|
||||||
import withSelect from 'wc-api/with-select';
|
import withSelect from 'wc-api/with-select';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
const getSteps = () => {
|
|
||||||
const steps = [];
|
|
||||||
steps.push( {
|
|
||||||
key: 'start',
|
|
||||||
container: Start,
|
|
||||||
} );
|
|
||||||
steps.push( {
|
|
||||||
key: 'plugins',
|
|
||||||
container: Plugins,
|
|
||||||
} );
|
|
||||||
steps.push( {
|
|
||||||
key: 'store-details',
|
|
||||||
container: StoreDetails,
|
|
||||||
label: __( 'Store Details', 'woocommerce-admin' ),
|
|
||||||
} );
|
|
||||||
steps.push( {
|
|
||||||
key: 'industry',
|
|
||||||
container: Industry,
|
|
||||||
label: __( 'Industry', 'woocommerce-admin' ),
|
|
||||||
} );
|
|
||||||
steps.push( {
|
|
||||||
key: 'product-types',
|
|
||||||
container: ProductTypes,
|
|
||||||
label: __( 'Product Types', 'woocommerce-admin' ),
|
|
||||||
} );
|
|
||||||
steps.push( {
|
|
||||||
key: 'business-details',
|
|
||||||
container: BusinessDetails,
|
|
||||||
label: __( 'Business Details', 'woocommerce-admin' ),
|
|
||||||
} );
|
|
||||||
steps.push( {
|
|
||||||
key: 'theme',
|
|
||||||
container: Theme,
|
|
||||||
label: __( 'Theme', 'woocommerce-admin' ),
|
|
||||||
} );
|
|
||||||
return steps;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ProfileWizard extends Component {
|
class ProfileWizard extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super( ...arguments );
|
super( ...arguments );
|
||||||
|
@ -94,12 +56,61 @@ class ProfileWizard extends Component {
|
||||||
document.body.classList.remove( 'woocommerce-admin-full-screen' );
|
document.body.classList.remove( 'woocommerce-admin-full-screen' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSteps() {
|
||||||
|
const { profileItems } = this.props;
|
||||||
|
const steps = [];
|
||||||
|
|
||||||
|
steps.push( {
|
||||||
|
key: 'start',
|
||||||
|
container: Start,
|
||||||
|
} );
|
||||||
|
steps.push( {
|
||||||
|
key: 'plugins',
|
||||||
|
container: Plugins,
|
||||||
|
isComplete: profileItems.hasOwnProperty( 'plugins' ) && null !== profileItems.plugins,
|
||||||
|
} );
|
||||||
|
steps.push( {
|
||||||
|
key: 'store-details',
|
||||||
|
container: StoreDetails,
|
||||||
|
label: __( 'Store Details', 'woocommerce-admin' ),
|
||||||
|
isComplete:
|
||||||
|
profileItems.hasOwnProperty( 'setup_client' ) && null !== profileItems.setup_client,
|
||||||
|
} );
|
||||||
|
steps.push( {
|
||||||
|
key: 'industry',
|
||||||
|
container: Industry,
|
||||||
|
label: __( 'Industry', 'woocommerce-admin' ),
|
||||||
|
isComplete: profileItems.hasOwnProperty( 'industry' ) && null !== profileItems.industry,
|
||||||
|
} );
|
||||||
|
steps.push( {
|
||||||
|
key: 'product-types',
|
||||||
|
container: ProductTypes,
|
||||||
|
label: __( 'Product Types', 'woocommerce-admin' ),
|
||||||
|
isComplete:
|
||||||
|
profileItems.hasOwnProperty( 'product_types' ) && null !== profileItems.product_types,
|
||||||
|
} );
|
||||||
|
steps.push( {
|
||||||
|
key: 'business-details',
|
||||||
|
container: BusinessDetails,
|
||||||
|
label: __( 'Business Details', 'woocommerce-admin' ),
|
||||||
|
isComplete:
|
||||||
|
profileItems.hasOwnProperty( 'product_count' ) && null !== profileItems.product_count,
|
||||||
|
} );
|
||||||
|
steps.push( {
|
||||||
|
key: 'theme',
|
||||||
|
container: Theme,
|
||||||
|
label: __( 'Theme', 'woocommerce-admin' ),
|
||||||
|
isComplete: profileItems.hasOwnProperty( 'theme' ) && null !== profileItems.theme,
|
||||||
|
} );
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentStep() {
|
getCurrentStep() {
|
||||||
const { step } = this.props.query;
|
const { step } = this.props.query;
|
||||||
const currentStep = getSteps().find( s => s.key === step );
|
const currentStep = this.getSteps().find( s => s.key === step );
|
||||||
|
|
||||||
if ( ! currentStep ) {
|
if ( ! currentStep ) {
|
||||||
return getSteps()[ 0 ];
|
return this.getSteps()[ 0 ];
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentStep;
|
return currentStep;
|
||||||
|
@ -108,8 +119,8 @@ class ProfileWizard extends Component {
|
||||||
async goToNextStep() {
|
async goToNextStep() {
|
||||||
const { createNotice, isError, updateProfileItems } = this.props;
|
const { createNotice, isError, updateProfileItems } = this.props;
|
||||||
const currentStep = this.getCurrentStep();
|
const currentStep = this.getCurrentStep();
|
||||||
const currentStepIndex = getSteps().findIndex( s => s.key === currentStep.key );
|
const currentStepIndex = this.getSteps().findIndex( s => s.key === currentStep.key );
|
||||||
const nextStep = getSteps()[ currentStepIndex + 1 ];
|
const nextStep = this.getSteps()[ currentStepIndex + 1 ];
|
||||||
|
|
||||||
if ( 'undefined' === typeof nextStep ) {
|
if ( 'undefined' === typeof nextStep ) {
|
||||||
await updateProfileItems( { completed: true } );
|
await updateProfileItems( { completed: true } );
|
||||||
|
@ -135,7 +146,7 @@ class ProfileWizard extends Component {
|
||||||
step,
|
step,
|
||||||
goToNextStep: this.goToNextStep,
|
goToNextStep: this.goToNextStep,
|
||||||
} );
|
} );
|
||||||
const steps = getSteps().map( _step => pick( _step, [ 'key', 'label' ] ) );
|
const steps = this.getSteps().map( _step => pick( _step, [ 'key', 'label', 'isComplete' ] ) );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -148,11 +159,12 @@ class ProfileWizard extends Component {
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
withSelect( select => {
|
withSelect( select => {
|
||||||
const { getProfileItemsError } = select( 'wc-api' );
|
const { getProfileItems, getProfileItemsError } = select( 'wc-api' );
|
||||||
|
|
||||||
const isError = Boolean( getProfileItemsError() );
|
return {
|
||||||
|
isError: Boolean( getProfileItemsError() ),
|
||||||
return { isError };
|
profileItems: getProfileItems(),
|
||||||
|
};
|
||||||
} ),
|
} ),
|
||||||
withDispatch( dispatch => {
|
withDispatch( dispatch => {
|
||||||
const { updateProfileItems } = dispatch( 'wc-api' );
|
const { updateProfileItems } = dispatch( 'wc-api' );
|
||||||
|
|
|
@ -14,87 +14,87 @@ export default withState( {
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
isPending: false,
|
isPending: false,
|
||||||
} )( ( { currentStep, isComplete, isPending, setState } ) => {
|
} )( ( { currentStep, isComplete, isPending, setState } ) => {
|
||||||
|
const goToStep = key => {
|
||||||
|
setState( { currentStep: key } );
|
||||||
|
};
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
key: 'first',
|
key: 'first',
|
||||||
label: 'First',
|
label: 'First',
|
||||||
description: 'Step item description',
|
description: 'Step item description',
|
||||||
content: <div>First step content.</div>,
|
content: <div>First step content.</div>,
|
||||||
|
onClick: goToStep,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'second',
|
key: 'second',
|
||||||
label: 'Second',
|
label: 'Second',
|
||||||
description: 'Step item description',
|
description: 'Step item description',
|
||||||
content: <div>Second step content.</div>,
|
content: <div>Second step content.</div>,
|
||||||
|
onClick: goToStep,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Third',
|
label: 'Third',
|
||||||
key: 'third',
|
key: 'third',
|
||||||
description: 'Step item description',
|
description: 'Step item description',
|
||||||
content: <div>Third step content.</div>,
|
content: <div>Third step content.</div>,
|
||||||
|
onClick: goToStep,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Fourth',
|
label: 'Fourth',
|
||||||
key: 'fourth',
|
key: 'fourth',
|
||||||
description: 'Step item description',
|
description: 'Step item description',
|
||||||
content: <div>Fourth step content.</div>,
|
content: <div>Fourth step content.</div>,
|
||||||
|
onClick: goToStep,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentIndex = steps.findIndex( s => currentStep === s.key );
|
const currentIndex = steps.findIndex( s => currentStep === s.key );
|
||||||
|
|
||||||
if ( isComplete ) {
|
if ( isComplete ) {
|
||||||
steps.forEach( s => s.isComplete = true );
|
steps.forEach( s => ( s.isComplete = true ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ isComplete ? (
|
{ isComplete ? (
|
||||||
<button onClick={ () => setState( { currentStep: 'first', isComplete: false } ) } >
|
<button onClick={ () => setState( { currentStep: 'first', isComplete: false } ) }>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={ () => setState( { currentStep: steps[ currentIndex - 1 ].key } ) }
|
|
||||||
disabled={ currentIndex < 1 }
|
|
||||||
>
|
|
||||||
Previous step
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
) : (
|
||||||
onClick={ () => setState( { currentStep: steps[ currentIndex + 1 ].key } ) }
|
<div>
|
||||||
disabled={ currentIndex >= steps.length - 1 }
|
<button
|
||||||
>
|
onClick={ () => setState( { currentStep: steps[ currentIndex - 1 ].key } ) }
|
||||||
Next step
|
disabled={ currentIndex < 1 }
|
||||||
</button>
|
>
|
||||||
<button
|
Previous step
|
||||||
onClick={ () => setState( { isComplete: true } ) }
|
</button>
|
||||||
disabled={ currentIndex !== steps.length - 1 }
|
<button
|
||||||
>
|
onClick={ () => setState( { currentStep: steps[ currentIndex + 1 ].key } ) }
|
||||||
Complete
|
disabled={ currentIndex >= steps.length - 1 }
|
||||||
</button>
|
>
|
||||||
<button
|
Next step
|
||||||
onClick={ () => setState( { isPending: ! isPending } ) }
|
</button>
|
||||||
>
|
<button
|
||||||
Toggle Spinner
|
onClick={ () => setState( { isComplete: true } ) }
|
||||||
</button>
|
disabled={ currentIndex !== steps.length - 1 }
|
||||||
</div>
|
>
|
||||||
) }
|
Complete
|
||||||
|
</button>
|
||||||
|
<button onClick={ () => setState( { isPending: ! isPending } ) }>Toggle Spinner</button>
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
|
||||||
<Stepper
|
<Stepper steps={ steps } currentStep={ currentStep } isPending={ isPending } />
|
||||||
steps={ steps }
|
|
||||||
currentStep={ currentStep }
|
|
||||||
isPending={ isPending }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Stepper
|
<Stepper
|
||||||
isPending={ isPending }
|
isPending={ isPending }
|
||||||
isVertical={ true }
|
isVertical={ true }
|
||||||
steps={ steps }
|
steps={ steps }
|
||||||
currentStep={ currentStep }
|
currentStep={ currentStep }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -24,11 +24,7 @@ class Stepper extends Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className="woocommerce-stepper_content">{ step.content }</div>;
|
||||||
<div className="woocommerce-stepper_content">
|
|
||||||
{ step.content }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -42,39 +38,45 @@ class Stepper extends Component {
|
||||||
<div className={ stepperClassName }>
|
<div className={ stepperClassName }>
|
||||||
<div className="woocommerce-stepper__steps">
|
<div className="woocommerce-stepper__steps">
|
||||||
{ steps.map( ( step, i ) => {
|
{ steps.map( ( step, i ) => {
|
||||||
const { key, label, description, isComplete } = step;
|
const { key, label, description, isComplete, onClick } = step;
|
||||||
const isCurrentStep = key === currentStep;
|
const isCurrentStep = key === currentStep;
|
||||||
const stepClassName = classnames( 'woocommerce-stepper__step', {
|
const stepClassName = classnames( 'woocommerce-stepper__step', {
|
||||||
'is-active': isCurrentStep,
|
'is-active': isCurrentStep,
|
||||||
'is-complete': 'undefined' !== typeof isComplete ? isComplete : currentIndex > i,
|
'is-complete': 'undefined' !== typeof isComplete ? isComplete : currentIndex > i,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const icon = isCurrentStep && isPending ? <Spinner /> : (
|
const icon =
|
||||||
<div className="woocommerce-stepper__step-icon">
|
isCurrentStep && isPending ? (
|
||||||
<span className="woocommerce-stepper__step-number">{ i + 1 }</span>
|
<Spinner />
|
||||||
<CheckIcon />
|
) : (
|
||||||
</div>
|
<div className="woocommerce-stepper__step-icon">
|
||||||
);
|
<span className="woocommerce-stepper__step-number">{ i + 1 }</span>
|
||||||
|
<CheckIcon />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LabelWrapper = 'function' === typeof onClick ? 'button' : 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={ key } >
|
<Fragment key={ key }>
|
||||||
<div
|
<div className={ stepClassName }>
|
||||||
className={ stepClassName }
|
<LabelWrapper
|
||||||
>
|
className="woocommerce-stepper__step-label-wrapper"
|
||||||
{ icon }
|
onClick={ 'function' === typeof onClick ? () => onClick( key ) : null }
|
||||||
<div className="woocommerce-stepper__step-text">
|
>
|
||||||
<span className="woocommerce-stepper__step-label">
|
{ icon }
|
||||||
{ label }
|
<div className="woocommerce-stepper__step-text">
|
||||||
</span>
|
<span className="woocommerce-stepper__step-label">{ label }</span>
|
||||||
{ description &&
|
{ description && (
|
||||||
<span className="woocommerce-stepper__step-description">
|
<span className="woocommerce-stepper__step-description">
|
||||||
{ description }
|
{ description }
|
||||||
</span>
|
</span>
|
||||||
}
|
) }
|
||||||
{ isCurrentStep && isVertical && this.renderCurrentStepContent() }
|
</div>
|
||||||
</div>
|
</LabelWrapper>
|
||||||
|
{ isCurrentStep && isVertical && this.renderCurrentStepContent() }
|
||||||
</div>
|
</div>
|
||||||
{ ! isVertical && <div className="woocommerce-stepper__step-divider" /> }
|
{ ! isVertical && <div className="woocommerce-stepper__step-divider" /> }
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
} ) }
|
} ) }
|
||||||
|
@ -101,13 +103,9 @@ Stepper.propTypes = {
|
||||||
steps: PropTypes.arrayOf(
|
steps: PropTypes.arrayOf(
|
||||||
PropTypes.shape( {
|
PropTypes.shape( {
|
||||||
/**
|
/**
|
||||||
* Key used to identify step.
|
* Content displayed when the step is active.
|
||||||
*/
|
*/
|
||||||
key: PropTypes.string.isRequired,
|
content: PropTypes.node,
|
||||||
/**
|
|
||||||
* Label displayed in stepper.
|
|
||||||
*/
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
/**
|
/**
|
||||||
* Description displayed beneath the label.
|
* Description displayed beneath the label.
|
||||||
*/
|
*/
|
||||||
|
@ -117,9 +115,17 @@ Stepper.propTypes = {
|
||||||
*/
|
*/
|
||||||
isComplete: PropTypes.bool,
|
isComplete: PropTypes.bool,
|
||||||
/**
|
/**
|
||||||
* Content displayed when the step is active.
|
* Key used to identify step.
|
||||||
*/
|
*/
|
||||||
content: PropTypes.node,
|
key: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* Label displayed in stepper.
|
||||||
|
*/
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* A function to be called when the step label is clicked.
|
||||||
|
*/
|
||||||
|
onClick: PropTypes.func,
|
||||||
} )
|
} )
|
||||||
).isRequired,
|
).isRequired,
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/** @format */
|
||||||
|
|
||||||
.woocommerce-stepper {
|
.woocommerce-stepper {
|
||||||
$step-icon-size: 24px;
|
$step-icon-size: 24px;
|
||||||
|
|
||||||
|
@ -8,11 +10,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-stepper__step {
|
.woocommerce-stepper__step {
|
||||||
display: inline-flex;
|
|
||||||
padding: $gap-smaller;
|
padding: $gap-smaller;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.woocommerce-stepper__step-label-wrapper {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.woocommerce-stepper__step-label-wrapper {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.woocommerce-stepper__step-text {
|
.woocommerce-stepper__step-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +57,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: $gap-small;
|
margin-right: $gap-small;
|
||||||
max-height: $step-icon-size;
|
max-height: $step-icon-size;
|
||||||
min-width: auto;
|
min-width: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: $studio-blue-50;
|
background: $studio-blue-50;
|
||||||
|
@ -127,7 +145,7 @@
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: $step-icon-size / 2 + $gap-smaller;
|
left: $step-icon-size / 2 + $gap-smaller;
|
||||||
top: $step-icon-size + ( $gap-smaller * 2 );
|
top: $step-icon-size + ($gap-smaller * 2);
|
||||||
height: calc(100% - #{$step-icon-size} - #{ $gap-smaller * 2 });
|
height: calc(100% - #{$step-icon-size} - #{ $gap-smaller * 2 });
|
||||||
border-left: 1px solid $studio-gray-5;
|
border-left: 1px solid $studio-gray-5;
|
||||||
}
|
}
|
||||||
|
@ -153,6 +171,7 @@
|
||||||
|
|
||||||
.woocommerce-stepper_content {
|
.woocommerce-stepper_content {
|
||||||
margin-top: $gap-smaller;
|
margin-top: $gap-smaller;
|
||||||
|
margin-left: $gap-small + $step-icon-size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue