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

CBIIT / crdc-datahub-ui / 11823490147

13 Nov 2024 06:27PM UTC coverage: 53.812% (+0.3%) from 53.521%
11823490147

Pull #531

github

web-flow
Merge 1f17e425a into 56461c97e
Pull Request #531: Sync releases

2571 of 5273 branches covered (48.76%)

Branch coverage included in aggregate %.

43 of 60 new or added lines in 8 files covered. (71.67%)

1 existing line in 1 file now uncovered.

3689 of 6360 relevant lines covered (58.0%)

135.67 hits per line

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

94.1
/src/components/DataSubmissions/DataSubmissionListFilters.tsx
1
import { memo, useEffect, useRef, useState } from "react";
2
import { FormControl, IconButton, MenuItem, Grid, Box, styled, Stack } from "@mui/material";
3
import { debounce, isEqual } from "lodash";
4
import RefreshIcon from "@mui/icons-material/Refresh";
5
import { Controller, useForm } from "react-hook-form";
6
import StyledSelectFormComponent from "../StyledFormComponents/StyledSelect";
7
import StyledTextFieldFormComponent from "../StyledFormComponents/StyledOutlinedInput";
8
import ColumnVisibilityButton from "../GenericTable/ColumnVisibilityButton";
9
import { ListSubmissionsInput, ListSubmissionsResp } from "../../graphql";
10
import { Column } from "../GenericTable";
11
import { useSearchParamsContext } from "../Contexts/SearchParamsContext";
12
import Tooltip from "../Tooltip";
13
import { isStringLengthBetween } from "../../utils";
14

15
const StyledFilters = styled("div")({
2✔
16
  paddingTop: "19px",
17
  paddingBottom: "15px",
18
  paddingLeft: "38px",
19
  paddingRight: "26px",
20
});
21

22
const StyledFormControl = styled(FormControl)({
2✔
23
  display: "flex",
24
  flexDirection: "row",
25
  alignItems: "center",
26
  gap: "15px",
27
});
28

29
const StyledInlineLabel = styled("label")({
2✔
30
  fontFamily: "'Nunito', 'Rubik', sans-serif !important",
31
  fontWeight: 700,
32
  fontSize: "16px",
33
  lineHeight: "16px",
34
  minWidth: "100px",
35
  textAlign: "right",
36
});
37

38
const StyledSelect = styled(StyledSelectFormComponent)({
2✔
39
  width: "280px",
40
});
41

42
const StyledTextField = styled(StyledTextFieldFormComponent)({
2✔
43
  width: "280px",
44
});
45

46
const StyledRefreshIcon = styled(RefreshIcon)({
2✔
47
  transform: "scale(-1, 1)",
48
  color: "#346798",
49
  fontSize: "31px",
50
});
51

52
const StyledIconButton = styled(IconButton)({
2✔
53
  padding: 0,
54
  border: "1px solid #D0D0D0",
55
  borderRadius: "5px",
56
});
57

58
const StyledActionWrapper = styled(Box)({
2✔
59
  minWidth: "44px",
60
  width: "100%",
61
  height: "44px",
62
  display: "flex",
63
  justifyContent: "flex-start",
64
  alignItems: "center",
65
});
66

67
const ActionButtonsContainer = styled(Box)({
2✔
68
  display: "flex",
69
  flexDirection: "column",
70
  justifyContent: "center",
71
  height: "100%",
72
  gap: "9px",
73
});
74

75
const initialTouchedFields: TouchedState = {
2✔
76
  organization: false,
77
  status: false,
78
  dataCommons: false,
79
  name: false,
80
  dbGaPID: false,
81
  submitterName: false,
82
};
83

84
const statusValues: SubmissionStatus[] = [
2✔
85
  "New",
86
  "In Progress",
87
  "Submitted",
88
  "Released",
89
  "Withdrawn",
90
  "Rejected",
91
  "Completed",
92
  "Canceled",
93
  "Deleted",
94
];
95

96
const defaultValues: FilterForm = {
2✔
97
  organization: "All",
98
  status: "All",
99
  dataCommons: "All",
100
  name: "",
101
  dbGaPID: "",
102
  submitterName: "All",
103
};
104

105
type T = ListSubmissionsResp["listSubmissions"]["submissions"][0];
106

107
export type FilterForm = Pick<
108
  ListSubmissionsInput,
109
  "organization" | "status" | "dataCommons" | "name" | "dbGaPID" | "submitterName"
110
>;
111

112
type FilterFormKey = keyof FilterForm;
113

114
type TouchedState = { [K in FilterFormKey]: boolean };
115

116
type Props = {
117
  columns: Column<T>[];
118
  organizations: Pick<Organization, "_id" | "name">[];
119
  submitterNames: string[];
120
  dataCommons: string[];
121
  columnVisibilityModel: ColumnVisibilityModel;
122
  onColumnVisibilityModelChange: (model: ColumnVisibilityModel) => void;
123
  onChange?: (data: FilterForm) => void;
124
};
125

126
const DataSubmissionListFilters = ({
2✔
127
  columns,
128
  organizations,
129
  submitterNames,
130
  dataCommons,
131
  columnVisibilityModel,
132
  onColumnVisibilityModelChange,
133
  onChange,
134
}: Props) => {
135
  const { searchParams, setSearchParams } = useSearchParamsContext();
326✔
136
  const { control, register, watch, reset, setValue, getValues } = useForm<FilterForm>({
326✔
137
    defaultValues,
138
  });
139
  const [
140
    statusFilter,
141
    orgFilter,
142
    dataCommonsFilter,
143
    nameFilter,
144
    dbGaPIDFilter,
145
    submitterNameFilter,
146
  ] = watch(["status", "organization", "dataCommons", "name", "dbGaPID", "submitterName"]);
326✔
147

148
  const [touchedFilters, setTouchedFilters] = useState<TouchedState>(initialTouchedFields);
326✔
149

150
  const debounceAfter3CharsInputs: FilterFormKey[] = ["name", "dbGaPID"];
326✔
151
  const debouncedOnChangeRef = useRef(
326✔
152
    debounce((form: FilterForm) => handleFormChange(form), 500)
10✔
153
  ).current;
154
  const debouncedDropdownRef = useRef(
326✔
155
    debounce((form: FilterForm) => handleFormChange(form), 0)
10✔
156
  ).current;
157

158
  useEffect(() => {
326✔
159
    // Reset submitterName filter if it is no longer a valid option
160
    // due to other filters changing
161
    if (
170!
162
      submitterNameFilter !== "All" &&
94✔
163
      Object.values(touchedFilters).some((filter) => filter) &&
44✔
164
      !submitterNames?.includes(submitterNameFilter)
165
    ) {
166
      const newSearchParams = new URLSearchParams(searchParams);
×
167
      newSearchParams.delete("submitterName");
×
168
      setSearchParams(newSearchParams);
×
169
      setValue("submitterName", "All");
×
170
    }
171
  }, [submitterNames, submitterNameFilter, touchedFilters]);
172

173
  useEffect(() => {
326✔
174
    // Reset organization filter if it is no longer a valid option
175
    // due to other filters changing
176
    const organizationIds = organizations?.map((org) => org._id);
336✔
177
    if (
170✔
178
      orgFilter !== "All" &&
142✔
179
      Object.values(touchedFilters).some((filter) => filter) &&
82✔
180
      !organizationIds?.includes(orgFilter)
181
    ) {
NEW
182
      const newSearchParams = new URLSearchParams(searchParams);
×
NEW
183
      newSearchParams.delete("organization");
×
NEW
184
      setSearchParams(newSearchParams);
×
NEW
185
      setValue("organization", "All");
×
186
    }
187
  }, [organizations, orgFilter, touchedFilters]);
188

189
  useEffect(() => {
326✔
190
    const organizationId = searchParams.get("organization");
124✔
191
    const status = searchParams.get("status");
124✔
192
    const dataCommon = searchParams.get("dataCommons");
124✔
193
    const name = searchParams.get("name");
124✔
194
    const dbGaPID = searchParams.get("dbGaPID");
124✔
195
    const submitterName = searchParams.get("submitterName");
124✔
196

197
    handleStatusChange(status);
124✔
198

199
    if (organizationId && organizationId !== orgFilter) {
124✔
200
      setValue("organization", organizationId);
2✔
201
    }
202
    if (dataCommon && dataCommon !== dataCommonsFilter) {
124✔
203
      setValue("dataCommons", dataCommon);
2✔
204
    }
205
    if (submitterName && submitterName !== submitterNameFilter) {
124✔
206
      setValue("submitterName", submitterName);
2✔
207
    }
208
    if (name && name !== nameFilter) {
124✔
209
      setValue("name", name);
2✔
210
    }
211
    if (dbGaPID && dbGaPID !== dbGaPIDFilter) {
124✔
212
      setValue("dbGaPID", dbGaPID);
2✔
213
    }
214

215
    if (Object.values(touchedFilters).every((filter) => !filter)) {
464✔
216
      handleFormChange(getValues());
44✔
217
    }
218
  }, [organizations, submitterNames, dataCommons, searchParams?.toString()]);
219

220
  useEffect(() => {
326✔
221
    if (Object.values(touchedFilters).every((filter) => !filter)) {
636✔
222
      return;
46✔
223
    }
224

225
    const newSearchParams = new URLSearchParams(searchParams);
124✔
226

227
    if (orgFilter && orgFilter !== "All") {
124✔
228
      newSearchParams.set("organization", orgFilter);
56✔
229
    } else if (orgFilter === "All") {
68!
230
      newSearchParams.delete("organization");
68✔
231
    }
232
    if (statusFilter && statusFilter !== "All") {
124✔
233
      newSearchParams.set("status", statusFilter);
50✔
234
    } else if (statusFilter === "All") {
74!
235
      newSearchParams.delete("status");
74✔
236
    }
237
    if (dataCommonsFilter && dataCommonsFilter !== "All") {
124✔
238
      newSearchParams.set("dataCommons", dataCommonsFilter);
50✔
239
    } else if (dataCommonsFilter === "All") {
74!
240
      newSearchParams.delete("dataCommons");
74✔
241
    }
242
    if (submitterNameFilter && submitterNameFilter !== "All") {
124✔
243
      newSearchParams.set("submitterName", submitterNameFilter);
8✔
244
    } else if (submitterNameFilter === "All") {
116!
245
      newSearchParams.delete("submitterName");
116✔
246
    }
247

248
    if (nameFilter?.length >= 3) {
124✔
249
      newSearchParams.set("name", nameFilter);
56✔
250
    } else {
251
      newSearchParams.delete("name");
68✔
252
    }
253
    if (dbGaPIDFilter?.length >= 3) {
124✔
254
      newSearchParams.set("dbGaPID", dbGaPIDFilter);
18✔
255
    } else {
256
      newSearchParams.delete("dbGaPID");
106✔
257
    }
258

259
    if (newSearchParams?.toString() !== searchParams?.toString()) {
124✔
260
      setSearchParams(newSearchParams);
78✔
261
    }
262
  }, [
263
    orgFilter,
264
    statusFilter,
265
    dataCommonsFilter,
266
    nameFilter,
267
    dbGaPIDFilter,
268
    submitterNameFilter,
269
    touchedFilters,
270
  ]);
271

272
  useEffect(() => {
326✔
273
    const subscription = watch((formValue: FilterForm, { name }) => {
44✔
274
      const isDebouncedDropdown = ["submitterName", "dataCommons", "organization"].includes(name);
124✔
275
      if (isDebouncedDropdown) {
124✔
276
        debouncedDropdownRef(formValue);
14✔
277
        return;
14✔
278
      }
279

280
      // Add debounce for input fields
281
      const isDebounceField = debounceAfter3CharsInputs.includes(name as FilterFormKey);
110✔
282
      // Debounce if value has at least 3 characters
283
      if (isDebounceField && formValue[name]?.length >= 3) {
110✔
284
        debouncedOnChangeRef(formValue);
48✔
285
        return;
48✔
286
      }
287
      // Do nothing if values has between 0 and 3 (exclusive) characters
288
      if (isDebounceField && formValue[name]?.length > 0) {
62✔
289
        debouncedOnChangeRef.cancel();
38✔
290
        return;
38✔
291
      }
292
      // If value is cleared, call the onChange immediately
293
      if (isDebounceField && formValue[name]?.length === 0) {
24✔
294
        debouncedOnChangeRef.cancel();
20✔
295
        handleFormChange(formValue);
20✔
296
        return;
20✔
297
      }
298

299
      // Immediately call the onChange if the change is not a debounce field
300
      handleFormChange(formValue);
4✔
301
    });
302

303
    return () => {
44✔
304
      debouncedOnChangeRef.cancel();
44✔
305
      subscription.unsubscribe();
44✔
306
    };
307
  }, [watch, debouncedOnChangeRef]);
308

309
  const isStatusFilterOption = (status: string): status is FilterForm["status"] =>
326✔
310
    ["All", ...statusValues].includes(status);
84✔
311

312
  const handleStatusChange = (status: string) => {
326✔
313
    if (status === statusFilter) {
124✔
314
      return;
40✔
315
    }
316

317
    if (isStatusFilterOption(status)) {
84✔
318
      setValue("status", status);
2✔
319
    }
320
  };
321

322
  const handleFormChange = (form: FilterForm) => {
326✔
323
    if (!onChange || !form) {
88!
324
      return;
×
325
    }
326

327
    const newForm: FilterForm = {
88✔
328
      ...form,
329
      name: form.name?.length >= 3 ? form.name : "",
44✔
330
      dbGaPID: form.dbGaPID?.length >= 3 ? form.dbGaPID : "",
44✔
331
    };
332

333
    onChange(newForm);
88✔
334
  };
335

336
  const handleFilterChange = (field: FilterFormKey) => {
326✔
337
    setTouchedFilters((prev) => ({ ...prev, [field]: true }));
122✔
338
  };
339

340
  const handleResetFilters = () => {
326✔
341
    const newSearchParams = new URLSearchParams(searchParams);
2✔
342
    searchParams.delete("organization");
2✔
343
    searchParams.delete("status");
2✔
344
    searchParams.delete("dataCommons");
2✔
345
    searchParams.delete("name");
2✔
346
    searchParams.delete("dbGaPID");
2✔
347
    searchParams.delete("submitterName");
2✔
348
    setSearchParams(newSearchParams);
2✔
349
    reset({ ...defaultValues });
2✔
350
  };
351

352
  return (
326✔
353
    <StyledFilters data-testid="data-submission-list-filters">
354
      <Stack direction="row" alignItems="center" gap="12px">
355
        <Grid container spacing={2} rowSpacing="9px">
356
          <Grid item xs={4}>
357
            <StyledFormControl>
358
              <StyledInlineLabel htmlFor="organization-filter">Organization</StyledInlineLabel>
359
              <Controller
360
                name="organization"
361
                control={control}
362
                render={({ field }) => (
363
                  <StyledSelect
326✔
364
                    {...field}
365
                    value={
366
                      organizations?.map((org) => org._id)?.includes(field.value)
648✔
367
                        ? field.value
368
                        : "All"
369
                    }
370
                    MenuProps={{ disablePortal: true }}
371
                    inputProps={{
372
                      id: "organization-filter",
373
                      "data-testid": "organization-select-input",
374
                    }}
375
                    data-testid="organization-select"
376
                    onChange={(e) => {
377
                      field.onChange(e);
6✔
378
                      handleFilterChange("organization");
6✔
379
                    }}
380
                  >
381
                    <MenuItem value="All" data-testid="organization-option-All">
382
                      All
383
                    </MenuItem>
384
                    {organizations?.map((org) => (
385
                      <MenuItem
648✔
386
                        key={org._id}
387
                        value={org._id}
388
                        data-testid={`organization-option-${org._id}`}
389
                      >
390
                        {org.name}
391
                      </MenuItem>
392
                    ))}
393
                  </StyledSelect>
394
                )}
395
              />
396
            </StyledFormControl>
397
          </Grid>
398

399
          <Grid item xs={4}>
400
            <StyledFormControl>
401
              <StyledInlineLabel htmlFor="status-filter">Status</StyledInlineLabel>
402
              <Controller
403
                name="status"
404
                control={control}
405
                render={({ field }) => (
406
                  <StyledSelect
326✔
407
                    {...field}
408
                    value={field.value}
409
                    MenuProps={{ disablePortal: true }}
410
                    inputProps={{ id: "status-filter", "data-testid": "status-select-input" }}
411
                    data-testid="status-select"
412
                    onChange={(e) => {
413
                      field.onChange(e);
2✔
414
                      handleFilterChange("status");
2✔
415
                    }}
416
                  >
417
                    <MenuItem value="All" data-testid="status-option-All">
418
                      All
419
                    </MenuItem>
420
                    {statusValues.map((value) => (
421
                      <MenuItem
2,934✔
422
                        key={`submission_status_${value}`}
423
                        value={value}
424
                        data-testid={`status-option-${value}`}
425
                      >
426
                        {value}
427
                      </MenuItem>
428
                    ))}
429
                  </StyledSelect>
430
                )}
431
              />
432
            </StyledFormControl>
433
          </Grid>
434

435
          <Grid item xs={4}>
436
            <StyledFormControl>
437
              <StyledInlineLabel htmlFor="data-commons-filter">
438
                Data
439
                <br />
440
                Commons
441
              </StyledInlineLabel>
442
              <Controller
443
                name="dataCommons"
444
                control={control}
445
                render={({ field }) => (
446
                  <StyledSelect
326✔
447
                    {...field}
448
                    value={dataCommons?.length ? field.value : "All"}
163✔
449
                    MenuProps={{ disablePortal: true }}
450
                    inputProps={{
451
                      id: "data-commons-filter",
452
                      "data-testid": "data-commons-select-input",
453
                    }}
454
                    data-testid="data-commons-select"
455
                    onChange={(e) => {
456
                      field.onChange(e);
4✔
457
                      handleFilterChange("dataCommons");
4✔
458
                    }}
459
                  >
460
                    <MenuItem value="All" data-testid="data-commons-option-All">
461
                      All
462
                    </MenuItem>
463
                    {dataCommons?.map((dc) => (
464
                      <MenuItem key={dc} value={dc} data-testid={`data-commons-option-${dc}`}>
648✔
465
                        {dc}
466
                      </MenuItem>
467
                    ))}
468
                  </StyledSelect>
469
                )}
470
              />
471
            </StyledFormControl>
472
          </Grid>
473

474
          <Grid item xs={4}>
475
            <StyledFormControl>
476
              <StyledInlineLabel id="submission-name-filter">
477
                Submission
478
                <br />
479
                Name
480
              </StyledInlineLabel>
481
              <StyledTextField
482
                {...register("name", {
483
                  setValueAs: (val) => val?.trim(),
118✔
484
                  onChange: () => handleFilterChange("name"),
62✔
485
                  onBlur: (e) =>
486
                    isStringLengthBetween(e?.target?.value, 0, 3) && setValue("name", ""),
8!
487
                })}
488
                size="small"
489
                placeholder="Minimum 3 characters required"
490
                inputProps={{
491
                  "aria-labelledby": "submission-name-filter",
492
                  "data-testid": "submission-name-input",
493
                }}
494
                required
495
              />
496
            </StyledFormControl>
497
          </Grid>
498

499
          <Grid item xs={4}>
500
            <StyledFormControl>
501
              <StyledInlineLabel id="dbGaPID-filter">dbGaP ID</StyledInlineLabel>
502
              <StyledTextField
503
                {...register("dbGaPID", {
504
                  setValueAs: (val) => val?.trim(),
100✔
505
                  onChange: () => handleFilterChange("dbGaPID"),
44✔
506
                  onBlur: (e) =>
507
                    isStringLengthBetween(e?.target?.value, 0, 3) && setValue("dbGaPID", ""),
8!
508
                })}
509
                size="small"
510
                placeholder="Minimum 3 characters required"
511
                inputProps={{
512
                  "aria-labelledby": "dbGaPID-filter",
513
                  "data-testid": "dbGaPID-input",
514
                }}
515
                required
516
              />
517
            </StyledFormControl>
518
          </Grid>
519

520
          <Grid item xs={4}>
521
            <StyledFormControl>
522
              <StyledInlineLabel htmlFor="submitter-name-filter">Submitter</StyledInlineLabel>
523
              <Controller
524
                name="submitterName"
525
                control={control}
526
                render={({ field }) => (
527
                  <StyledSelect
326✔
528
                    {...field}
529
                    value={submitterNames?.includes(field.value) ? field.value : "All"}
163✔
530
                    MenuProps={{ disablePortal: true }}
531
                    inputProps={{
532
                      id: "submitter-name-filter",
533
                      "data-testid": "submitter-name-select-input",
534
                    }}
535
                    data-testid="submitter-name-select"
536
                    onChange={(e) => {
537
                      field.onChange(e);
4✔
538
                      handleFilterChange("submitterName");
4✔
539
                    }}
540
                  >
541
                    <MenuItem value="All" data-testid="submitter-name-option-All">
542
                      All
543
                    </MenuItem>
544
                    {submitterNames?.map((submitter) => (
545
                      <MenuItem
648✔
546
                        key={`submitter_${submitter}`}
547
                        value={submitter}
548
                        data-testid={`submitter-name-option-${submitter}`}
549
                      >
550
                        {submitter}
551
                      </MenuItem>
552
                    ))}
553
                  </StyledSelect>
554
                )}
555
              />
556
            </StyledFormControl>
557
          </Grid>
558
        </Grid>
559

560
        {/* Action Buttons */}
561
        <ActionButtonsContainer>
562
          <StyledActionWrapper>
563
            <Tooltip
564
              open={undefined}
565
              title="Reset filters."
566
              placement="top"
567
              disableHoverListener={false}
568
            >
569
              <StyledIconButton
570
                onClick={handleResetFilters}
571
                data-testid="reset-filters-button"
572
                aria-label="Reset filters button"
573
              >
574
                <StyledRefreshIcon />
575
              </StyledIconButton>
576
            </Tooltip>
577
          </StyledActionWrapper>
578
          <StyledActionWrapper>
579
            <ColumnVisibilityButton
580
              columns={columns}
581
              getColumnKey={(column) => column.fieldKey ?? column.field}
1,956✔
582
              getColumnLabel={(column) => column.label?.toString()}
1,304✔
583
              columnVisibilityModel={columnVisibilityModel}
584
              onColumnVisibilityModelChange={onColumnVisibilityModelChange}
585
              data-testid="column-visibility-button"
586
            />
587
          </StyledActionWrapper>
588
        </ActionButtonsContainer>
589
      </Stack>
590
    </StyledFilters>
591
  );
592
};
593

594
export default memo(DataSubmissionListFilters, isEqual);
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