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

DLR-SC / ESID / 15257190808

26 May 2025 03:08PM UTC coverage: 51.267% (-0.9%) from 52.215%
15257190808

push

github

kunkoala
:wrench: enhance drag and drop functionalities

- Added `isDragging` prop to `DataCard`, `CardTooltip`, and `MainCard` components to manage drag state.
- Updated tooltip visibility logic to show when dragging or hovering.
- Adjusted cursor style during dragging for better user experience.

397 of 504 branches covered (78.77%)

Branch coverage included in aggregate %.

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

119 existing lines in 12 files now uncovered.

3771 of 7626 relevant lines covered (49.45%)

4.72 hits per line

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

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

4
import React, {useCallback, useContext, useMemo, useRef, useState} from 'react';
×
5
import Paper from '@mui/material/Paper';
×
6
import Popper from '@mui/material/Popper';
×
7
import Box from '@mui/material/Box';
×
8
import Button from '@mui/material/Button';
×
9
import {useTheme} from '@mui/material/styles';
×
10
import {useTranslation} from 'react-i18next';
×
11
import Typography from '@mui/material/Typography';
×
12
import Divider from '@mui/material/Divider';
×
13
import ClickAwayListener from '@mui/material/ClickAwayListener';
×
14
import {useAppDispatch, useAppSelector} from 'store/hooks';
×
15
import IconButton from '@mui/material/IconButton';
×
16
import Close from '@mui/icons-material/Close';
×
17
import CardTitle from './CardsComponents/MainCard/CardTitle';
×
18
import WebAssetOff from '@mui/icons-material/WebAssetOff';
×
19
import LibraryAddOutlined from '@mui/icons-material/LibraryAddOutlined';
×
20
import Dialog from '@mui/material/Dialog';
×
21
import NewScenarioDialog, {NewScenarioData} from './NewScenarioDialog';
×
22
import {InterventionTemplates, Models, NodeLists, Scenario} from 'store/services/APITypes';
UNCOV
23
import {useGetMultiScenariosQuery} from 'store/services/scenarioApi';
×
24
import {updateScenario} from 'store/DataSelectionSlice';
×
25
import {setScenarioColors} from 'store/UserPreferenceSlice';
×
26
import {DataContext} from 'context/SelectedDataContext';
×
27
export default function ScenarioLibrary(): JSX.Element {
×
28
  const dispatch = useAppDispatch();
×
29
  const {t} = useTranslation();
×
30
  const theme = useTheme();
×
31

32
  const {scenarios, simulationModels, npis, nodeLists} = useContext(DataContext)!;
×
33
  const scenariosState = useAppSelector((state) => state.dataSelection.scenarios);
×
34

35
  const {data: completeScenarios} = useGetMultiScenariosQuery(scenarios?.map((s) => s.id) ?? [], {skip: !scenarios});
×
36

37
  const scenarioCreated = useCallback(
×
38
    (data: NewScenarioData) => {
×
39
      const result = Object.values(completeScenarios ?? {}).find((scenario: Scenario) => {
×
40
        if (scenario.name === 'casedata') {
×
41
          return false;
×
42
        }
×
43
        if (scenario.linkedInterventions.length === data.npis.length) {
×
44
          return data.npis.every((id) =>
×
45
            scenario.linkedInterventions.find((intervention) => intervention.interventionId === id)
×
46
          );
×
47
        }
×
48
        return false;
×
49
      });
×
50

51
      if (result) {
×
52
        dispatch(
×
53
          updateScenario({
×
54
            id: result.id,
×
55
            state: {
×
56
              name: data.name,
×
57
              description: data.description,
×
58
              visibility: 'faceUp',
×
59
              colors: data.colors,
×
60
            },
×
61
          })
×
62
        );
×
63
        dispatch(
×
64
          setScenarioColors({
×
65
            scenarioId: result.id,
×
66
            colors: data.colors,
×
67
          })
×
68
        );
×
69
      }
×
70
    },
×
71
    [completeScenarios, dispatch]
×
72
  );
×
73

74
  const hiddenScenarios = useMemo(() => {
×
75
    return Object.entries(scenariosState)
×
76
      .filter(([_, value]) => value.visibility === 'inLibrary')
×
77
      .map(([key, scenario]) => ({
×
78
        id: key,
×
79
        name: scenario.name,
×
80
      }));
×
81
  }, [scenariosState]);
×
82

83
  const anchorRef = useRef<HTMLButtonElement>(null);
×
84

85
  const [open, setOpen] = useState(false);
×
86
  const handleToggle = () => {
×
87
    setOpen((prevOpen) => !prevOpen);
×
88
  };
×
89

90
  const id = open ? 'simple-popper' : undefined;
×
91
  const handleClose = (event: Event | React.SyntheticEvent) => {
×
92
    if (anchorRef.current?.contains(event.target as HTMLElement)) {
×
93
      return;
×
94
    }
×
95

96
    setOpen(false);
×
97
  };
×
98

99
  return (
×
100
    <Box>
×
101
      <Button
×
102
        ref={anchorRef}
×
103
        aria-describedby={id}
×
104
        type='button'
×
105
        onClick={handleToggle}
×
106
        id='scenario-add-button'
×
107
        variant='outlined'
×
108
        color='success'
×
109
        sx={{
×
110
          height: '244px',
×
111
          width: '200px',
×
112
          margin: theme.spacing(3),
×
113
          marginTop: theme.spacing(2),
×
114
          fontWeight: 'bolder',
×
115
          fontSize: '3rem',
×
116
          border: `2px ${theme.palette.primary.light} dashed`,
×
117
          borderRadius: '3px',
×
118
          color: theme.palette.primary.light,
×
119
          alignSelf: 'top',
×
120

121
          '&:hover': {
×
122
            border: `2px ${theme.palette.primary.light} dashed`,
×
123
            background: '#E7E7E7',
×
124
          },
×
125
        }}
×
126
        aria-label={t('scenario-library.add')}
×
127
      >
×
128
        +
129
      </Button>
×
130
      <Popper id={id} open={open} anchorEl={anchorRef.current} placement='bottom-start' sx={{zIndex: 100}}>
×
131
        <Paper>
×
132
          <ClickAwayListener onClickAway={handleClose}>
×
133
            <Box
×
134
              sx={{
×
135
                width: '930px',
×
136
                margin: theme.spacing(2),
×
137
                display: 'flex',
×
138
                flexDirection: 'column',
×
139
                flexGrow: '1',
×
140
                padding: theme.spacing(4),
×
141
                alignItems: 'center',
×
142
              }}
×
143
            >
144
              <Box
×
145
                id='group-filter-dialog-title-bar'
×
146
                sx={{
×
147
                  display: 'grid',
×
148
                  gridTemplateColumns: '1fr auto 1fr',
×
149
                  gridColumnGap: '5px',
×
150
                  alignItems: 'center',
×
151
                  justifyItems: 'center',
×
152
                  width: '100%',
×
153
                  marginBottom: theme.spacing(2),
×
154
                }}
×
155
              >
156
                <div />
×
157
                <Typography variant='h1'>{t('scenario-library.title')}</Typography>
×
158
                <IconButton color='primary' sx={{marginLeft: 'auto'}} onClick={() => setOpen(false)}>
×
159
                  <Close />
×
160
                </IconButton>
×
161
              </Box>
×
162
              <Divider orientation='horizontal' variant='middle' flexItem />
×
163
              <Box
×
164
                sx={{
×
165
                  display: 'grid',
×
166
                  gridTemplateColumns: 'repeat(auto-fill, 204px)',
×
167
                  gridGap: '1rem',
×
168
                  justifyContent: 'space-between',
×
169
                  width: '100%',
×
170
                  marginTop: theme.spacing(2),
×
171
                  maxHeight: '500px',
×
172
                  overflowY: 'auto',
×
173
                }}
×
174
              >
175
                <NewScenarioCard
×
176
                  models={simulationModels ?? []}
×
177
                  npis={npis ?? []}
×
178
                  nodeLists={nodeLists ?? []}
×
179
                  scenarioCreated={scenarioCreated}
×
180
                />
×
181
                {hiddenScenarios.length > 0 ? (
×
182
                  hiddenScenarios.map((scenario) => <LibraryCard key={scenario.id} {...scenario} />)
×
183
                ) : (
184
                  <Box
×
185
                    sx={{
×
186
                      margin: 'auto',
×
187
                      display: 'flex',
×
188
                      alignItems: 'center',
×
189
                      justifyContent: 'center',
×
190
                      flexDirection: 'column',
×
191
                    }}
×
192
                  >
193
                    <WebAssetOff color='primary' fontSize='large' />
×
194
                    <Typography variant='body1'>{t('scenario-library.no-scenarios')}</Typography>
×
195
                  </Box>
×
196
                )}
197
              </Box>
×
198
            </Box>
×
199
          </ClickAwayListener>
×
200
        </Paper>
×
201
      </Popper>
×
202
    </Box>
×
203
  );
204
}
×
205

206
function LibraryCard(props: Readonly<{id: string; name: string}>): JSX.Element {
×
207
  const dispatch = useAppDispatch();
×
208
  const theme = useTheme();
×
209
  const {t: tBackend, i18n} = useTranslation('backend');
×
210
  const scenarioColors = useAppSelector((state) => state.userPreference.scenarioColors);
×
211

212
  const handleRestore = () => {
×
213
    const savedColors = scenarioColors[props.id];
×
214
    dispatch(
×
215
      updateScenario({
×
216
        id: props.id,
×
217
        state: {
×
218
          visibility: 'faceUp',
×
219
          colors: savedColors,
×
220
        },
×
221
      })
×
222
    );
×
223
  };
×
224

225
  return (
×
226
    <Box
×
227
      id={`card-root-${props.id}`}
×
228
      sx={{
×
229
        display: 'flex',
×
230
        flexDirection: 'row',
×
231
        color: theme.palette.divider,
×
232
        width: 'min-content',
×
233
      }}
×
234
    >
235
      <Box
×
236
        id='card-container'
×
237
        sx={{
×
238
          position: 'relative',
×
239
          zIndex: 0,
×
240
          flexGrow: 0,
×
241
          flexShrink: 0,
×
242
          width: '200px',
×
243
          boxSizing: 'border-box',
×
244
          marginX: '2px',
×
245
          marginY: theme.spacing(2),
×
246
          marginBottom: 0,
×
247
        }}
×
248
      >
249
        <Box
×
250
          id={`card-main-card-${props.id}`}
×
251
          sx={{
×
252
            position: 'relative',
×
253
            zIndex: 0,
×
254
            boxSizing: 'border-box',
×
255
            height: '244px',
×
256
            paddingTop: theme.spacing(2),
×
257
            paddingBottom: theme.spacing(2),
×
258
            border: `2px solid ${theme.palette.secondary.light}`,
×
259
            borderRadius: '3px',
×
260
            background: theme.palette.background.paper,
×
261
            color: theme.palette.secondary.main,
×
262
            cursor: 'pointer',
×
263

264
            '&:hover': {
×
265
              background: '#EEEEEEEE',
×
266
            },
×
267
          }}
×
268
          onClick={handleRestore}
×
269
        >
270
          <Box
×
271
            id={`card-front-${props.id}`}
×
272
            sx={{
×
273
              marginTop: '6px',
×
274
              marginBottom: '6px',
×
275
              height: '100%',
×
276
              display: 'flex',
×
277
              flexDirection: 'column',
×
278
            }}
×
279
          >
280
            <CardTitle
×
281
              color={theme.palette.secondary.light}
×
282
              label={
×
283
                i18n.exists(`scenario-names.${props.name}`, {ns: 'backend'})
×
284
                  ? tBackend(`scenario-names.${props.name}`)
×
285
                  : props.name
×
286
              }
287
            />
×
288
            <Box
×
289
              sx={{
×
290
                fontWeight: 'bolder',
×
291
                fontSize: '4rem',
×
292
                color: theme.palette.secondary.light,
×
293
                textAlign: 'center',
×
294
                flexGrow: 1,
×
295
                display: 'flex',
×
296
                justifyContent: 'center',
×
297
                alignItems: 'center',
×
298
              }}
×
299
              aria-label={'+'}
×
300
            >
×
301
              +
302
            </Box>
×
303
          </Box>
×
304
        </Box>
×
305
      </Box>
×
306
    </Box>
×
307
  );
308
}
×
309

310
function NewScenarioCard({
×
311
  models,
×
312
  npis,
×
313
  nodeLists,
×
314
  scenarioCreated,
×
315
}: {
×
316
  models: Models;
317
  npis: InterventionTemplates;
318
  nodeLists: NodeLists;
319
  scenarioCreated: (data: NewScenarioData) => void;
320
}): JSX.Element {
×
321
  const theme = useTheme();
×
322
  const {t} = useTranslation();
×
323
  const [newScenarioDialogOpen, setNewScenarioDialogOpen] = useState(false);
×
324

325
  return (
×
326
    <Box
×
327
      id={`new-scenario-card-root`}
×
328
      sx={{
×
329
        display: 'flex',
×
330
        flexDirection: 'row',
×
331
        color: theme.palette.divider,
×
332
        width: 'min-content',
×
333
      }}
×
334
    >
335
      <Box
×
336
        id='new-scenario-card-container'
×
337
        sx={{
×
338
          position: 'relative',
×
339
          zIndex: 0,
×
340
          flexGrow: 0,
×
341
          flexShrink: 0,
×
342
          width: '200px',
×
343
          boxSizing: 'border-box',
×
344
          marginX: '2px',
×
345
          marginY: theme.spacing(2),
×
346
          marginBottom: 0,
×
347
        }}
×
348
      >
349
        <Box
×
350
          id={`new-scenario-card-main-card`}
×
351
          sx={{
×
352
            position: 'relative',
×
353
            zIndex: 0,
×
354
            boxSizing: 'border-box',
×
355
            height: '244px',
×
356
            paddingTop: theme.spacing(2),
×
357
            paddingBottom: theme.spacing(2),
×
358
            border: `2px solid ${theme.palette.primary.main}`,
×
359
            borderRadius: '3px',
×
360
            background: theme.palette.background.paper,
×
361
            color: theme.palette.primary.main,
×
362
            cursor: 'pointer',
×
363

364
            '&:hover': {
×
365
              background: '#EEEEEEEE',
×
366
            },
×
367
          }}
×
368
          onClick={() => setNewScenarioDialogOpen(true)}
×
369
        >
370
          <Box
×
371
            id={`new-scenario-card-front`}
×
372
            sx={{
×
373
              marginTop: '6px',
×
374
              marginBottom: '6px',
×
375
              height: '100%',
×
376
              display: 'flex',
×
377
              flexDirection: 'column',
×
378
            }}
×
379
          >
380
            <CardTitle label={t('scenario-library.new-scenario')} />
×
381
            <Box
×
382
              sx={{
×
383
                fontWeight: 'bolder',
×
384
                fontSize: '3rem',
×
385
                color: theme.palette.primary.main,
×
386
                textAlign: 'center',
×
387
                flexGrow: 1,
×
388
                display: 'flex',
×
389
                justifyContent: 'center',
×
390
                alignItems: 'center',
×
391
              }}
×
392
              aria-label={t('scenario-library.new-scenario') as unknown as string}
×
393
            >
394
              <LibraryAddOutlined color='primary' fontSize='large' />
×
395
            </Box>
×
396
          </Box>
×
397
        </Box>
×
398
      </Box>
×
399
      <Dialog open={newScenarioDialogOpen} maxWidth='sm' fullWidth={true}>
×
400
        <NewScenarioDialog
×
401
          models={[models[0]?.name ?? 'SECIRVV']}
×
402
          npiOptions={npis.map((npi) => {
×
403
            return {
×
404
              id: npi.id,
×
405
              name: npi.name,
×
406
            };
×
407
          })}
×
408
          nodeOptions={[nodeLists[0]?.name ?? 'Districts of Germany']}
×
409
          colorOptions={theme.custom.scenarios.slice(1)}
×
410
          onSubmit={(data) => {
×
411
            if (data) {
×
412
              scenarioCreated(data);
×
413
            }
×
414
            setNewScenarioDialogOpen(false);
×
415
          }}
×
416
        />
×
417
      </Dialog>
×
418
    </Box>
×
419
  );
420
}
×
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