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

CBIIT / bento-c3dc-frontend / 20382662153

19 Dec 2025 09:11PM UTC coverage: 0.175% (-0.005%) from 0.18%
20382662153

Pull #439

github

web-flow
Merge 391a80f6b into 88005922a
Pull Request #439: C3dc 2011

6 of 3499 branches covered (0.17%)

Branch coverage included in aggregate %.

0 of 180 new or added lines in 5 files covered. (0.0%)

26 existing lines in 7 files now uncovered.

10 of 5643 relevant lines covered (0.18%)

0.09 hits per line

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

0.0
/src/components/CohortModal/utils.js
1
import { DOWNLOAD_MANIFEST_KEYS, CCDI_HUB_BASE_URL, CCDI_INTEROP_SERVICE_URL, CCDI_HUB_LEGACY_BASE_URL, CCDI_HUB_DBGAP_PARAM, DEFAULT_QUERY_LIMIT } from '../../bento/cohortModalData';
×
2
import { GET_COHORT_MANIFEST_QUERY, GET_COHORT_METADATA_QUERY } from '../../bento/dashboardTabData';
×
3
import client from '../../utils/graphqlClient';
×
4

5
function generateDownloadFileName(isManifest, cohortID) {
6
    const date = new Date();
×
7
    const yyyy = date.getFullYear();
×
8
    let dd = date.getDate();
×
9
    let mm = (date.getMonth() + 1);
×
10
  
11
    if (dd < 10) { dd = `0${dd}`; }
×
12
  
13
    if (mm < 10) { mm = `0${mm}`; }
×
14
  
15
    const todaysDate = `${yyyy}-${mm}-${dd}`;
×
16
  
17
    let hours = date.getHours();
×
18
    let minutes = date.getMinutes();
×
19
    let seconds = date.getSeconds();
×
20
  
21
    if (hours < 10) { hours = `0${hours}`; }
×
22
  
23
    if (minutes < 10) { minutes = `0${minutes}`; }
×
24
  
25
    if (seconds < 10) { seconds = `0${seconds}`; }
×
26

27
    if (isManifest) {
×
28
        const fileName = 'Manifest';
×
29
        return `${fileName}_${cohortID}_${todaysDate} ${hours}-${minutes}-${seconds}${'.csv'}`;
×
30
    }
31
    else {
32
        const fileName = 'Metadata';
×
33
        return `${fileName}_${cohortID}_${todaysDate} ${hours}-${minutes}-${seconds}${'.json'}`;
×
34
    }
35
}
36

37
function cleanGraphQLTypenames(obj) {
38
    if (Array.isArray(obj)) {
×
39
      return obj.map(item => cleanGraphQLTypenames(item));
×
40
    } else if (typeof obj === 'object' && obj !== null) {
×
41
      const newObj = {};
×
42
      Object.keys(obj).forEach(key => {
×
43
        if (key !== '__typename') {
×
44
          newObj[key] = cleanGraphQLTypenames(obj[key]);
×
45
        }
46
      });
47
      return newObj;
×
48
    }
49
    return obj; 
×
50
  };
51

52
  /**
53
   * Helper function to format array values for CSV export
54
   * Handles GraphQL response arrays (like race, diagnosis, etc.) by flattening them
55
   * into single CSV cell values using semicolon separation.
56
   *
57
   * @param {*} value - The value to format (may be array or primitive)
58
   * @returns {string} - Formatted value safe for CSV export
59
   *
60
   * Example: ['White', 'Asian'] -> 'White; Asian'
61
   */
62
  const formatArrayForCSV = (value) => {
×
63
    if (Array.isArray(value)) {
×
64
      // Join array elements with semicolon separator to avoid comma conflicts in CSV
65
      const joinedValue = value.filter(item => item !== null && item !== undefined && item !== '').join('; ');
×
66
      return joinedValue || '';
×
67
    }
68
    return value || '';
×
69
  };
70

71
  /**
72
   * Downloads cohort data as CSV file with proper handling of array fields.
73
   * Automatically flattens array values (like race) into semicolon-separated strings
74
   * to prevent CSV parsing issues with multiple columns.
75
   *
76
   * @param {Array} arr - Array of data objects to export
77
   * @param {string} cohortID - Cohort identifier for filename
78
   */
79
  export const arrayToCSVDownload = (arr, cohortID) => {
×
80
    const keys = Object.keys(DOWNLOAD_MANIFEST_KEYS);
×
81
    const header = keys.join(',');
×
82
    const rows = arr.map((row) => {
×
83
        return keys.map((k) => {
×
84
            let value = row[DOWNLOAD_MANIFEST_KEYS[k]];
×
85

86
            if (row.participant) {
×
87
                if (k === 'Participant ID') {
×
88
                    value = row.participant.participant_id || '';
×
89
                } else if (k === 'Sex at Birth') {
×
90
                    value = row.participant.sex_at_birth || '';
×
91
                } else if (k === 'Race') {
×
92
                    value = row.participant.race || '';
×
93
                }
94
            }
95

96
            // Handle all potential array fields uniformly
97
            value = formatArrayForCSV(value);
×
98

99
            // Escape CSV special characters
100
            if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) {
×
101
                value = `"${value.replace(/"/g, '""')}"`;
×
102
            }
103

104
            return value;
×
105
        }).join(',');
106
    });
107

108
    const csvData = [header, ...rows].join('\n');
×
109

110
    const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8' });
×
111
    const JsonURL = window.URL.createObjectURL(blob);
×
112

113
    let tempLink = document.createElement('a');
×
114
    tempLink.setAttribute('href', JsonURL);
×
115
    tempLink.setAttribute('download', generateDownloadFileName(true, cohortID));
×
116
    document.body.appendChild(tempLink);
×
117
    tempLink.click();
×
118
    document.body.removeChild(tempLink);
×
119
};
×
120

121

122

123
export const objectToJsonDownload = (obj, cohortID) => {
×
124
    const cleanedObj = cleanGraphQLTypenames(obj);
×
125
  
126
    const json = JSON.stringify({ [cohortID]: cleanedObj }, null, 2);
×
127
    const blob = new Blob([json], { type: 'application/json' });
×
128
    const JsonURL = window.URL.createObjectURL(blob);
×
129
    
130
    let tempLink = document.createElement('a');
×
131
    tempLink.setAttribute('href', JsonURL);
×
132
    tempLink.setAttribute('download', generateDownloadFileName(false, cohortID));
×
133
    document.body.appendChild(tempLink);
×
134
    tempLink.click();
×
135
    document.body.removeChild(tempLink);
×
136
  };
×
137

138
  export const hasUnsavedChanges = (obj1, obj2, ignoredFields) => {
×
139
    if (!obj1 || !obj2) return false; // If either object is undefined, consider no changes
×
140
  
141
    const sharedKeys = Object.keys(obj1).filter(key => key in obj2 && !ignoredFields.includes(key) );
×
142
    const filteredObj1 = sharedKeys.reduce((acc, key) => {
×
143
        acc[key] = obj1[key];
×
144
        return acc;
×
145
    }, {});
146
    const filteredObj2 = sharedKeys.reduce((acc, key) => {
×
147
        acc[key] = obj2[key];
×
148
        return acc;
×
149
    }, {});
150
    return JSON.stringify(filteredObj1) !== JSON.stringify(filteredObj2);
×
151
  };
152

153
// Helper function to create manifest payload for interop service
×
154
export const getManifestPayload = (participants) => {
×
155
  if (!participants || !Array.isArray(participants)) {
×
156
    return [];
×
157
  }
158
  
159
  // Group participants by study_id (dbgap_accession)
160
  const studyGroups = participants.reduce((acc, participant) => {
×
161
    const studyId = participant.dbgap_accession;
×
162
    
163
    // Skip participants with missing or invalid dbgap_accession
164
    if (studyId === undefined || studyId === null || studyId === '') {
×
165
      console.warn('Participant missing dbgap_accession:', participant.participant_id || 'Unknown participant');
×
166
      return acc;
×
167
    }
168
    
169
    if (!acc[studyId]) {
×
170
      acc[studyId] = {
×
171
        study_id: studyId,
172
        participant_id: []
173
      };
174
    }
175
    acc[studyId].participant_id.push(participant.participant_id);
×
176
    return acc;
×
177
  }, {});
178
  
179
  // Convert to array format
180
  return Object.values(studyGroups);
×
181
};
182

183
// Helper function to truncate signed CloudFront URLs at .json
×
184
export const truncateSignedUrl = (url) => {
×
185
  if (!url || typeof url !== 'string') return url;
×
186

187
  const jsonIndex = url.indexOf('.json');
×
188
  if (jsonIndex !== -1) {
×
189
    return url.substring(0, jsonIndex + 5); // +5 to include ".json"
×
190
  }
191

192
  return url; // Return original if no .json found
×
193
};
194

195
// Centralized CCDI Hub export utility
×
196
export const exportToCCDIHub = async (participants, options = {}) => {
×
197
  const {
198
    showAlert,
199
    useInteropService = true,
×
200
    onLoadingStateChange
201
  } = options;
×
202

203
  // Validate input
204
  if (!participants || !Array.isArray(participants) || participants.length === 0) {
×
205
    if (showAlert) showAlert('error', 'No participants available for export.');
×
206
    return null;
×
207
  }
208

209
  // Configuration constants are now imported at module level for better performance
210

211
  try {
×
212
    if (useInteropService) {
×
213
      // Use the robust interop service approach (recommended)
214
      if (onLoadingStateChange) onLoadingStateChange(true);
×
215

216
      const manifestPayload = getManifestPayload(participants);
×
217
      if (!manifestPayload || manifestPayload.length === 0) {
×
218
        throw new Error('Unable to generate manifest payload from participants');
×
219
      }
220

221
      const query = `
×
222
        query storeManifest($manifestString: String!, $type: String!) {
223
          storeManifest(manifest: $manifestString, type: $type)
224
        }
225
      `;
226

227
      const response = await fetch(CCDI_INTEROP_SERVICE_URL, {
×
228
        method: 'POST',
229
        headers: {
230
          'Content-Type': 'application/json',
231
        },
232
        body: JSON.stringify({
233
          query,
234
          variables: {
235
            manifestString: JSON.stringify(manifestPayload),
236
            type: "json"
237
          }
238
        })
239
      });
240

241
      if (!response.ok) {
×
242
        throw new Error(`HTTP error! status: ${response.status}`);
×
243
      }
244

245
      const result = await response.json();
×
246

247
      if (result.errors) {
×
248
        const errorMessage = (result.errors[0] && result.errors[0].message) || 'Unknown GraphQL error';
×
249
        const participantCount = manifestPayload ? manifestPayload.length : 0;
×
250
        throw new Error(`CCDI Interop Service Error: ${errorMessage} (Processing ${participantCount} study groups)`);
×
251
      }
252

253
      // Process and open the URL
254
      const processedUrl = result.data.storeManifest ? truncateSignedUrl(result.data.storeManifest) : null;
×
255
      if (!processedUrl) {
×
256
        throw new Error('No valid URL returned from interop service');
×
257
      }
258

259
      const finalUrl = `${CCDI_HUB_BASE_URL}${processedUrl}`;
×
260
      window.open(finalUrl, '_blank');
×
261

262
      if (showAlert) showAlert('success', 'CCDI Hub opened in new tab!');
×
263
      return finalUrl;
×
264

265
    } else {
266
      // Fallback to direct URL construction (legacy approach)
267
      console.warn('Using legacy direct URL construction. Consider updating to interop service approach.');
×
268

269
      const participantIds = participants.map(p => p.participant_id).join("|");
×
270
      const dbgapAccessions = [...new Set(participants.map(p => p.dbgap_accession))].join("|");
×
271

272
      const finalUrl = `${CCDI_HUB_LEGACY_BASE_URL}${participantIds}${CCDI_HUB_DBGAP_PARAM}${dbgapAccessions}`;
×
273

274
      // Check for potential URL length issues
275
      if (finalUrl.length > 2000) {
×
276
        console.warn('Generated URL may be too long for some browsers. Consider using interop service approach.');
×
277
      }
278

279
      window.open(finalUrl, '_blank');
×
280
      if (showAlert) showAlert('success', 'CCDI Hub opened in new tab!');
×
281
      return finalUrl;
×
282
    }
283

284
  } catch (error) {
285
    console.error('Error exporting to CCDI Hub:', error);
×
286
    if (showAlert) {
×
287
      showAlert('error', `Failed to export to CCDI Hub: ${error.message}`);
×
288
    }
289
    return null;
×
290
  } finally {
291
    if (onLoadingStateChange) onLoadingStateChange(false);
×
292
  }
293
};
294

295
// Centralized download functions for cohort data
×
296
export const downloadCohortManifest = async (participants, cohortId, options = {}) => {
×
297
  const { showAlert, onLoadingStateChange } = options;
×
298

299
  try {
×
300
    if (onLoadingStateChange) onLoadingStateChange(true);
×
301

302
    const participantPKs = participants.map(item => item.participant_pk);
×
303
    const { data } = await client.query({
×
304
      query: GET_COHORT_MANIFEST_QUERY,
305
      variables: {
306
        "participant_pk": participantPKs,
307
        "first": DEFAULT_QUERY_LIMIT
308
      },
309
    });
310

NEW
311
    arrayToCSVDownload(data['cohortManifest'], cohortId);
×
312
    if (showAlert) showAlert('success', 'Manifest CSV downloaded successfully!');
×
313

314
    return data;
×
315
  } catch (error) {
316
    console.error('Error downloading cohort manifest:', error);
×
317
    if (showAlert) showAlert('error', 'Failed to download manifest. Please try again.');
×
318
    throw error;
×
319
  } finally {
320
    if (onLoadingStateChange) onLoadingStateChange(false);
×
321
  }
322
};
×
323

324
export const downloadCohortMetadata = async (participants, cohortId, options = {}) => {
×
325
  const { showAlert, onLoadingStateChange } = options;
×
326

327
  try {
×
328
    if (onLoadingStateChange) onLoadingStateChange(true);
×
329

330
    const participantPKs = participants.map(item => item.participant_pk);
×
331
    const { data } = await client.query({
×
332
      query: GET_COHORT_METADATA_QUERY,
333
      variables: {
334
        "participant_pk": participantPKs,
335
        "first": DEFAULT_QUERY_LIMIT
336
      },
337
    });
338

339
    objectToJsonDownload(data['cohortMetadata'], cohortId);
×
340
    if (showAlert) showAlert('success', 'Metadata JSON downloaded successfully!');
×
341

342
    return data;
×
343
  } catch (error) {
344
    console.error('Error downloading cohort metadata:', error);
×
345
    if (showAlert) showAlert('error', 'Failed to download metadata. Please try again.');
×
346
    throw error;
×
347
  } finally {
348
    if (onLoadingStateChange) onLoadingStateChange(false);
×
349
  }
350
};
×
351

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc