• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cfpb / ccdb5-ui / 30943832050

04 Aug 2026 07:33PM UTC coverage: 88.124%. First build
30943832050

Pull #619

github

web-flow
Merge 6c912d4fd into 42d26b663
Pull Request #619: DATAP-2042 - Remove JSON format

1309 of 1603 branches covered (81.66%)

Branch coverage included in aggregate %.

16 of 18 new or added lines in 2 files covered. (88.89%)

2839 of 3104 relevant lines covered (91.46%)

206.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.47
/src/components/dialogs/data-export/data-export.js
1
import './data-export.scss';
2
import { getFullUrl, sendAnalyticsEvent } from '../../../utils';
3
import { buildAllResultsUri, buildSomeResultsUri } from './data-export-utils';
4
import { modalHidden, modalShown } from '../../../reducers/view/view-slice';
5
import { useDispatch, useSelector } from 'react-redux';
6
import { Button, Heading } from '@cfpb/design-system-react';
7
import { useMemo, useState } from 'react';
8
import { MODAL_TYPE_EXPORT_CONFIRMATION } from '../../../constants';
9
import { selectQueryRoot } from '../../../reducers/query/selectors';
10
import { selectViewTab } from '../../../reducers/view/selectors';
11
import { selectFiltersRoot } from '../../../reducers/filters/selectors';
12
import { useGetAggregations } from '../../../api/hooks/use-get-aggregations';
13
import { getElementById } from '../../../utils/dom';
14

15
const FORMAT_CSV = 'csv';
4✔
16
const FORMAT_JSON = 'json';
4✔
17

18
const DATASET_FILTERED = 'filtered';
4✔
19
const DATASET_FULL = 'full';
4✔
20
const FILTER_MAX = 1e5;
4✔
21

22
export const DataExport = () => {
4✔
23
  const dispatch = useDispatch();
25✔
24
  const queryState = useSelector(selectQueryRoot);
25✔
25
  const filtersState = useSelector(selectFiltersRoot);
25✔
26
  const tab = useSelector(selectViewTab);
25✔
27
  const { data } = useGetAggregations();
25✔
28
  const someComplaintsCount = data?.total || 0;
25✔
29
  const allComplaintsCount = data?.doc_count || 0;
25✔
30
  const isFullDatasetOnly = someComplaintsCount === allComplaintsCount;
25✔
31

32
  // can only be full or filtered
33
  const [dataset, setDataset] = useState(
25✔
34
    someComplaintsCount > FILTER_MAX ? DATASET_FULL : DATASET_FILTERED,
25✔
35
  );
36
  // can only be csv or json
37
  const [format, setFormat] = useState(FORMAT_CSV);
25✔
38

39
  const [copied, setCopied] = useState(false);
25✔
40

41
  const exportDataset = isFullDatasetOnly ? DATASET_FULL : dataset;
25✔
42
  // Filtered exports are CSV-only; format options (CSV/JSON) are for the full dataset zip.
43
  const showFormatOptions = exportDataset === DATASET_FULL;
25✔
44
  const exportFormat = showFormatOptions ? format : FORMAT_CSV;
25✔
45

46
  const exportUri = useMemo(() => {
25✔
47
    const mergedState = {
17✔
48
      ...filtersState,
49
      ...queryState,
50
    };
51
    const url =
52
      exportDataset === DATASET_FULL
17✔
53
        ? buildAllResultsUri(exportFormat)
54
        : buildSomeResultsUri(someComplaintsCount, mergedState);
55
    return getFullUrl(url);
17✔
56
  }, [
57
    exportDataset,
58
    exportFormat,
59
    someComplaintsCount,
60
    filtersState,
61
    queryState,
62
  ]);
63

64
  const handleExportClicked = () => {
25✔
65
    if (exportDataset === DATASET_FULL) {
3✔
66
      sendAnalyticsEvent('Export All Data', tab + ':' + exportFormat);
2✔
67
    } else {
68
      sendAnalyticsEvent('Export Some Data', tab);
1✔
69
    }
70

71
    location.assign(exportUri);
3✔
72
    dispatch(modalShown(MODAL_TYPE_EXPORT_CONFIRMATION));
3✔
73
  };
74

75
  const selectDataset = (nextDataset) => {
25✔
76
    setCopied(false);
4✔
77
    setDataset(nextDataset);
4✔
78
    if (nextDataset === DATASET_FILTERED) {
4✔
79
      setFormat(FORMAT_CSV);
2✔
80
    }
81
  };
82

83
  const copyToClipboard = (ev) => {
25✔
84
    const uriControl = getElementById('export-uri-input');
1✔
85
    uriControl.select();
1✔
86
    // For mobile devices
87
    uriControl.setSelectionRange(0, 99_999);
1✔
88
    navigator.clipboard.writeText(uriControl.value);
1✔
89
    ev.target.focus();
1✔
90

91
    setCopied(true);
1✔
92
  };
93

94
  const instructions = showFormatOptions
25✔
95
    ? isFullDatasetOnly
20✔
96
      ? 'To download a copy of this dataset, choose the file format below.'
97
      : 'To download a copy of this dataset, choose the file format and which complaints you want to export below.'
98
    : 'To download a copy of this dataset, choose which complaints you want to export below.';
99

100
  return (
25✔
101
    <section className="export-modal">
102
      <div className="header layout-row">
103
        <Heading type="3" className="flex-all">
104
          Export complaints
105
        </Heading>
106
        <Button
107
          label="Close"
108
          iconRight="error-round"
109
          isLink
110
          data-gtm_ignore="true"
111
          onClick={() => {
112
            dispatch(modalHidden());
2✔
113
          }}
114
        />
115
      </div>
116
      <div className="body">
117
        <div className="instructions">{instructions}</div>
118
        {showFormatOptions ? (
25✔
119
          <div className="group">
120
            <div className="group-title">
121
              Select a format for the exported file
122
            </div>
123
            <div>
124
              <div className="m-form-field m-form-field--radio m-form-field--lg-target">
125
                <input
126
                  checked={exportFormat === FORMAT_CSV}
127
                  className="a-radio"
128
                  id="format-csv"
129
                  onChange={() => {
NEW
130
                    setCopied(false);
×
NEW
131
                    setFormat(FORMAT_CSV);
×
132
                  }}
133
                  type="radio"
134
                  value="csv"
135
                />
136
                <label className="a-label" htmlFor="format-csv">
137
                  CSV
138
                </label>
139
              </div>
140
              <div className="m-form-field m-form-field--radio m-form-field--lg-target">
141
                <input
142
                  checked={exportFormat === FORMAT_JSON}
143
                  className="a-radio"
144
                  id="format-json"
145
                  onChange={() => {
146
                    setCopied(false);
2✔
147
                    setFormat(FORMAT_JSON);
2✔
148
                  }}
149
                  type="radio"
150
                  value="json"
151
                />
152
                <label className="a-label" htmlFor="format-json">
153
                  JSON
154
                </label>
155
              </div>
156
            </div>
157
          </div>
158
        ) : null}
159
        {isFullDatasetOnly ? null : (
25✔
160
          <div className="group">
161
            <div className="group-title">
162
              Select which complaints you’d like to export
163
            </div>
164
            <div>
165
              <div className="m-form-field m-form-field--radio m-form-field--lg-target">
166
                <input
167
                  checked={dataset === DATASET_FILTERED}
168
                  disabled={someComplaintsCount > FILTER_MAX}
169
                  className="a-radio"
170
                  id="dataset-filtered"
171
                  onChange={() => {
172
                    selectDataset(DATASET_FILTERED);
2✔
173
                  }}
174
                  type="radio"
175
                  value="filtered"
176
                />
177
                <label className="a-label" htmlFor="dataset-filtered">
178
                  {'Filtered dataset (' +
179
                    someComplaintsCount.toLocaleString() +
180
                    ' complaints)'}
181
                  <br />
182
                  {someComplaintsCount > FILTER_MAX
8!
183
                    ? `(limited to ${FILTER_MAX.toLocaleString()} complaints or fewer)`
184
                    : '(only the results of the last search and/or filter)'}
185
                </label>
186
              </div>
187
              <div className="m-form-field m-form-field--radio m-form-field--lg-target">
188
                <input
189
                  checked={dataset === DATASET_FULL}
190
                  className="a-radio"
191
                  id="dataset-full"
192
                  onChange={() => {
193
                    selectDataset(DATASET_FULL);
2✔
194
                  }}
195
                  type="radio"
196
                  value="full"
197
                />
198
                <label className="a-label" htmlFor="dataset-full">
199
                  {'Full dataset (' +
200
                    allComplaintsCount.toLocaleString() +
201
                    ' complaints)'}
202
                  <br />
203
                  (large, zipped file of every complaint)
204
                </label>
205
              </div>
206
            </div>
207
          </div>
208
        )}
209

210
        <div className="heres-the-url">
211
          <Heading type="4">
212
            Link to your complaint search results for future reference
213
          </Heading>
214
          <div className="layout-row">
215
            <input
216
              className="flex-all a-text-input"
217
              id="export-uri-input"
218
              type="text"
219
              value={exportUri}
220
              readOnly
221
            />
222
            <Button
223
              label={copied ? 'Copied' : 'Copy'}
25✔
224
              iconLeft={copied ? 'checkmark-round' : 'copy'}
25✔
225
              className={`a-btn ${
226
                copied ? 'export-url-copied' : 'a-btn__secondary'
25✔
227
              }`}
228
              disabled={!exportUri}
229
              onClick={copyToClipboard}
230
            />
231
          </div>
232
        </div>
233
        <div className="timeliness-warning">
234
          The export process could take several minutes if you’re downloading
235
          many complaints
236
        </div>
237
      </div>
238
      <div className="footer layout-row">
239
        <Button
240
          label="Start export"
241
          data-gtm_ignore="true"
242
          onClick={() => {
243
            handleExportClicked();
3✔
244
          }}
245
        />
246
        <Button
247
          label="Cancel"
248
          isLink
249
          appearance="warning"
250
          data-gtm_ignore="true"
251
          onClick={() => {
252
            dispatch(modalHidden());
1✔
253
          }}
254
        />
255
      </div>
256
    </section>
257
  );
258
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc