Skip to content

Commit

Permalink
Merge pull request #4873 from kobotoolbox/task-542-transcription-esti…
Browse files Browse the repository at this point in the history
…mation

[TASK-542] Display automated transcription estimate
  • Loading branch information
LMNTL committed Apr 1, 2024
2 parents 9f012d0 + 999e583 commit 2d75ef4
Show file tree
Hide file tree
Showing 30 changed files with 398 additions and 214 deletions.
4 changes: 0 additions & 4 deletions jsapp/js/bemComponents.ts
Expand Up @@ -27,10 +27,6 @@ bem.KoboSelect__optionBadge = makeBem(bem.KoboSelect, 'option-badge');
bem.PageWrapper = makeBem(null, 'page-wrapper');
bem.PageWrapper__content = makeBem(bem.PageWrapper, 'content');

bem.Loading = makeBem(null, 'loading');
bem.Loading__inner = makeBem(bem.Loading, 'inner');
bem.Loading__msg = makeBem(bem.Loading, 'msg');

bem.EmptyContent = makeBem(null, 'empty-content', 'section');
bem.EmptyContent__icon = makeBem(bem.EmptyContent, 'icon', 'i');
bem.EmptyContent__title = makeBem(bem.EmptyContent, 'title', 'h1');
Expand Down
7 changes: 1 addition & 6 deletions jsapp/js/components/bigModal/bigModal.es6
Expand Up @@ -4,7 +4,6 @@ import autoBind from 'react-autobind';
import Reflux from 'reflux';
import alertify from 'alertifyjs';
import {actions} from 'js/actions';
import bem from 'js/bem';
import LoadingSpinner from 'js/components/common/loadingSpinner';
import Modal from 'js/components/common/modal';
import {stores} from 'js/stores';
Expand Down Expand Up @@ -386,11 +385,7 @@ class BigModal extends React.Component {
}
{ this.props.params.type === MODAL_TYPES.SUBMISSION && !this.state.sid &&
<div>
<bem.Loading>
<bem.Loading__inner>
<i className='k-spin k-icon k-icon-spinner'/>
</bem.Loading__inner>
</bem.Loading>
<LoadingSpinner message={false} />
</div>
}
{ this.props.params.type === MODAL_TYPES.TABLE_SETTINGS &&
Expand Down
20 changes: 20 additions & 0 deletions jsapp/js/components/common/centeredMessage.component.tsx
@@ -0,0 +1,20 @@
import type {ReactElement} from 'react';
import React from 'react';
import styles from './centeredMessage.module.scss';

interface CenteredMessageProps {
message: ReactElement<any, any> | string;
}

/**
* A centered message component.
*/
export default function CenteredMessage(props: CenteredMessageProps) {
return (
<figure className={styles.centeredMessage}>
<div className={styles.centeredMessageInner}>
{props.message}
</div>
</figure>
);
}
16 changes: 16 additions & 0 deletions jsapp/js/components/common/centeredMessage.module.scss
@@ -0,0 +1,16 @@
.centeredMessage {
display: table;
vertical-align: middle;
height: 100%;
width: 100%;
font-size: 18px;
margin: 0;
}

.centeredMessageInner {
display: table-cell;
vertical-align: middle;
text-align: center;
padding-left: 20px;
padding-right: 20px;
}
2 changes: 1 addition & 1 deletion jsapp/js/components/common/koboImage.tsx
Expand Up @@ -49,7 +49,7 @@ class KoboImage extends React.Component<KoboImageProps, KoboImageState> {
return (
<bem.KoboImage >
{this.state.isLoading &&
<LoadingSpinner hideMessage/>
<LoadingSpinner message={false} />
}

{!this.state.isLoading &&
Expand Down
99 changes: 99 additions & 0 deletions jsapp/js/components/common/loadingSpinner.module.scss
@@ -0,0 +1,99 @@
@use '~kobo-common/src/styles/colors';
@use 'scss/_variables';

// Loading messages
.loading {
display: table;
vertical-align: middle;
height: 100%;
width: 100%;
font-size: 18px;
opacity: 0.8;
}

// Adjust spinning icon position for the regular spinner
.loadingTypeRegular {
i:first-child {
vertical-align: -7px;
display: inline-block;
}
}

// Moves message a bit off center to make it appear centered (ellipsis causes
// this optical illusion)
.loadingHasDefaultMessage {
.loadingMessage {
padding-left: 5px;
}
}

.loadingMessage {
margin-top: 10px;
display: block;
}

.loadingInner {
display: table-cell;
vertical-align: middle;
text-align: center;
padding-left: 20px;
padding-right: 20px;
overflow: hidden; // avoids spinner icon overflowing scrollable areas

code {
margin: 20px;
padding: 15px;
font-size: 13px;
display: block;
background: colors.$kobo-white;
width: 80%;
max-height: 300px;
overflow: auto;
word-wrap: break-word;
text-align: left;
}
}

@keyframes rotate {
from {transform: rotateZ(360deg);}
to {transform: rotateZ(0deg);}
}

$spinner-size: 64px;
$spinner-line-size: 10px;
$spinner-mask-size: $spinner-size * 0.5 - $spinner-line-size;

.bigSpinner {
display: block;
margin: 0 auto;
position: relative;
height: $spinner-size;
width: $spinner-size;
border-radius: 50%;
background: conic-gradient(#{colors.$kobo-blue}, transparent);
animation: rotate 1s linear infinite;
mask-image: radial-gradient(circle, transparent $spinner-mask-size, black 33%);
}

.bigSpinner::before,
.bigSpinner::after {
content: '';
position: absolute;
border-radius: 50%;
}

.bigSpinner::before {
width: $spinner-size - (2 * $spinner-line-size);
height: $spinner-size - (2 * $spinner-line-size);
top: $spinner-line-size;
left: $spinner-line-size;
background-color: colors.$kobo-white;
}

.bigSpinner::after {
height: $spinner-line-size;
width: $spinner-line-size;
background-color: colors.$kobo-blue;
top: 0;
left: ($spinner-size - $spinner-line-size) * 0.5;
}
49 changes: 0 additions & 49 deletions jsapp/js/components/common/loadingSpinner.scss

This file was deleted.

45 changes: 45 additions & 0 deletions jsapp/js/components/common/loadingSpinner.stories.tsx
@@ -0,0 +1,45 @@
import React from 'react';
import type {ComponentStory, ComponentMeta} from '@storybook/react';
import LoadingSpinner from './loadingSpinner';
import type {LoadingSpinnerType} from './loadingSpinner';

const spinnerTypes: LoadingSpinnerType[] = ['regular', 'big'];

export default {
title: 'common/LoadingSpinner',
component: LoadingSpinner,
argTypes: {
type: {
description: 'Type of LoadingSpinner',
options: spinnerTypes,
control: 'radio',
},
message: {
options: [
undefined,
false,
'Please wait things are coming…',
'Please wait until this process finishes, as there are a lot of things going on in the background. But since you started reading this, most probably the whole thing have already finished…',
],
description:
'Displayed underneath the animating spinner. If custom message is not provided, default message will be displayed. If `false` is passed, message will not be displayed.',
control: 'select',
},
},
} as ComponentMeta<typeof LoadingSpinner>;

const Template: ComponentStory<typeof LoadingSpinner> = (args) => (
<LoadingSpinner {...args} />
);

export const Regular = Template.bind({});
Regular.args = {
type: 'regular',
message: 'To infinity and beyond…',
};

export const Big = Template.bind({});
Big.args = {
type: 'big',
message: 'Working on it…',
};
62 changes: 37 additions & 25 deletions jsapp/js/components/common/loadingSpinner.tsx
@@ -1,36 +1,48 @@
import React from 'react';
import bem from 'js/bem';
import './loadingSpinner.scss';
import cx from 'classnames';
import styles from './loadingSpinner.module.scss';
import Icon from 'js/components/common/icon';

export type LoadingSpinnerType = 'regular' | 'big';

interface LoadingSpinnerProps {
message?: string;
/** Changes the looks of the spinner animation. */
type?: LoadingSpinnerType;
/**
* Most of the times we want a message, either custom or default one, but
* sometimes we want just the spinner. We need a boolean to hide it, because
* component has a fallback message.
* There is a default message if nothing is provided. If you want to hide
* the message completely, pass `false`.
*/
hideMessage?: boolean;
hideSpinner?: boolean;
message?: string | boolean;
'data-cy'?: string;
}

export default class LoadingSpinner extends React.Component<
LoadingSpinnerProps,
{}
> {
render() {
const message = this.props.message || t('loading…');
/**
* Displays a spinner animation above a customizable yet optional message.
*/
export default function LoadingSpinner(props: LoadingSpinnerProps) {
const spinnerType: LoadingSpinnerType = props.type || 'regular';
const message = props.message || t('loading…');

return (
<div
className={cx({
[styles.loading]: true,
[styles.loadingTypeRegular]: spinnerType === 'regular',
[styles.loadingHasDefaultMessage]: props.message === undefined,
})}
data-cy={props['data-cy']}
>
<div className={styles.loadingInner}>
{spinnerType === 'regular' && (
<Icon name='spinner' size='xl' classNames={['k-spin']} />
)}

{spinnerType === 'big' && <span className={styles.bigSpinner} />}

return (
<bem.Loading data-cy={this.props['data-cy']}>
<bem.Loading__inner>
{!this.props.hideSpinner && (
<Icon name='spinner' size='xl' classNames={['k-spin']} />
)}
{!this.props.hideMessage && message}
</bem.Loading__inner>
</bem.Loading>
);
}
{props.message !== false && (
<span className={styles.loadingMessage}>{message}</span>
)}
</div>
</div>
);
}
7 changes: 2 additions & 5 deletions jsapp/js/components/formNotFound.js
@@ -1,16 +1,13 @@
import React from 'react';
import bem from 'js/bem';
import CenteredMessage from 'js/components/common/centeredMessage.component';

export default class FormNotFound extends React.Component {
render() {
return (
<bem.uiPanel>
<bem.uiPanel__body>
<bem.Loading>
<bem.Loading__inner>
{t('path not found / recognized')}
</bem.Loading__inner>
</bem.Loading>
<CenteredMessage message={t('path not found / recognized')}/>
</bem.uiPanel__body>
</bem.uiPanel>
);
Expand Down
7 changes: 2 additions & 5 deletions jsapp/js/components/formXform.js
@@ -1,6 +1,7 @@
import React from 'react';
import {dataInterface} from 'js/dataInterface';
import bem from 'js/bem';
import CenteredMessage from 'js/components/common/centeredMessage.component';

export default class FormXform extends React.Component {
constructor(props) {
Expand All @@ -23,11 +24,7 @@ export default class FormXform extends React.Component {
return (
<bem.uiPanel>
<bem.uiPanel__body>
<bem.Loading>
<bem.Loading__inner>
<p>XForm is loading</p>
</bem.Loading__inner>
</bem.Loading>
<CenteredMessage message={t('XForm is loading')} />
</bem.uiPanel__body>
</bem.uiPanel>
);
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/components/languages/languageSelector.tsx
Expand Up @@ -390,7 +390,7 @@ class LanguageSelector extends React.Component<
pageStart={0}
loadMore={this.fetchMoreLanguages.bind(this)}
hasMore={this.store.hasMoreLanguages}
loader={<LoadingSpinner hideMessage key='loadingspinner'/>}
loader={<LoadingSpinner message={false} key='loadingspinner'/>}
useWindow={false}
>
<ul key='unorderedlist'>
Expand Down

0 comments on commit 2d75ef4

Please sign in to comment.