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

DLR-SC / ESID / 16219447743

11 Jul 2025 11:57AM UTC coverage: 52.16% (-1.9%) from 54.09%
16219447743

Pull #416

github

JonasGilg
:wrench: Apply suggestions from code review.
Pull Request #416: Fix Filters

456 of 577 branches covered (79.03%)

Branch coverage included in aggregate %.

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

106 existing lines in 3 files now uncovered.

4397 of 8727 relevant lines covered (50.38%)

10.72 hits per line

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

0.0
/src/components/ScenarioComponents/FilterComponents/GroupFilterEditor.tsx
1
// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR)
2
// SPDX-License-Identifier: Apache-2.0
3

4
import Box from '@mui/material/Box';
×
5
import TextField from '@mui/material/TextField';
×
6
import Typography from '@mui/material/Typography';
×
7
import FormGroup from '@mui/material/FormGroup';
×
8
import FormControlLabel from '@mui/material/FormControlLabel';
×
9
import Checkbox from '@mui/material/Checkbox';
×
10
import Button from '@mui/material/Button';
×
11
import useTheme from '@mui/material/styles/useTheme';
×
12
import {useState, useEffect, useCallback, Dispatch} from 'react';
×
13
import {useTranslation} from 'react-i18next';
×
14
import React from 'react';
15
import {GroupFilter} from 'types/group';
16
import {Localization} from 'types/localization';
17

18
interface GroupFilterEditorProps {
19
  /** The GroupFilter item to be edited. */
20
  groupFilter: GroupFilter;
21

22
  /** A dictionary of group filters.*/
23
  groupFilters: Record<string, GroupFilter>;
24

25
  /** An array of group category.*/
26
  categories: Array<{id: string; name: string}>;
27

28
  /** An array of group subcategory.*/
29
  groups: Array<{id: string; name: string; category: string}>;
30

31
  /** A function that allows setting the groupFilter state so that if the user adds a filter, the new filter will be visible */
32
  setGroupFilters: Dispatch<Record<string, GroupFilter>>;
33

34
  /**
35
   * Callback function that is called, when a new filter is created, so it will be selected immediately or when the user
36
   * wants to close the editor.
37
   * @param groupFilter - Either the current filter or null when the user wants to close the current filter's editor.
38
   */
39
  selectGroupFilterCallback: (groupFilter: GroupFilter | null) => void;
40

41
  /**
42
   * A callback that notifies the parent, if there are currently unsaved changes for this group filter.
43
   * @param unsavedChanges - If the group filter has been modified without saving.
44
   */
45
  unsavedChangesCallback: (unsavedChanges: boolean) => void;
46

47
  /** An object containing localization information (translation & number formattation).*/
48
  localization?: Localization;
49
}
50

51
/**
52
 * This is the detail view of the GroupFilter dialog. It allows to edit and create groups. It has a text field for the
53
 * name at the top and columns of checkboxes for groups in the center. It requires that at least one checkbox of each
54
 * group is selected before the apply button becomes available. It is also possible to discard changes by clicking the
55
 * abort button before applying the changes.
56
 */
57
export default function GroupFilterEditor({
×
58
  groupFilter,
×
59
  groupFilters,
×
60
  categories,
×
61
  groups,
×
62
  setGroupFilters,
×
63
  selectGroupFilterCallback,
×
64
  unsavedChangesCallback,
×
65
  localization = {formatNumber: (value: number) => value.toString(), customLang: 'global', overrides: {}},
×
66
}: GroupFilterEditorProps) {
×
67
  const {t: defaultT} = useTranslation();
×
68
  const {t: customT} = useTranslation(localization.customLang);
×
69
  const theme = useTheme();
×
70
  const [name, setName] = useState(groupFilter.name);
×
71
  const [groupSelection, setGroupSelection] = useState(groupFilter.groups);
×
72

73
  // Every group must have at least one element selected to be valid.
74
  const [valid, setValid] = useState(
×
75
    name.length > 0 && Object.values(groupSelection).every((group) => group.length > 0)
×
76
  );
×
77
  const [unsavedChanges, setUnsavedChanges] = useState(false);
×
78

79
  // Checks if the group filer is in a valid state.
80
  useEffect(() => {
×
81
    setValid(name.length > 0 && Object.values(groupSelection).every((group) => group.length > 0));
×
82
  }, [name, groupSelection]);
×
83

84
  // Updates the parent about the current save state of the group filter.
85
  useEffect(() => {
×
86
    unsavedChangesCallback(unsavedChanges);
×
87
  }, [unsavedChanges, unsavedChangesCallback]);
×
88

89
  const toggleGroup = useCallback(
×
90
    (group: {id: string; name: string; category: string}) => {
×
91
      let category = [...groupSelection[group.category]];
×
92

93
      if (category.includes(group.id)) {
×
94
        category = category.filter((key) => key !== group.id);
×
95
      } else {
×
96
        category.push(group.id);
×
97
      }
×
98

99
      setGroupSelection({
×
100
        ...groupSelection,
×
101
        [group.category]: category,
×
102
      });
×
103
      setUnsavedChanges(true);
×
104
    },
×
105
    [groupSelection, setGroupSelection]
×
106
  );
×
107

NEW
108
  const toggleCategory = useCallback(
×
NEW
109
    (categoryId: string) => {
×
NEW
110
      const categoryGroups = groups.filter((group) => group.category === categoryId);
×
NEW
111
      const allChecked = categoryGroups.length === groupSelection[categoryId].length;
×
NEW
112
      setGroupSelection({
×
NEW
113
        ...groupSelection,
×
NEW
114
        [categoryId]: allChecked ? [] : categoryGroups.map((group) => group.id),
×
NEW
115
      });
×
NEW
116
      setUnsavedChanges(true);
×
NEW
117
    },
×
NEW
118
    [groupSelection, groups]
×
NEW
119
  );
×
120

NEW
121
  const isCategoryFullyChecked = useCallback(
×
NEW
122
    (categoryId: string) => {
×
NEW
123
      const totalGroups = groups.filter((group) => group.category === categoryId).length;
×
NEW
124
      return totalGroups === groupSelection[categoryId].length;
×
NEW
125
    },
×
NEW
126
    [groupSelection, groups]
×
NEW
127
  );
×
128

NEW
129
  const isCategoryPartiallyChecked = useCallback(
×
NEW
130
    (categoryId: string) => {
×
NEW
131
      const totalGroups = groups.filter((group) => group.category === categoryId).length;
×
NEW
132
      return groupSelection[categoryId].length > 0 && groupSelection[categoryId].length < totalGroups;
×
NEW
133
    },
×
NEW
134
    [groupSelection, groups]
×
NEW
135
  );
×
136

137
  return (
×
138
    <Box
×
139
      sx={{
×
140
        display: 'flex',
×
141
        flexGrow: '1',
×
142
        flexDirection: 'column',
×
143
        padding: 3,
×
144
      }}
×
145
    >
146
      <TextField
×
147
        label={
×
148
          localization.overrides && localization.overrides['group-filters.name']
×
149
            ? customT(localization.overrides['group-filters.name'])
×
150
            : defaultT('group-filters.name')
×
151
        }
152
        variant='outlined'
×
153
        defaultValue={name}
×
154
        autoFocus={true}
×
155
        error={name.length === 0}
×
156
        onFocus={(e) => e.target.select()}
×
157
        onChange={(e) => {
×
158
          setUnsavedChanges(true);
×
159
          setName(e.target.value);
×
160
        }}
×
161
      />
×
162
      <Box
×
163
        sx={{
×
164
          display: 'flex',
×
165
          flexGrow: '1',
×
166
          flexDirection: 'row',
×
167
          paddingTop: 2,
×
168
          paddingBottom: 2,
×
169
        }}
×
170
      >
171
        {categories.map((category) => (
×
172
          <Box
×
173
            key={category.id}
×
174
            sx={{
×
175
              display: 'flex',
×
176
              flexDirection: 'column',
×
177
            }}
×
178
          >
NEW
179
            <FormControlLabel
×
NEW
180
              label={
×
NEW
181
                <Typography
×
NEW
182
                  color={groupSelection[category.id].length > 0 ? theme.palette.text.primary : theme.palette.error.main}
×
NEW
183
                  variant='h2'
×
184
                >
NEW
185
                  {category.name}
×
NEW
186
                </Typography>
×
187
              }
NEW
188
              control={
×
NEW
189
                <Checkbox
×
NEW
190
                  checked={isCategoryFullyChecked(category.id)}
×
NEW
191
                  indeterminate={isCategoryPartiallyChecked(category.id)}
×
NEW
192
                  onClick={() => toggleCategory(category.id)}
×
NEW
193
                />
×
194
              }
NEW
195
            />
×
NEW
196
            <FormGroup sx={{paddingLeft: theme.spacing(3)}}>
×
197
              {groups
×
198
                .filter((group) => group.category === category.id)
×
199
                .map((group) => (
×
200
                  <FormControlLabel
×
201
                    key={group.id}
×
202
                    label={group.name}
×
203
                    control={
×
204
                      <Checkbox
×
205
                        checked={groupSelection[category.id].includes(group.id)}
×
206
                        onClick={() => toggleGroup(group)}
×
207
                      />
×
208
                    }
209
                  />
×
210
                ))}
×
211
            </FormGroup>
×
212
          </Box>
×
213
        ))}
×
214
      </Box>
×
215
      <Box
×
216
        sx={{
×
217
          display: 'flex',
×
218
          flexGrow: '1',
×
219
          gap: 2,
×
220
          flexDirection: 'row',
×
221
          justifyContent: 'flex-end',
×
222
        }}
×
223
      >
224
        <Button
×
225
          variant='outlined'
×
226
          color='error'
×
227
          sx={{marginRight: theme.spacing(2)}}
×
228
          onClick={() => {
×
229
            setUnsavedChanges(false);
×
230
            selectGroupFilterCallback(null);
×
231
          }}
×
232
        >
233
          {localization.overrides && localization.overrides['group-filters.close']
×
234
            ? customT(localization.overrides['group-filters.close'])
×
235
            : defaultT('group-filters.close')}
×
236
        </Button>
×
237
        <Button
×
238
          variant='outlined'
×
239
          color='primary'
×
240
          disabled={!valid || !unsavedChanges}
×
241
          onClick={() => {
×
242
            setUnsavedChanges(false);
×
243
            const newFilter = {
×
244
              id: groupFilter.id,
×
245
              name: name,
×
246
              isVisible: true,
×
247
              groups: groupSelection,
×
248
            };
×
249
            setGroupFilters({
×
250
              ...groupFilters,
×
251
              [newFilter.id]: newFilter,
×
252
            });
×
253
          }}
×
254
        >
255
          {localization.overrides && localization.overrides['group-filters.apply']
×
256
            ? customT(localization.overrides['group-filters.apply'])
×
257
            : defaultT('group-filters.apply')}
×
258
        </Button>
×
259
      </Box>
×
260
    </Box>
×
261
  );
262
}
×
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