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

CBIIT / crdc-datahub-ui / 16421677142

21 Jul 2025 03:47PM UTC coverage: 73.687% (+2.1%) from 71.57%
16421677142

Pull #787

github

web-flow
Merge 1ebb35bac into d93ea77f5
Pull Request #787: CRDCDH-2991 OMB Banner Dynamic Details

3844 of 4256 branches covered (90.32%)

Branch coverage included in aggregate %.

72 of 72 new or added lines in 3 files covered. (100.0%)

83 existing lines in 4 files now uncovered.

24583 of 34322 relevant lines covered (71.62%)

115.54 hits per line

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

90.87
/src/content/organizations/ListView.tsx
1
import {
1✔
2
  Alert,
3
  Box,
4
  Button,
5
  Container,
6
  FormControl,
7
  MenuItem,
8
  Stack,
9
  TableCell,
10
  TableHead,
11
  styled,
12
} from "@mui/material";
13
import { ElementType, FC, useEffect, useRef, useState } from "react";
1✔
14
import { Controller, useForm } from "react-hook-form";
1✔
15
import { Link, LinkProps, useLocation } from "react-router-dom";
1✔
16

17
import {
1✔
18
  useOrganizationListContext,
19
  Status as OrgStatus,
20
} from "../../components/Contexts/OrganizationListContext";
21
import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
1✔
22
import GenericTable, { Column } from "../../components/GenericTable";
1✔
23
import PageBanner from "../../components/PageBanner";
1✔
24
import StudyList from "../../components/StudyList";
1✔
25
import StyledTextField from "../../components/StyledFormComponents/StyledOutlinedInput";
1✔
26
import StyledSelect from "../../components/StyledFormComponents/StyledSelect";
1✔
27
import TruncatedText from "../../components/TruncatedText";
1✔
28
import type { ListOrgsResp } from "../../graphql";
29
import usePageTitle from "../../hooks/usePageTitle";
1✔
30
import { sortData } from "../../utils";
1✔
31

32
type T = ListOrgsResp["listPrograms"]["programs"][number];
33

34
type FilterForm = {
35
  organization: string;
36
  study: string;
37
  status: Organization["status"] | "All";
38
};
39

40
const StyledContainer = styled(Container)({
1✔
41
  marginTop: "-180px",
1✔
42
  paddingBottom: "90px",
1✔
43
});
1✔
44

45
const StyledButton = styled(Button)<{ component: ElementType } & LinkProps>({
1✔
46
  padding: "14px 20px",
1✔
47
  fontWeight: 700,
1✔
48
  fontSize: "16px",
1✔
49
  letterSpacing: "2%",
1✔
50
  lineHeight: "20.14px",
1✔
51
  borderRadius: "8px",
1✔
52
  color: "#fff",
1✔
53
  textTransform: "none",
1✔
54
  borderColor: "#26B893 !important",
1✔
55
  background: "#1B8369 !important",
1✔
56
  marginRight: "25px",
1✔
57
});
1✔
58

59
const StyledBannerBody = styled(Stack)({
1✔
60
  marginTop: "-53px",
1✔
61
});
1✔
62

63
const StyledFilterContainer = styled(Box)({
1✔
64
  display: "flex",
1✔
65
  alignItems: "center",
1✔
66
  justifyContent: "flex-start",
1✔
67
  paddingBottom: "10px",
1✔
68
});
1✔
69

70
const StyledTableHead = styled(TableHead)({
1✔
71
  background: "#083A50",
1✔
72
});
1✔
73

74
const StyledFormControl = styled(FormControl)({
1✔
75
  margin: "10px",
1✔
76
  marginRight: "15px",
1✔
77
  minWidth: "250px",
1✔
78
});
1✔
79

80
const StyledInlineLabel = styled("label")({
1✔
81
  padding: "0 10px",
1✔
82
  fontWeight: "700",
1✔
83
});
1✔
84

85
const StyledHeaderCell = styled(TableCell)({
1✔
86
  fontWeight: 700,
1✔
87
  fontSize: "14px",
1✔
88
  color: "#fff !important",
1✔
89
  "&.MuiTableCell-root": {
1✔
90
    padding: "8px 16px",
1✔
91
    color: "#fff !important",
1✔
92
  },
1✔
93
  "& .MuiSvgIcon-root,  & .MuiButtonBase-root": {
1✔
94
    color: "#fff !important",
1✔
95
  },
1✔
96
  "&:last-of-type": {
1✔
97
    textAlign: "center",
1✔
98
  },
1✔
99
});
1✔
100

101
const StyledTableCell = styled(TableCell)({
1✔
102
  fontSize: "14px",
1✔
103
  color: "#083A50 !important",
1✔
104
  "&.MuiTableCell-root": {
1✔
105
    padding: "8px 16px",
1✔
106
  },
1✔
107
  "&:last-of-type": {
1✔
108
    textAlign: "center",
1✔
109
  },
1✔
110
});
1✔
111

112
const StyledActionButton = styled(Button)(
1✔
113
  ({ bg, text, border }: { bg: string; text: string; border: string }) => ({
1✔
114
    background: `${bg} !important`,
222✔
115
    borderRadius: "8px",
222✔
116
    border: `2px solid ${border}`,
222✔
117
    color: `${text} !important`,
222✔
118
    width: "100px",
222✔
119
    height: "30px",
222✔
120
    textTransform: "none",
222✔
121
    fontWeight: 700,
222✔
122
    fontSize: "16px",
222✔
123
  })
222✔
124
);
1✔
125

126
type TouchedState = { [K in keyof FilterForm]: boolean };
127

128
const initialTouchedFields: TouchedState = {
1✔
129
  organization: false,
1✔
130
  study: false,
1✔
131
  status: false,
1✔
132
};
1✔
133

134
const columns: Column<T>[] = [
1✔
135
  {
1✔
136
    label: "Name",
1✔
137
    renderValue: (a) => <TruncatedText text={a.name} maxCharacters={30} />,
1✔
138
    comparator: (a, b) => a.name.localeCompare(b.name),
1✔
139
    field: "name",
1✔
140
    default: true,
1✔
141
    sx: {
1✔
142
      width: "356px",
1✔
143
    },
1✔
144
  },
1✔
145
  {
1✔
146
    label: "Data Concierge",
1✔
147
    renderValue: (a) => <TruncatedText text={a.conciergeName} maxCharacters={15} />,
1✔
148
    comparator: (a, b) => (a?.conciergeName || "").localeCompare(b?.conciergeName || ""),
1✔
149
    field: "conciergeName",
1✔
150
    sx: {
1✔
151
      width: "290px",
1✔
152
    },
1✔
153
  },
1✔
154
  {
1✔
155
    label: "Studies",
1✔
156
    renderValue: ({ studies }) => {
1✔
157
      if (!studies || studies?.length < 1) {
295✔
158
        return "";
88✔
159
      }
88✔
160

161
      return (
207✔
162
        <StudyList
207✔
163
          studies={studies}
207✔
164
          emptyText=""
207✔
165
          renderStudy={(s) => (
207✔
166
            <TruncatedText text={s?.studyAbbreviation || s?.studyName} maxCharacters={37} />
155!
167
          )}
207✔
168
        />
169
      );
170
    },
295✔
171
    field: "studies",
1✔
172
    sortDisabled: true,
1✔
173
  },
1✔
174
  {
1✔
175
    label: "Status",
1✔
176
    renderValue: (a) => a.status,
1✔
177
    comparator: (a, b) => (a?.status || "").localeCompare(b?.status || ""),
1✔
178
    field: "status",
1✔
179
    sx: {
1✔
180
      width: "100px",
1✔
181
    },
1✔
182
  },
1✔
183
  {
1✔
184
    label: "Action",
1✔
185
    renderValue: (a) => (
1✔
186
      <Link to={`/programs/${a?._id}`}>
295✔
187
        <StyledActionButton bg="#C5EAF2" text="#156071" border="#84B4BE">
295✔
188
          Edit
189
        </StyledActionButton>
295✔
190
      </Link>
295✔
191
    ),
192
    sortDisabled: true,
1✔
193
    sx: {
1✔
194
      width: "100px",
1✔
195
    },
1✔
196
  },
1✔
197
];
198

199
/**
200
 * A view for the list of Programs.
201
 *
202
 * @returns The ListView component
203
 */
204
const ListView: FC = () => {
1✔
205
  usePageTitle("Manage Programs");
149✔
206

207
  const { state } = useLocation();
149✔
208
  const { data, status: orgStatus } = useOrganizationListContext();
149✔
209
  const { searchParams, setSearchParams } = useSearchParamsContext();
149✔
210
  const { watch, register, control, setValue } = useForm<FilterForm>({
149✔
211
    defaultValues: {
149✔
212
      organization: "",
149✔
213
      study: "",
149✔
214
      status: "Active",
149✔
215
    },
149✔
216
  });
149✔
217

218
  const [dataset, setDataset] = useState<T[]>([]);
149✔
219
  const [count, setCount] = useState<number>(0);
149✔
220
  const [touchedFilters, setTouchedFilters] = useState<TouchedState>(initialTouchedFields);
149✔
221

222
  const orgFilter = watch("organization");
149✔
223
  const studyFilter = watch("study");
149✔
224
  const statusFilter = watch("status");
149✔
225
  const tableRef = useRef<TableMethods>(null);
149✔
226

227
  const handleFetchData = async (fetchListing: FetchListing<T>, force: boolean) => {
149✔
228
    const { first, offset, sortDirection, orderBy, comparator } = fetchListing || {};
44!
229

230
    if (!data?.length) {
44!
231
      setDataset([]);
×
232
      setCount(0);
×
233
      return;
×
234
    }
×
235

236
    const filters: FilterFunction<T>[] = [
44✔
237
      (u: T) => {
44✔
238
        if (!orgFilter || orgFilter.trim().length < 1) {
132✔
239
          return true;
27✔
240
        }
27✔
241

242
        const nameMatch = u.name.toLowerCase().indexOf(orgFilter.toLowerCase()) !== -1;
105✔
243
        const abbrMatch = u.abbreviation
105✔
244
          ? u.abbreviation.toLowerCase().indexOf(orgFilter.toLowerCase()) !== -1
105!
245
          : false;
×
246

247
        return nameMatch || abbrMatch;
132✔
248
      },
132✔
249
      (u: T) => (statusFilter && statusFilter !== "All" ? u.status === statusFilter : true),
44!
250
      (u: T) => {
44✔
251
        if (!studyFilter || studyFilter.trim().length < 1) {
73!
252
          return true;
73✔
253
        }
73!
254

255
        const nameMatch = u?.studies?.some(
×
256
          (s) => s.studyName.toLowerCase().indexOf(studyFilter.toLowerCase()) !== -1
73✔
257
        );
73✔
258
        const abbrMatch = u?.studies?.some(
73!
259
          (s) => s.studyAbbreviation.toLowerCase().indexOf(studyFilter.toLowerCase()) !== -1
73✔
260
        );
73✔
261

262
        return nameMatch || abbrMatch;
73!
263
      },
44✔
264
    ];
265

266
    const filteredData = data.filter((u) => filters.every((filter) => filter(u)));
44✔
267
    const sortedData = sortData(filteredData, orderBy, sortDirection, comparator);
44✔
268
    const paginatedData = sortedData.slice(offset, first + offset);
44✔
269

270
    setCount(sortedData?.length);
44✔
271
    setDataset(paginatedData);
44✔
272
  };
44✔
273

274
  const isStatusFilterOption = (status: string): status is FilterForm["status"] =>
149✔
275
    ["All", "Inactive", "Active"].includes(status);
44✔
276

277
  useEffect(() => {
149✔
278
    if (!data?.length) {
66✔
279
      return;
22✔
280
    }
22✔
281

282
    const organizationId = searchParams.get("organization") || "";
66✔
283
    const study = searchParams.get("study") || "";
66✔
284
    const status = searchParams.get("status");
66✔
285

286
    if (organizationId !== orgFilter) {
66!
UNCOV
287
      setValue("organization", organizationId);
×
288
    }
✔
289
    if (study !== studyFilter) {
66!
290
      setValue("study", study);
×
291
    }
✔
292
    if (isStatusFilterOption(status) && status !== statusFilter) {
66!
293
      setValue("status", status);
×
294
    }
✔
295

296
    setTablePage(0);
44✔
297
  }, [
149✔
298
    data,
149✔
299
    searchParams.get("organization"),
149✔
300
    searchParams.get("study"),
149✔
301
    searchParams.get("status"),
149✔
302
  ]);
149✔
303

304
  useEffect(() => {
149✔
305
    if (!touchedFilters.organization && !touchedFilters.study && !touchedFilters.status) {
47✔
306
      return;
11✔
307
    }
11✔
308

309
    const newSearchParams = new URLSearchParams(searchParams);
36✔
310

311
    if (orgFilter) {
47✔
312
      newSearchParams.set("organization", orgFilter);
35✔
313
    } else {
47✔
314
      newSearchParams.delete("organization");
1✔
315
    }
1✔
316
    if (studyFilter) {
47!
317
      newSearchParams.set("study", studyFilter);
×
318
    } else {
47✔
319
      newSearchParams.delete("study");
36✔
320
    }
36✔
321
    if (statusFilter && statusFilter !== "Active") {
47!
322
      newSearchParams.set("status", statusFilter);
×
323
    } else if (statusFilter === "Active") {
47✔
324
      newSearchParams.delete("status");
36✔
325
    }
36✔
326

327
    if (newSearchParams?.toString() !== searchParams?.toString()) {
47✔
328
      setSearchParams(newSearchParams);
36✔
329
    }
36✔
330
  }, [orgFilter, studyFilter, statusFilter]);
149✔
331

332
  const setTablePage = (page: number) => {
149✔
333
    tableRef.current?.setPage(page, true);
44✔
334
  };
44✔
335

336
  const handleFilterChange = (field: keyof FilterForm) => {
149✔
337
    setTouchedFilters((prev) => ({ ...prev, [field]: true }));
39✔
338
  };
39✔
339

340
  return (
149✔
341
    <>
149✔
342
      <Container maxWidth="xl">
149✔
343
        {(state?.error || orgStatus === OrgStatus.ERROR) && (
149!
344
          <Alert sx={{ mt: 2, mx: "auto", p: 2 }} severity="error">
×
345
            {state?.error || "An error occurred while loading the data."}
×
346
          </Alert>
×
347
        )}
348
      </Container>
149✔
349

350
      <PageBanner
149✔
351
        title="Manage Programs"
149✔
352
        subTitle=""
149✔
353
        padding="38px 0 0 25px"
149✔
354
        body={
149✔
355
          <StyledBannerBody direction="row" alignItems="center" justifyContent="flex-end">
149✔
356
            <StyledButton component={Link} to="/programs/new">
149✔
357
              Add Program
358
            </StyledButton>
149✔
359
          </StyledBannerBody>
149✔
360
        }
149✔
361
      />
362

363
      <StyledContainer maxWidth="xl">
149✔
364
        <StyledFilterContainer>
149✔
365
          <StyledInlineLabel htmlFor="organization-filter">Program</StyledInlineLabel>
149✔
366
          <StyledFormControl>
149✔
367
            <StyledTextField
149✔
368
              {...register("organization", {
149✔
369
                onChange: (e) => handleFilterChange("organization"),
149✔
370
                setValueAs: (val) => val?.trim(),
149✔
371
              })}
149✔
372
              placeholder="Enter a Program"
149✔
373
              id="organization-filter"
149✔
374
              required
149✔
375
            />
376
          </StyledFormControl>
149✔
377
          <StyledInlineLabel htmlFor="study-filter">Study</StyledInlineLabel>
149✔
378
          <StyledFormControl>
149✔
379
            <StyledTextField
149✔
380
              {...register("study", {
149✔
381
                onChange: (e) => handleFilterChange("study"),
149✔
382
                setValueAs: (val) => val?.trim(),
149✔
383
              })}
149✔
384
              placeholder="Enter a Study"
149✔
385
              id="study-filter"
149✔
386
              required
149✔
387
            />
388
          </StyledFormControl>
149✔
389
          <StyledInlineLabel htmlFor="status-filter">Status</StyledInlineLabel>
149✔
390
          <StyledFormControl>
149✔
391
            <Controller
149✔
392
              name="status"
149✔
393
              control={control}
149✔
394
              render={({ field }) => (
149✔
395
                <StyledSelect
149✔
396
                  {...field}
149✔
397
                  value={field.value}
149✔
398
                  MenuProps={{ disablePortal: true }}
149✔
399
                  inputProps={{ id: "status-filter" }}
149✔
400
                  onChange={(e) => {
149✔
401
                    field.onChange(e);
×
UNCOV
402
                    handleFilterChange("status");
×
403
                  }}
×
404
                >
405
                  <MenuItem value="All">All</MenuItem>
149✔
406
                  <MenuItem value="Active">Active</MenuItem>
149✔
407
                  <MenuItem value="Inactive">Inactive</MenuItem>
149✔
408
                </StyledSelect>
149✔
409
              )}
149✔
410
            />
411
          </StyledFormControl>
149✔
412
        </StyledFilterContainer>
149✔
413
        <GenericTable
149✔
414
          ref={tableRef}
149✔
415
          columns={columns}
149✔
416
          data={dataset || []}
149!
417
          total={count || 0}
149✔
418
          loading={orgStatus === OrgStatus.LOADING}
149✔
419
          disableUrlParams={false}
149✔
420
          defaultRowsPerPage={20}
149✔
421
          defaultOrder="asc"
149✔
422
          setItemKey={(item, idx) => `${idx}_${item._id}`}
149✔
423
          onFetchData={handleFetchData}
149✔
424
          containerProps={{ sx: { marginBottom: "8px", borderColor: "#083A50" } }}
149✔
425
          CustomTableHead={StyledTableHead}
149✔
426
          CustomTableHeaderCell={StyledHeaderCell}
149✔
427
          CustomTableBodyCell={StyledTableCell}
149✔
428
        />
429
      </StyledContainer>
149✔
430
    </>
149✔
431
  );
432
};
149✔
433

434
export default ListView;
1✔
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

© 2025 Coveralls, Inc