Skip to content

Commit a2c4ca0

Browse files
Merge pull request #5191 from kobotoolbox/task-884-implement-reusable-export-button
[TASK-884] Implement reusable export button
2 parents 1c7ccea + 72d942a commit a2c4ca0

File tree

4 files changed

+123
-6
lines changed

4 files changed

+123
-6
lines changed

jsapp/js/components/activity/activityLogs.query.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {keepPreviousData, useQuery} from '@tanstack/react-query';
22
import type {KoboSelectOption} from 'js/components/common/koboSelect';
3-
import type {PaginatedResponse} from 'js/dataInterface';
3+
import type {FailResponse, PaginatedResponse} from 'js/dataInterface';
44
import moment from 'moment';
55
import {QueryKeys} from 'js/query/queryKeys';
66

@@ -70,6 +70,26 @@ const getFilterOptions = async () =>
7070
setTimeout(() => resolve(mockOptions), 1000);
7171
});
7272

73+
/**
74+
* Starts the exporting process of the activity logs.
75+
* @returns {Promise<void>} The promise that starts the export
76+
*/
77+
const startActivityLogsExport = async () =>
78+
new Promise<void>((resolve, reject) => {
79+
// Simulates backend export process.
80+
setTimeout(() => {
81+
if (Math.random() > 0.5) {
82+
resolve();
83+
} else {
84+
const failResponse: FailResponse = {
85+
status: 500,
86+
statusText: 'Mocked error',
87+
};
88+
reject(failResponse);
89+
}
90+
}, 500);
91+
});
92+
7393
/**
7494
*
7595
* This is a hook that fetches activity logs from the server.
@@ -94,3 +114,9 @@ export const useActivityLogsFilterOptionsQuery = () =>
94114
queryKey: [QueryKeys.activityLogsFilter],
95115
queryFn: () => getFilterOptions(),
96116
});
117+
118+
/**
119+
* This is a hook to start the exporting process of the activity logs.
120+
* @returns {() => void} The function to start the export
121+
*/
122+
export const useExportActivityLogs = () => startActivityLogsExport;

jsapp/js/components/activity/formActivity.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import '../../../scss/components/_kobo.form-view.scss';
44
import type {KoboSelectOption} from '../common/koboSelect';
55
import KoboSelect from '../common/koboSelect';
66
import type {UniversalTableColumn} from 'jsapp/js/universalTable/universalTable.component';
7-
import Button from '../common/button';
87
import PaginatedQueryUniversalTable from 'jsapp/js/universalTable/paginatedQueryUniversalTable.component';
98
import type {ActivityLogsItem} from './activityLogs.query';
109
import {
1110
useActivityLogsFilterOptionsQuery,
1211
useActivityLogsQuery,
12+
useExportActivityLogs,
1313
} from './activityLogs.query';
1414
import styles from './formActivity.module.scss';
1515
import cx from 'classnames';
1616
import {formatTime} from 'jsapp/js/utils';
1717
import Avatar from '../common/avatar';
18+
import ExportToEmailButton from '../exportToEmailButton/exportToEmailButton.component';
1819

1920
const EventDescription = ({
2021
who,
@@ -56,6 +57,8 @@ export default function FormActivity() {
5657
const [selectedFilterOption, setSelectedFilterOption] =
5758
useState<KoboSelectOption | null>(null);
5859

60+
const exportData = useExportActivityLogs();
61+
5962
const handleFilterChange = (value: string | null) => {
6063
setSelectedFilterOption(
6164
filterOptions?.find((option) => option.value === value) || null
@@ -78,11 +81,9 @@ export default function FormActivity() {
7881
placeholder={t('Filter by')}
7982
options={filterOptions || []}
8083
/>
81-
<Button
82-
size='m'
83-
type='primary'
84-
startIcon='download'
84+
<ExportToEmailButton
8585
label={t('Export all data')}
86+
exportFunction={exportData}
8687
/>
8788
</div>
8889
</div>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Button from '../common/button';
2+
import KoboPrompt from '../modals/koboPrompt';
3+
import {useState} from 'react';
4+
import {handleApiFail} from 'jsapp/js/api';
5+
import type {FailResponse} from 'jsapp/js/dataInterface';
6+
7+
const MessageModal = ({onClose}: {onClose: () => void}) => (
8+
<KoboPrompt
9+
isOpen
10+
onRequestClose={onClose}
11+
title={t('Exporting data')}
12+
buttons={[
13+
{
14+
label: 'Ok',
15+
onClick: onClose,
16+
},
17+
]}
18+
>
19+
{t(
20+
"Your export request is currently being processed. Once the export is complete, you'll receive an email with all the details."
21+
)}
22+
</KoboPrompt>
23+
);
24+
25+
/**
26+
* Button to be used in views that export data to email.
27+
* The button receives a label and an export function that should return a promise.
28+
* The function is called when the button is clicked and if no error occurs, a message is shown to the user.
29+
*/
30+
export default function ExportToEmailButton({
31+
exportFunction,
32+
label,
33+
}: {
34+
exportFunction: () => Promise<void>;
35+
label: string;
36+
}) {
37+
const [isMessageOpen, setIsMessageOpen] = useState(false);
38+
const [isPending, setIsPending] = useState(false);
39+
40+
const handleClick = () => {
41+
setIsPending(true);
42+
exportFunction()
43+
.then(() => {
44+
setIsMessageOpen(true);
45+
})
46+
.catch((error) => handleApiFail(error as FailResponse))
47+
.finally(() => {
48+
setIsPending(false);
49+
});
50+
};
51+
52+
return (
53+
<>
54+
<Button
55+
size='m'
56+
type='primary'
57+
label={label}
58+
startIcon='download'
59+
onClick={handleClick}
60+
isPending={isPending}
61+
/>
62+
{isMessageOpen && (
63+
<MessageModal onClose={() => setIsMessageOpen(false)} />
64+
)}
65+
</>
66+
);
67+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type {Meta, StoryFn} from '@storybook/react';
2+
3+
import ExportToEmailButton from './exportToEmailButton.component';
4+
5+
export default {
6+
title: 'misc/ExportToEmailButton',
7+
component: ExportToEmailButton,
8+
argTypes: {
9+
label: {
10+
control: 'text',
11+
},
12+
},
13+
} as Meta<typeof ExportToEmailButton>;
14+
15+
const Template: StoryFn<typeof ExportToEmailButton> = (args) => (
16+
<ExportToEmailButton {...args} />
17+
);
18+
19+
export const Primary = Template.bind({});
20+
Primary.args = {
21+
label: 'Export all data',
22+
exportFunction: () => new Promise((resolve) => setTimeout(resolve, 500)),
23+
};

0 commit comments

Comments
 (0)