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

CBIIT / crdc-datahub-ui / 9553410815

17 Jun 2024 07:13PM UTC coverage: 31.563% (+0.4%) from 31.21%
9553410815

push

github

web-flow
Merge pull request #396 from CBIIT/CRDCDH-1160

CRDCDH-1160 Organization dropdown sorting

938 of 3716 branches covered (25.24%)

Branch coverage included in aggregate %.

8 of 29 new or added lines in 5 files covered. (27.59%)

2 existing lines in 1 file now uncovered.

1630 of 4420 relevant lines covered (36.88%)

94.95 hits per line

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

0.0
/src/content/dataSubmissions/DataSubmissionsListView.tsx
1
import React, { FC, useMemo, useState } from "react";
2
import { Link, useLocation } from "react-router-dom";
3
import {
4
  Alert,
5
  Container,
6
  Stack,
7
  styled,
8
  Table,
9
  TableBody,
10
  TableCell,
11
  TableContainer,
12
  TableHead,
13
  TablePagination,
14
  TableRow,
15
  TableSortLabel,
16
  Typography,
17
  FormControl,
18
  Select,
19
  MenuItem,
20
  Box,
21
} from "@mui/material";
22
import { useSnackbar } from "notistack";
23
import { useQuery } from "@apollo/client";
24
import { query, Response } from "../../graphql/listSubmissions";
25
import bannerSvg from "../../assets/banner/submission_banner.png";
26
import PageBanner from "../../components/PageBanner";
27
import { FormatDate } from "../../utils";
28
import { useAuthContext } from "../../components/Contexts/AuthContext";
29
import SuspenseLoader from "../../components/SuspenseLoader";
30
import usePageTitle from "../../hooks/usePageTitle";
31
import CreateDataSubmissionDialog from "./CreateDataSubmissionDialog";
32
import {
33
  Status,
34
  useOrganizationListContext,
35
} from "../../components/Contexts/OrganizationListContext";
36

37
type T = Submission;
38

39
type Column = {
40
  label: string;
41
  value: (a: T, user: User) => React.ReactNode;
42
  field?: string;
43
  default?: true;
44
};
45

46
const StyledBannerBody = styled(Stack)({
×
47
  marginTop: "-20px",
48
});
49

50
const StyledContainer = styled(Container)({
×
51
  marginTop: "-62px",
52
});
53

54
const StyledTableContainer = styled(TableContainer)({
×
55
  borderRadius: "8px",
56
  border: "1px solid #083A50",
57
  marginBottom: "25px",
58
  position: "relative",
59
});
60

61
const OrganizationStatusContainer = styled("div")({
×
62
  height: "45px",
63
  display: "flex",
64
  alignItems: "center",
65
  justifyContent: "flex-start",
66
  paddingLeft: "6px",
67
});
68

69
const StyledHeaderCell = styled(TableCell)({
×
70
  fontWeight: 700,
71
  fontSize: "16px",
72
  color: "#fff !important",
73
  "&.MuiTableCell-root": {
74
    padding: "8px 8px",
75
    color: "#fff !important",
76
  },
77
  "& .MuiSvgIcon-root,  & .MuiButtonBase-root": {
78
    color: "#fff !important",
79
  },
80
  "&:last-of-type": {
81
    paddingRight: "4px",
82
  },
83
});
84

85
const StyledTableCell = styled(TableCell)({
×
86
  fontSize: "16px",
87
  color: "#083A50 !important",
88
  "&.MuiTableCell-root": {
89
    padding: "8px 8px",
90
    overflowWrap: "anywhere",
91
  },
92
  "&:last-of-type": {
93
    paddingRight: "4px",
94
  },
95
});
96

97
const StyledInlineLabel = styled("label")({
×
98
  paddingLeft: "10px",
99
  fontWeight: "700",
100
  fontSize: "16px",
101
});
102

103
const StyledFormControl = styled(FormControl)({
×
104
  margin: "10px 0",
105
  minWidth: "0",
106
});
107

108
const baseTextFieldStyles = {
×
109
  borderRadius: "8px",
110
  minWidth: "300px",
111
  "& .MuiInputBase-input": {
112
    fontWeight: 400,
113
    fontSize: "16px",
114
    fontFamily: "'Nunito', 'Rubik', sans-serif",
115
    padding: "10px",
116
    height: "20px",
117
  },
118
  "& .MuiOutlinedInput-notchedOutline": {
119
    borderColor: "#6B7294",
120
  },
121
  "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
122
    border: "1px solid #209D7D",
123
    boxShadow:
124
      "2px 2px 4px 0px rgba(38, 184, 147, 0.10), -1px -1px 6px 0px rgba(38, 184, 147, 0.20)",
125
  },
126
  "& .Mui-disabled": {
127
    cursor: "not-allowed",
128
  },
129
  "& .Mui-readOnly.MuiOutlinedInput-input:read-only": {
130
    backgroundColor: "#E5EEF4",
131
    color: "#083A50",
132
    cursor: "not-allowed",
133
    borderRadius: "8px",
134
  },
135
  "& .MuiList-root": {
136
    padding: "0 !important",
137
  },
138
  "& .MuiMenuItem-root.Mui-selected": {
139
    background: "#3E7E6D !important",
140
    color: "#FFFFFF !important",
141
  },
142
  "& .MuiMenuItem-root:hover": {
143
    background: "#D5EDE5",
144
  },
145
};
146

147
const StyledSelect = styled(Select)(baseTextFieldStyles);
×
148

149
const StyledDeletedText = styled(Box)(({ theme }) => ({
×
150
  color: theme.palette.text.disabled,
151
}));
152

153
const columns: Column[] = [
×
154
  {
155
    label: "Submission Name",
156
    value: (a) =>
157
      a.status === "Deleted" ? (
×
158
        <StyledDeletedText>{a.name}</StyledDeletedText>
159
      ) : (
160
        <Link to={`/data-submission/${a._id}/data-activity`}>{a.name}</Link>
161
      ),
162
    field: "name",
163
  },
164
  {
165
    label: "Submitter",
166
    value: (a) => a.submitterName,
×
167
    field: "submitterName",
168
  },
169
  {
170
    label: "Data Commons",
171
    value: (a) => a.dataCommons,
×
172
    field: "dataCommons",
173
  },
174
  {
175
    label: "Type",
176
    value: (a) => a.intention,
×
177
    field: "intention",
178
  },
179
  {
180
    label: "DM Version",
181
    value: (a) => a.modelVersion,
×
182
    field: "modelVersion",
183
  },
184
  {
185
    label: "Organization",
186
    value: (a) => a.organization.name,
×
187
    field: "organization.name",
188
  },
189
  {
190
    label: "Study",
191
    value: (a) => a.studyAbbreviation,
×
192
    field: "studyAbbreviation",
193
  },
194
  {
195
    label: "dbGaP ID",
196
    value: (a) => a.dbGaPID,
×
197
    field: "dbGaPID",
198
  },
199
  {
200
    label: "Status",
201
    value: (a) => a.status,
×
202
    field: "status",
203
  },
204
  {
205
    label: "Primary Contact",
206
    value: (a) => a.conciergeName,
×
207
    field: "conciergeName",
208
  },
209
  {
210
    label: "Created Date",
211
    value: (a) => (a.createdAt ? FormatDate(a.createdAt, "M/D/YYYY h:mm A") : ""),
×
212
    field: "createdAt",
213
  },
214
  {
215
    label: "Last Updated",
216
    value: (a) => (a.updatedAt ? FormatDate(a.updatedAt, "M/D/YYYY h:mm A") : ""),
×
217
    field: "updatedAt",
218
    default: true,
219
  },
220
];
221

222
const statusValues: string[] = [
×
223
  "All",
224
  "New",
225
  "In Progress",
226
  "Submitted",
227
  "Released",
228
  "Withdrawn",
229
  "Rejected",
230
  "Completed",
231
  "Archived",
232
  "Canceled",
233
  "Deleted",
234
];
235
const statusOptionArray: SelectOption[] = statusValues.map((v) => ({
×
236
  label: v,
237
  value: v,
238
}));
239
/**
240
 * View for List of Questionnaire/Submissions
241
 *
242
 * @returns {JSX.Element}
243
 */
244
const ListingView: FC = () => {
×
245
  usePageTitle("Data Submission List");
×
246

247
  const { state } = useLocation();
×
248
  const { user } = useAuthContext();
×
249
  const { enqueueSnackbar } = useSnackbar();
×
NEW
250
  const { status: orgStatus, activeOrganizations: allOrganizations } = useOrganizationListContext();
×
251

252
  const [order, setOrder] = useState<"asc" | "desc">("desc");
×
253
  const [orderBy, setOrderBy] = useState<Column>(
×
254
    columns.find((c) => c.default) || columns.find((c) => c.field)
×
255
  );
256

257
  // Only org owners/submitters with organizations assigned can create data submissions
258
  const orgOwnerOrSubmitter = user?.role === "Organization Owner" || user?.role === "Submitter";
×
259
  const hasOrganizationAssigned = user?.organization !== null && user?.organization?.orgID !== null;
×
260
  const shouldHaveAllFilter =
261
    user?.role === "Admin" ||
×
262
    user?.role === "Federal Lead" ||
263
    user?.role === "Data Curator" ||
264
    user?.role === "Data Commons POC";
265
  const [page, setPage] = useState<number>(0);
×
266
  const [perPage, setPerPage] = useState<number>(10);
×
267
  const [organizationFilter, setOrganizationFilter] = useState<string>(
×
268
    // eslint-disable-next-line no-nested-ternary
269
    shouldHaveAllFilter ? "All" : hasOrganizationAssigned ? user.organization?.orgName : "All"
×
270
  );
271
  const [statusFilter, setStatusFilter] = useState<string>("All");
×
272

273
  const { data, loading, error, refetch } = useQuery<Response>(query, {
×
274
    variables: {
275
      first: perPage,
276
      offset: page * perPage,
277
      sortDirection: order.toUpperCase(),
278
      orderBy: orderBy.field,
279
      organization:
280
        organizationFilter !== "All"
×
NEW
281
          ? allOrganizations?.find((org) => org.name === organizationFilter)?._id
×
282
          : "All",
283
      status: statusFilter,
284
    },
285
    context: { clientName: "backend" },
286
    fetchPolicy: "no-cache",
287
  });
288

289
  // eslint-disable-next-line arrow-body-style
290
  const emptyRows = useMemo(() => {
×
291
    return page > 0 && data?.listSubmissions?.total
×
292
      ? Math.max(0, (1 + page) * perPage - (data?.listSubmissions?.total || 0))
×
293
      : 0;
294
  }, [data]);
295

296
  const handleRequestSort = (column: Column) => {
×
297
    setOrder(orderBy === column && order === "asc" ? "desc" : "asc");
×
298
    setOrderBy(column);
×
299
  };
300

301
  const handleChangeRowsPerPage = (event) => {
×
302
    setPerPage(parseInt(event.target.value, 10));
×
303
    setPage(0);
×
304
  };
305

306
  const handleOnCreateSubmission = () => {
×
307
    refetch();
×
308
    enqueueSnackbar("Data Submission Created Successfully", {
×
309
      variant: "success",
310
    });
311
  };
312

NEW
313
  const organizationNames: SelectOption[] = allOrganizations?.map((org) => ({
×
314
    label: org.name,
315
    value: org.name,
316
  }));
317
  organizationNames?.unshift({ label: "All", value: "All" });
×
318

319
  return (
×
320
    <>
321
      <PageBanner
322
        title="Data Submission List"
323
        subTitle="Below is a list of data submissions that are associated with your account. Please click on any of the data submissions to review or continue work."
324
        padding="57px 0 0 25px"
325
        body={
326
          <StyledBannerBody direction="row" alignItems="center" justifyContent="flex-end">
327
            {/* NOTE For MVP-2: Organization Owners are just Users */}
328
            {/* Create a submission only available to org owners and submitters that have organizations assigned */}
329
            <CreateDataSubmissionDialog
330
              organizations={allOrganizations}
331
              onCreate={handleOnCreateSubmission}
332
            />
333
          </StyledBannerBody>
334
        }
335
        bannerSrc={bannerSvg}
336
      />
337
      <StyledContainer maxWidth="xl">
338
        {(state?.error || error) && (
×
339
          <Alert sx={{ mb: 3, p: 2 }} severity="error">
340
            {state?.error || "An error occurred while loading the data."}
×
341
          </Alert>
342
        )}
343

344
        <StyledTableContainer>
345
          <Table>
346
            <TableHead>
347
              <TableRow>
348
                <TableCell colSpan={12}>
349
                  <OrganizationStatusContainer>
350
                    <StyledInlineLabel htmlFor="data-submissions-table-organization">
351
                      Organization
352
                    </StyledInlineLabel>
353
                    <StyledFormControl>
354
                      <StyledSelect
355
                        sx={{
356
                          minWidth: "300px",
357
                          marginLeft: "24px",
358
                          marginRight: "64px",
359
                        }}
360
                        value={organizationFilter}
361
                        MenuProps={{ disablePortal: true }}
362
                        inputProps={{
363
                          id: "data-submissions-table-organization",
364
                        }}
365
                        readOnly={orgOwnerOrSubmitter || user?.role === "User"}
×
366
                        onChange={(e) => setOrganizationFilter(e.target.value as unknown as string)}
×
367
                      >
368
                        {organizationNames?.map(({ value, label }) => (
369
                          <MenuItem key={value} value={value}>
×
370
                            {label}
371
                          </MenuItem>
372
                        ))}
373
                      </StyledSelect>
374
                    </StyledFormControl>
375
                    <StyledInlineLabel htmlFor="data-submissions-table-status">
376
                      Status
377
                    </StyledInlineLabel>
378
                    <StyledFormControl>
379
                      <StyledSelect
380
                        sx={{
381
                          minWidth: "300px",
382
                          marginLeft: "24px",
383
                          marginRight: "64px",
384
                        }}
385
                        value={statusFilter}
386
                        MenuProps={{ disablePortal: true }}
387
                        inputProps={{ id: "data-submissions-table-status" }}
388
                        onChange={(e) => setStatusFilter(e.target.value as unknown as string)}
×
389
                      >
390
                        {statusOptionArray.map(({ value, label }) => (
391
                          <MenuItem key={value} value={value}>
×
392
                            {label}
393
                          </MenuItem>
394
                        ))}
395
                      </StyledSelect>
396
                    </StyledFormControl>
397
                  </OrganizationStatusContainer>
398
                </TableCell>
399
              </TableRow>
400
              <TableRow sx={{ background: "#083A50" }}>
401
                {columns.map((col: Column, index) => (
402
                  <StyledHeaderCell
×
403
                    sx={{ paddingLeft: index === 0 ? "32px !important" : "" }}
×
404
                    key={col.label}
405
                  >
406
                    {col.field ? (
×
407
                      <TableSortLabel
408
                        active={orderBy === col}
409
                        direction={orderBy === col ? order : "asc"}
×
410
                        onClick={() => handleRequestSort(col)}
×
411
                      >
412
                        {col.label}
413
                      </TableSortLabel>
414
                    ) : (
415
                      col.label
416
                    )}
417
                  </StyledHeaderCell>
418
                ))}
419
              </TableRow>
420
            </TableHead>
421
            <TableBody>
422
              {(loading || orgStatus === Status.LOADING) && (
×
423
                <TableRow>
424
                  <TableCell>
425
                    <SuspenseLoader fullscreen={false} />
426
                  </TableCell>
427
                </TableRow>
428
              )}
429
              {data?.listSubmissions?.submissions?.map((d: T, index) => (
430
                <TableRow
×
431
                  sx={{ background: index % 2 === 0 ? "#fff" : "#E3EEF9" }}
×
432
                  tabIndex={-1}
433
                  hover
434
                  key={d["_id"]}
435
                >
436
                  {columns.map((col: Column, index) => (
437
                    <StyledTableCell
×
438
                      sx={{ paddingLeft: index === 0 ? "32px !important" : "" }}
×
439
                      key={`${d["_id"]}_${col.label}`}
440
                    >
441
                      {col.value(d, user)}
442
                    </StyledTableCell>
443
                  ))}
444
                </TableRow>
445
              ))}
446

447
              {/* Fill the difference between perPage and count to prevent height changes */}
448
              {emptyRows > 0 && (
×
449
                <TableRow style={{ height: 53 * emptyRows }}>
450
                  <TableCell colSpan={columns.length} />
451
                </TableRow>
452
              )}
453

454
              {/* No content message */}
455
              {(!data?.listSubmissions?.total || data?.listSubmissions?.total === 0) && (
×
456
                <TableRow style={{ height: 53 * 10 }}>
457
                  <TableCell colSpan={columns.length}>
458
                    <Typography variant="h6" align="center" fontSize={18} color="#757575">
459
                      There are no data submissions associated with your account
460
                    </Typography>
461
                  </TableCell>
462
                </TableRow>
463
              )}
464
            </TableBody>
465
          </Table>
466
          <TablePagination
467
            rowsPerPageOptions={[5, 10, 20, 50]}
468
            component="div"
469
            count={data?.listSubmissions?.total || 0}
×
470
            rowsPerPage={perPage}
471
            page={page}
472
            onPageChange={(e, newPage) => setPage(newPage)}
×
473
            onRowsPerPageChange={handleChangeRowsPerPage}
474
            nextIconButtonProps={{
475
              disabled:
476
                perPage === -1 ||
×
477
                !data?.listSubmissions ||
478
                data?.listSubmissions?.total === 0 ||
479
                data?.listSubmissions?.total <= (page + 1) * perPage ||
480
                emptyRows > 0 ||
481
                loading ||
482
                orgStatus === Status.LOADING,
483
            }}
484
            SelectProps={{
485
              inputProps: { "aria-label": "rows per page" },
486
              native: true,
487
            }}
488
            backIconButtonProps={{
489
              disabled: page === 0 || loading || orgStatus === Status.LOADING,
×
490
            }}
491
          />
492
        </StyledTableContainer>
493
      </StyledContainer>
494
    </>
495
  );
496
};
497

498
export default ListingView;
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