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

eclipsesource / jsonforms / 9503084136

13 Jun 2024 04:10PM UTC coverage: 83.244% (+0.007%) from 83.237%
9503084136

push

github

lucas-koehler
Allow handing in numbers (indices) to path composition

9295 of 21268 branches covered (43.7%)

14 of 15 new or added lines in 9 files covered. (93.33%)

88 existing lines in 24 files now uncovered.

16459 of 19772 relevant lines covered (83.24%)

28.62 hits per line

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

99.19
/packages/material-renderers/src/complex/MaterialTableControl.tsx
1
/*
2
  The MIT License
3

4
  Copyright (c) 2017-2019 EclipseSource Munich
5
  https://github.com/eclipsesource/jsonforms
6

7
  Permission is hereby granted, free of charge, to any person obtaining a copy
8
  of this software and associated documentation files (the "Software"), to deal
9
  in the Software without restriction, including without limitation the rights
10
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
  copies of the Software, and to permit persons to whom the Software is
12
  furnished to do so, subject to the following conditions:
13

14
  The above copyright notice and this permission notice shall be included in
15
  all copies or substantial portions of the Software.
16

17
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
  THE SOFTWARE.
24
*/
25
import isEmpty from 'lodash/isEmpty';
31✔
26
import union from 'lodash/union';
31✔
27
import {
31✔
28
  DispatchCell,
29
  JsonFormsStateContext,
30
  useJsonForms,
31
} from '@jsonforms/react';
32
import startCase from 'lodash/startCase';
31✔
33
import range from 'lodash/range';
31✔
34
import React, { Fragment, useMemo } from 'react';
31✔
35
import {
31✔
36
  FormHelperText,
37
  Grid,
38
  IconButton,
39
  Table,
40
  TableBody,
41
  TableCell,
42
  TableHead,
43
  TableRow,
44
  Tooltip,
45
  Typography,
46
} from '@mui/material';
47
import {
31✔
48
  ArrayLayoutProps,
49
  ControlElement,
50
  errorsAt,
51
  formatErrorMessage,
52
  JsonSchema,
53
  Paths,
54
  Resolve,
55
  JsonFormsRendererRegistryEntry,
56
  JsonFormsCellRendererRegistryEntry,
57
  ArrayTranslations,
58
} from '@jsonforms/core';
59
import {
31✔
60
  Delete as DeleteIcon,
61
  ArrowDownward,
62
  ArrowUpward,
63
} from '@mui/icons-material';
64

65
import { WithDeleteDialogSupport } from './DeleteDialog';
66
import NoBorderTableCell from './NoBorderTableCell';
31✔
67
import TableToolbar from './TableToolbar';
31✔
68
import { ErrorObject } from 'ajv';
69
import merge from 'lodash/merge';
31✔
70

71
// we want a cell that doesn't automatically span
72
const styles = {
31✔
73
  fixedCell: {
74
    width: '150px',
75
    height: '50px',
76
    paddingLeft: 0,
77
    paddingRight: 0,
78
    textAlign: 'center',
79
  },
80
  fixedCellSmall: {
81
    width: '50px',
82
    height: '50px',
83
    paddingLeft: 0,
84
    paddingRight: 0,
85
    textAlign: 'center',
86
  },
87
};
88

89
const generateCells = (
31✔
90
  Cell: React.ComponentType<OwnPropsOfNonEmptyCell | TableHeaderCellProps>,
91
  schema: JsonSchema,
92
  rowPath: string,
93
  enabled: boolean,
94
  cells?: JsonFormsCellRendererRegistryEntry[]
95
) => {
96
  if (schema.type === 'object') {
179✔
97
    return getValidColumnProps(schema).map((prop) => {
108✔
98
      const cellPath = Paths.compose(rowPath, prop);
198✔
99
      const props = {
198✔
100
        propName: prop,
101
        schema,
102
        title: schema.properties?.[prop]?.title ?? startCase(prop),
1,782✔
103
        rowPath,
104
        cellPath,
105
        enabled,
106
        cells,
107
      };
108
      return <Cell key={cellPath} {...props} />;
198✔
109
    });
110
  } else {
111
    // primitives
112
    const props = {
71✔
113
      schema,
114
      rowPath,
115
      cellPath: rowPath,
116
      enabled,
117
    };
118
    return <Cell key={rowPath} {...props} />;
71✔
119
  }
120
};
121

122
const getValidColumnProps = (scopedSchema: JsonSchema) => {
31✔
123
  if (
120✔
124
    scopedSchema.type === 'object' &&
236✔
125
    typeof scopedSchema.properties === 'object'
126
  ) {
127
    return Object.keys(scopedSchema.properties).filter(
114✔
128
      (prop) => scopedSchema.properties[prop].type !== 'array'
207✔
129
    );
130
  }
131
  // primitives
132
  return [''];
6✔
133
};
134

135
export interface EmptyTableProps {
136
  numColumns: number;
137
  translations: ArrayTranslations;
138
}
139

140
const EmptyTable = ({ numColumns, translations }: EmptyTableProps) => (
31✔
141
  <TableRow>
142
    <NoBorderTableCell colSpan={numColumns}>
143
      <Typography align='center'>{translations.noDataMessage}</Typography>
144
    </NoBorderTableCell>
145
  </TableRow>
146
);
147

148
interface TableHeaderCellProps {
149
  title: string;
150
}
151

152
const TableHeaderCell = React.memo(function TableHeaderCell({
31✔
153
  title,
110✔
154
}: TableHeaderCellProps) {
155
  return <TableCell>{title}</TableCell>;
110✔
156
});
157

158
interface NonEmptyCellProps extends OwnPropsOfNonEmptyCell {
159
  rootSchema: JsonSchema;
160
  errors: string;
161
  path: string;
162
  enabled: boolean;
163
}
164
interface OwnPropsOfNonEmptyCell {
165
  rowPath: string;
166
  propName?: string;
167
  schema: JsonSchema;
168
  enabled: boolean;
169
  renderers?: JsonFormsRendererRegistryEntry[];
170
  cells?: JsonFormsCellRendererRegistryEntry[];
171
}
172
const ctxToNonEmptyCellProps = (
31✔
173
  ctx: JsonFormsStateContext,
174
  ownProps: OwnPropsOfNonEmptyCell
175
): NonEmptyCellProps => {
176
  const path =
177
    ownProps.rowPath +
138✔
178
    (ownProps.schema.type === 'object' ? '/' + ownProps.propName : '');
138✔
179
  const errors = formatErrorMessage(
138✔
180
    union(
181
      errorsAt(
182
        path,
183
        ownProps.schema,
184
        (p) => p === path
106✔
185
      )(ctx.core.errors).map((error: ErrorObject) => error.message)
27✔
186
    )
187
  );
188
  return {
138✔
189
    rowPath: ownProps.rowPath,
190
    propName: ownProps.propName,
191
    schema: ownProps.schema,
192
    rootSchema: ctx.core.schema,
193
    errors,
194
    path,
195
    enabled: ownProps.enabled,
196
    cells: ownProps.cells || ctx.cells,
258✔
197
    renderers: ownProps.renderers || ctx.renderers,
276✔
198
  };
199
};
200

201
const controlWithoutLabel = (scope: string): ControlElement => ({
120✔
202
  type: 'Control',
203
  scope: scope,
204
  label: false,
205
});
206

207
interface NonEmptyCellComponentProps {
208
  path: string;
209
  propName?: string;
210
  schema: JsonSchema;
211
  rootSchema: JsonSchema;
212
  errors: string;
213
  enabled: boolean;
214
  renderers?: JsonFormsRendererRegistryEntry[];
215
  cells?: JsonFormsCellRendererRegistryEntry[];
216
  isValid: boolean;
217
}
218
const NonEmptyCellComponent = React.memo(function NonEmptyCellComponent({
31✔
219
  path,
120✔
220
  propName,
120✔
221
  schema,
120✔
222
  rootSchema,
120✔
223
  errors,
120✔
224
  enabled,
120✔
225
  renderers,
120✔
226
  cells,
120✔
227
  isValid,
120✔
228
}: NonEmptyCellComponentProps) {
229
  return (
120✔
230
    <NoBorderTableCell>
231
      {schema.properties ? (
120✔
232
        <DispatchCell
233
          schema={Resolve.schema(
234
            schema,
235
            Paths.compose('#', 'properties', propName),
236
            rootSchema
237
          )}
238
          uischema={controlWithoutLabel(
239
            Paths.compose('#', 'properties', propName)
240
          )}
241
          path={path}
242
          enabled={enabled}
243
          renderers={renderers}
244
          cells={cells}
245
        />
246
      ) : (
247
        <DispatchCell
248
          schema={schema}
249
          uischema={controlWithoutLabel('#')}
250
          path={path}
251
          enabled={enabled}
252
          renderers={renderers}
253
          cells={cells}
254
        />
255
      )}
256
      <FormHelperText error={!isValid}>{!isValid && errors}</FormHelperText>
146✔
257
    </NoBorderTableCell>
258
  );
259
});
260

261
const NonEmptyCell = (ownProps: OwnPropsOfNonEmptyCell) => {
31✔
262
  const ctx = useJsonForms();
138✔
263
  const emptyCellProps = ctxToNonEmptyCellProps(ctx, ownProps);
138✔
264

265
  const isValid = isEmpty(emptyCellProps.errors);
138✔
266
  return <NonEmptyCellComponent {...emptyCellProps} isValid={isValid} />;
138✔
267
};
268

269
interface NonEmptyRowProps {
270
  childPath: string;
271
  schema: JsonSchema;
272
  rowIndex: number;
273
  moveUpCreator: (path: string, position: number) => () => void;
274
  moveDownCreator: (path: string, position: number) => () => void;
275
  enableUp: boolean;
276
  enableDown: boolean;
277
  showSortButtons: boolean;
278
  enabled: boolean;
279
  cells?: JsonFormsCellRendererRegistryEntry[];
280
  path: string;
281
  translations: ArrayTranslations;
282
  disableRemove?: boolean;
283
}
284

285
const NonEmptyRowComponent = ({
31✔
286
  childPath,
108✔
287
  schema,
108✔
288
  rowIndex,
108✔
289
  openDeleteDialog,
108✔
290
  moveUpCreator,
108✔
291
  moveDownCreator,
108✔
292
  enableUp,
108✔
293
  enableDown,
108✔
294
  showSortButtons,
108✔
295
  enabled,
108✔
296
  cells,
108✔
297
  path,
108✔
298
  translations,
108✔
299
  disableRemove,
108✔
300
}: NonEmptyRowProps & WithDeleteDialogSupport) => {
301
  const moveUp = useMemo(
108✔
302
    () => moveUpCreator(path, rowIndex),
92✔
303
    [moveUpCreator, path, rowIndex]
304
  );
305
  const moveDown = useMemo(
108✔
306
    () => moveDownCreator(path, rowIndex),
92✔
307
    [moveDownCreator, path, rowIndex]
308
  );
309
  return (
108✔
310
    <TableRow key={childPath} hover>
311
      {generateCells(NonEmptyCell, schema, childPath, enabled, cells)}
312
      {enabled ? (
108✔
313
        <NoBorderTableCell
314
          style={showSortButtons ? styles.fixedCell : styles.fixedCellSmall}
100✔
315
        >
316
          <Grid
317
            container
318
            direction='row'
319
            justifyContent='flex-end'
320
            alignItems='center'
321
          >
322
            {showSortButtons ? (
100✔
323
              <Fragment>
324
                <Grid item>
325
                  <Tooltip
326
                    id='tooltip-up'
327
                    title={translations.up}
328
                    placement='bottom'
329
                    open={enableUp ? undefined : false}
52✔
330
                  >
331
                    <IconButton
332
                      aria-label={translations.upAriaLabel}
333
                      onClick={moveUp}
334
                      disabled={!enableUp}
335
                      size='large'
336
                    >
337
                      <ArrowUpward />
338
                    </IconButton>
339
                  </Tooltip>
340
                </Grid>
341
                <Grid item>
342
                  <Tooltip
343
                    id='tooltip-down'
344
                    title={translations.down}
345
                    placement='bottom'
346
                    open={enableDown ? undefined : false}
52✔
347
                  >
348
                    <IconButton
349
                      aria-label={translations.downAriaLabel}
350
                      onClick={moveDown}
351
                      disabled={!enableDown}
352
                      size='large'
353
                    >
354
                      <ArrowDownward />
355
                    </IconButton>
356
                  </Tooltip>
357
                </Grid>
358
              </Fragment>
359
            ) : null}
360
            {!disableRemove ? (
100✔
361
              <Grid item>
362
                <Tooltip
363
                  id='tooltip-remove'
364
                  title={translations.removeTooltip}
365
                  placement='bottom'
366
                >
367
                  <IconButton
368
                    aria-label={translations.removeAriaLabel}
369
                    onClick={() => openDeleteDialog(childPath, rowIndex)}
1✔
370
                    size='large'
371
                  >
372
                    <DeleteIcon />
373
                  </IconButton>
374
                </Tooltip>
375
              </Grid>
376
            ) : null}
377
          </Grid>
378
        </NoBorderTableCell>
379
      ) : null}
380
    </TableRow>
381
  );
382
};
383
export const NonEmptyRow = React.memo(NonEmptyRowComponent);
31✔
384
interface TableRowsProp {
385
  data: number;
386
  path: string;
387
  schema: JsonSchema;
388
  uischema: ControlElement;
389
  config?: any;
390
  enabled: boolean;
391
  cells?: JsonFormsCellRendererRegistryEntry[];
392
  moveUp?(path: string, toMove: number): () => void;
393
  moveDown?(path: string, toMove: number): () => void;
394
  translations: ArrayTranslations;
395
  disableRemove?: boolean;
396
}
397
const TableRows = ({
31✔
398
  data,
103✔
399
  path,
103✔
400
  schema,
103✔
401
  openDeleteDialog,
103✔
402
  moveUp,
103✔
403
  moveDown,
103✔
404
  uischema,
103✔
405
  config,
103✔
406
  enabled,
103✔
407
  cells,
103✔
408
  translations,
103✔
409
  disableRemove,
103✔
410
}: TableRowsProp & WithDeleteDialogSupport) => {
411
  const isEmptyTable = data === 0;
103✔
412

413
  if (isEmptyTable) {
103✔
414
    return (
12✔
415
      <EmptyTable
416
        numColumns={getValidColumnProps(schema).length + 1}
417
        translations={translations}
418
      />
419
    );
420
  }
421

422
  const appliedUiSchemaOptions = merge({}, config, uischema.options);
91✔
423

424
  return (
91✔
425
    <React.Fragment>
426
      {range(data).map((index: number) => {
427
        const childPath = Paths.compose(path, index);
110✔
428

429
        return (
110✔
430
          <NonEmptyRow
431
            key={childPath}
432
            childPath={childPath}
433
            rowIndex={index}
434
            schema={schema}
435
            openDeleteDialog={openDeleteDialog}
436
            moveUpCreator={moveUp}
437
            moveDownCreator={moveDown}
438
            enableUp={index !== 0}
439
            enableDown={index !== data - 1}
440
            showSortButtons={
441
              appliedUiSchemaOptions.showSortButtons ||
162✔
442
              appliedUiSchemaOptions.showArrayTableSortButtons
443
            }
444
            enabled={enabled}
445
            cells={cells}
446
            path={path}
447
            translations={translations}
448
            disableRemove={disableRemove}
449
          />
450
        );
451
      })}
452
    </React.Fragment>
453
  );
454
};
455

456
export class MaterialTableControl extends React.Component<
81✔
457
  ArrayLayoutProps & WithDeleteDialogSupport,
458
  any
459
> {
460
  addItem = (path: string, value: any) => this.props.addItem(path, value);
95✔
461
  render() {
31✔
462
    const {
463
      label,
103✔
464
      description,
103✔
465
      path,
103✔
466
      schema,
103✔
467
      rootSchema,
103✔
468
      uischema,
103✔
469
      errors,
103✔
470
      openDeleteDialog,
103✔
471
      visible,
103✔
472
      enabled,
103✔
473
      cells,
103✔
474
      translations,
103✔
475
      disableAdd,
103✔
476
      disableRemove,
103✔
477
      config,
103✔
478
    } = this.props;
103✔
479

480
    const appliedUiSchemaOptions = merge({}, config, uischema.options);
103✔
481
    const doDisableAdd = disableAdd || appliedUiSchemaOptions.disableAdd;
103✔
482
    const doDisableRemove =
483
      disableRemove || appliedUiSchemaOptions.disableRemove;
103✔
484

485
    const controlElement = uischema as ControlElement;
103✔
486
    const isObjectSchema = schema.type === 'object';
103✔
487
    const headerCells: any = isObjectSchema
103✔
488
      ? generateCells(TableHeaderCell, schema, path, enabled, cells)
489
      : undefined;
490

491
    if (!visible) {
103!
UNCOV
492
      return null;
×
493
    }
494

495
    return (
103✔
496
      <Table>
497
        <TableHead>
498
          <TableToolbar
499
            errors={errors}
500
            label={label}
501
            description={description}
502
            addItem={this.addItem}
503
            numColumns={isObjectSchema ? headerCells.length : 1}
103✔
504
            path={path}
505
            uischema={controlElement}
506
            schema={schema}
507
            rootSchema={rootSchema}
508
            enabled={enabled}
509
            translations={translations}
510
            disableAdd={doDisableAdd}
511
          />
512
          {isObjectSchema && (
174✔
513
            <TableRow>
514
              {headerCells}
515
              {enabled ? <TableCell /> : null}
71✔
516
            </TableRow>
517
          )}
518
        </TableHead>
519
        <TableBody>
520
          <TableRows
521
            openDeleteDialog={openDeleteDialog}
522
            translations={translations}
523
            {...this.props}
524
            disableRemove={doDisableRemove}
525
          />
526
        </TableBody>
527
      </Table>
528
    );
529
  }
530
}
31✔
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