Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(projectDownloads): migrate files to TypeScript #5426

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions jsapp/js/actions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,18 @@ interface GetExportDefinition extends Function {
}

interface GetExportCompletedDefinition extends Function {
(response: any): void;
listen: (callback: (response: any) => void) => Function;
(response: ExportDataResponse): void;
listen: (callback: (response: ExportDataResponse) => void) => Function;
}

interface GetExportSettingsDefinition extends Function {
(assetUid: string, options: {preselectLastSettings: boolean}): void;
completed: GetExportSettingsCompletedDefinition;
failed: GenericFailedDefinition;
}
interface GetExportSettingsCompletedDefinition extends Function {
(response: PaginatedResponse<ExportSetting>, passData: {}): void;
listen: (callback: (response: PaginatedResponse<ExportSetting>, passData?: {preselectLastSettings?: boolean}) => void) => Function;
}

interface TableUpdateSettingsDefinition extends Function {
Expand Down Expand Up @@ -256,6 +266,13 @@ export namespace actions {
const media: object;
const exports: {
getExport: GetExportDefinition;
getExports: GenericDefinition;
createExport: GenericDefinition;
deleteExport: GenericDefinition;
getExportSettings: GetExportSettingsDefinition;
updateExportSetting: GenericDefinition;
createExportSetting: GenericDefinition;
deleteExportSetting: GenericDefinition;
};
const dataShare: object;
}
4 changes: 2 additions & 2 deletions jsapp/js/assetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ export function renderQuestionTypeIcon(
*/
export function injectSupplementalRowsIntoListOfRows(
asset: AssetResponse,
rows: string[],
rows: Set<string> | Array<string>,
) {
if (asset.content?.survey === undefined) {
throw new Error('Asset has no content');
Expand Down Expand Up @@ -713,7 +713,7 @@ export function getAssetSubmissionProcessingUrl(
) {
const processingUrl = getAssetProcessingUrl(assetUid);
if (processingUrl) {
return processingUrl + '?submission=' + submission
return processingUrl + '?submission=' + submission;
}
return undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/components/common/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface CheckboxProps {
* `evt.stopPropagation()` and be happy.
*/
onClick?: (evt: React.MouseEvent<HTMLInputElement> | React.TouchEvent<HTMLInputElement>) => void;
label?: string;
label?: React.ReactNode;
/** Only needed if checkbox is in submittable form. */
name?: string;
'data-cy'?: string;
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/components/common/multiCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface MultiCheckboxProps {
* A MultiCheckbox generic component.
* Use optional `bem.MultiCheckbox__wrapper` to display a frame around it.
*/
export default function MultiCheckbox (props: MultiCheckboxProps) {
export default function MultiCheckbox(props: MultiCheckboxProps) {
function onChange(itemIndex: number, isChecked: boolean) {
const updatedList = props.items;
updatedList[itemIndex].checked = isChecked;
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/components/formSubScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const DataTable = React.lazy(() =>
);
const ProjectDownloads = React.lazy(() =>
import(
/* webpackPrefetch: true */ 'js/components/projectDownloads/projectDownloads'
/* webpackPrefetch: true */ 'js/components/projectDownloads/ProjectDownloads'
)
);
const FormGallery = React.lazy(() =>
Expand Down
6 changes: 3 additions & 3 deletions jsapp/js/components/locking/lockingConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface LockingRestrictionDefinition {
}

// all restrictions for questions and choices
export const QUESTION_RESTRICTIONS = [
export const QUESTION_RESTRICTIONS: LockingRestrictionDefinition[] = [
{name: LockingRestrictionName.choice_add, label: t('Add choice to question')},
{name: LockingRestrictionName.choice_delete, label: t('Remove choice from question')},
{name: LockingRestrictionName.choice_label_edit, label: t('Edit choice labels')},
Expand All @@ -62,7 +62,7 @@ export const QUESTION_RESTRICTIONS = [
];

// all restrictions for groups
export const GROUP_RESTRICTIONS = [
export const GROUP_RESTRICTIONS: LockingRestrictionDefinition[] = [
{name: LockingRestrictionName.group_delete, label: t('Delete entire group')},
{name: LockingRestrictionName.group_label_edit, label: t('Edit group labels')},
{name: LockingRestrictionName.group_question_add, label: t('Add question to group')},
Expand All @@ -74,7 +74,7 @@ export const GROUP_RESTRICTIONS = [
];

// all restrictions for form
export const FORM_RESTRICTIONS = [
export const FORM_RESTRICTIONS: LockingRestrictionDefinition[] = [
{name: LockingRestrictionName.form_appearance, label: t('Change form appearance')},
{name: LockingRestrictionName.form_meta_edit, label: t('Change form meta questions')},
{name: LockingRestrictionName.form_replace, label: t('Replace whole form')},
Expand Down
2 changes: 2 additions & 0 deletions jsapp/js/components/locking/lockingUtils.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const minimalAssetResponse = {
'csv_legacy': 'http://kc.kobo.local/zefir/exports/aBcDe12345/csv/',
'zip_legacy': 'http://kc.kobo.local/zefir/exports/aBcDe12345/zip/',
'kml_legacy': 'http://kc.kobo.local/zefir/exports/aBcDe12345/kml/',
'geojson': 'http://kc.kobo.local/zefir/exports/aBcDe12345/geojson/',
'spss_labels': 'http://kc.kobo.local/zefir/exports/aBcDe12345/spss/',
'xls': 'http://kc.kobo.local/zefir/reports/aBcDe12345/export.xlsx',
'csv': 'http://kc.kobo.local/zefir/reports/aBcDe12345/export.csv',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,62 @@
// Libraries
import React from 'react';
import autoBind from 'react-autobind';
import bem from 'js/bem';

// Partial components
import ExportTypeSelector from 'js/components/projectDownloads/ExportTypeSelector';
import Button from 'js/components/common/button';

// Stores, hooks and utilities
import {actions} from 'js/actions';
import {downloadUrl} from 'js/utils';
import {
EXPORT_STATUSES,
DEFAULT_EXPORT_SETTINGS,
} from 'js/components/projectDownloads/exportsConstants';
import {getContextualDefaultExportFormat} from 'js/components/projectDownloads/exportsUtils';
import exportsStore from 'js/components/projectDownloads/exportsStore';
import ExportTypeSelector from 'js/components/projectDownloads/exportTypeSelector';
import ExportFetcher from 'js/components/projectDownloads/exportFetcher';
import Button from 'js/components/common/button';

// Constants and types
import {
ExportStatusName,
DEFAULT_EXPORT_SETTINGS,
type ExportTypeDefinition,
} from 'js/components/projectDownloads/exportsConstants';
import type {AssetResponse, ExportDataResponse} from 'jsapp/js/dataInterface';

interface AnonymousExportsProps {
asset: AssetResponse;
}

interface AnonymousExportsState {
selectedExportType: ExportTypeDefinition;
isPending: boolean;
exportUrl: string | null;
}

/**
* A compontent that ROUTES.FORM_DOWNLOADS route is displayint for not logged in
* users. It allows to select an export type and download a file.
* @prop {object} asset
*/
export default class AnonymousExports extends React.Component {
constructor(props){
export default class AnonymousExports extends React.Component<
AnonymousExportsProps,
AnonymousExportsState
> {
constructor(props: AnonymousExportsProps){
super(props);
this.state = {
selectedExportType: exportsStore.getExportType(),
isPending: false,
exportUrl: null,
};
this.unlisteners = [];
autoBind(this);
}

private unlisteners: Function[] = [];
private exportFetcher?: ExportFetcher;

componentDidMount() {
this.unlisteners.push(
exportsStore.listen(this.onExportsStoreChange),
actions.exports.createExport.completed.listen(this.onCreateExportCompleted),
actions.exports.getExport.completed.listen(this.onGetExportCompleted),
exportsStore.listen(this.onExportsStoreChange.bind(this), this),
actions.exports.createExport.completed.listen(this.onCreateExportCompleted.bind(this)),
actions.exports.getExport.completed.listen(this.onGetExportCompleted.bind(this)),
);
}

Expand All @@ -49,19 +71,22 @@ export default class AnonymousExports extends React.Component {
});
}

onCreateExportCompleted(exportData) {
onCreateExportCompleted(exportData: ExportDataResponse) {
this.fetchExport(exportData.uid);
}

onGetExportCompleted(exportData) {
onGetExportCompleted(exportData: ExportDataResponse) {
this.checkExportFetcher(exportData.uid, exportData.status);

if (exportData.status === EXPORT_STATUSES.complete) {
if (exportData.status === ExportStatusName.complete) {
this.setState({
isPending: false,
exportUrl: exportData.result,
}, () => {
if (this.state.exportUrl !== null) {
downloadUrl(this.state.exportUrl);
}
});
downloadUrl(this.state.exportUrl);
}
}

Expand Down Expand Up @@ -90,19 +115,19 @@ export default class AnonymousExports extends React.Component {
}
}

checkExportFetcher(exportUid, exportStatus) {
checkExportFetcher(exportUid: string, exportStatus: ExportStatusName) {
if (
exportStatus !== EXPORT_STATUSES.error &&
exportStatus !== EXPORT_STATUSES.complete &&
!this.fetchIntervalId
exportStatus !== ExportStatusName.error &&
exportStatus !== ExportStatusName.complete &&
!this.exportFetcher
) {
this.exportFetcher = new ExportFetcher(this.props.asset.uid, exportUid);
}

// clean up after it is completed
if (
exportStatus === EXPORT_STATUSES.error ||
exportStatus === EXPORT_STATUSES.complete
exportStatus === ExportStatusName.error ||
exportStatus === ExportStatusName.complete
) {
if (this.exportFetcher) {
this.exportFetcher.stop();
Expand All @@ -111,7 +136,7 @@ export default class AnonymousExports extends React.Component {
}
}

fetchExport(exportUid) {
fetchExport(exportUid: string) {
actions.exports.getExport(this.props.asset.uid, exportUid);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import React from 'react';
import autoBind from 'react-autobind';
import Select from 'react-select';
import bem from 'js/bem';
import {EXPORT_TYPES} from 'js/components/projectDownloads/exportsConstants';
import {EXPORT_TYPES, type ExportTypeDefinition} from 'js/components/projectDownloads/exportsConstants';
import exportsStore from 'js/components/projectDownloads/exportsStore';

interface ExportTypeSelectorProps {
disabled?: boolean;
/** Hides legacy options */
noLegacy?: boolean;
}

interface ExportTypeSelectorState {
selectedExportType: ExportTypeDefinition;
}

/**
* This is a selector that is handling the currently selected export type and
* is storing it in exportsStore.
* @prop {boolean} [disabled]
* @prop {boolean} [noLegacy] hides legacy options
*/
export default class ExportTypeSelector extends React.Component {
constructor(props){
export default class ExportTypeSelector extends React.Component<
ExportTypeSelectorProps,
ExportTypeSelectorState
> {
constructor(props: ExportTypeSelectorProps) {
super(props);
this.state = {selectedExportType: exportsStore.getExportType()};
this.unlisteners = [];
autoBind(this);
}

private unlisteners: Function[] = [];

componentDidMount() {
this.unlisteners.push(
exportsStore.listen(this.onExportsStoreChange),
exportsStore.listen(this.onExportsStoreChange.bind(this), this),
);
}

Expand All @@ -33,13 +43,17 @@ export default class ExportTypeSelector extends React.Component {
this.setState({selectedExportType: exportsStore.getExportType()});
}

onSelectedExportTypeChange(newValue) {
exportsStore.setExportType(newValue);
onSelectedExportTypeChange(newValue: ExportTypeDefinition | null) {
// It's not really possible to have `null` here, as Select requires a value
// to always be set.
if (newValue !== null) {
exportsStore.setExportType(newValue);
}
}

render() {
// make xls topmost (as most popular)
const exportTypesOptions = [
const exportTypesOptions: ExportTypeDefinition[] = [
EXPORT_TYPES.xls,
EXPORT_TYPES.csv,
EXPORT_TYPES.geojson,
Expand All @@ -60,10 +74,10 @@ export default class ExportTypeSelector extends React.Component {
{t('Select export type')}
</bem.ProjectDownloads__title>

<Select
<Select<ExportTypeDefinition>
value={this.state.selectedExportType}
options={exportTypesOptions}
onChange={this.onSelectedExportTypeChange}
onChange={this.onSelectedExportTypeChange.bind(this)}
className='kobo-select'
classNamePrefix='kobo-select'
menuPlacement='auto'
Expand Down
Loading
Loading