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

eclipsesource / jsonforms / 31018915041

05 Aug 2026 03:10PM UTC coverage: 84.286%. Remained the same
31018915041

Pull #2614

github

web-flow
Merge 87a79fdde into 141f28838
Pull Request #2614: MUI v9 upgrade (material-renderers)

12543 of 31932 branches covered (39.28%)

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

4 existing lines in 2 files now uncovered.

20318 of 24106 relevant lines covered (84.29%)

33.65 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';
32✔
26
import union from 'lodash/union';
32✔
27
import {
32✔
28
  DispatchCell,
29
  JsonFormsStateContext,
30
  useJsonForms,
31
} from '@jsonforms/react';
32
import startCase from 'lodash/startCase';
32✔
33
import range from 'lodash/range';
32✔
34
import React, { Fragment, useMemo } from 'react';
32✔
35
import {
32✔
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 {
32✔
48
  ArrayLayoutProps,
49
  ControlElement,
50
  errorAt,
51
  formatErrorMessage,
52
  JsonSchema,
53
  Paths,
54
  Resolve,
55
  JsonFormsRendererRegistryEntry,
56
  JsonFormsCellRendererRegistryEntry,
57
  encode,
58
  ArrayTranslations,
59
} from '@jsonforms/core';
60
import { Delete, ArrowDownward, ArrowUpward } from '@mui/icons-material';
32✔
61

62
import { WithDeleteDialogSupport } from './DeleteDialog';
63
import NoBorderTableCell from './NoBorderTableCell';
32✔
64
import TableToolbar from './TableToolbar';
32✔
65
import { ErrorObject } from 'ajv';
66
import merge from 'lodash/merge';
32✔
67

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

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

119
const getValidColumnProps = (scopedSchema: JsonSchema) => {
32✔
120
  if (
140✔
121
    scopedSchema.type === 'object' &&
276✔
122
    typeof scopedSchema.properties === 'object'
123
  ) {
124
    return Object.keys(scopedSchema.properties).filter(
134✔
125
      (prop) => scopedSchema.properties[prop].type !== 'array'
249✔
126
    );
127
  }
128
  // primitives
129
  return [''];
6✔
130
};
131

132
export interface EmptyTableProps {
133
  numColumns: number;
134
  translations: ArrayTranslations;
135
}
136

137
const EmptyTable = ({ numColumns, translations }: EmptyTableProps) => (
32✔
138
  <TableRow>
139
    <NoBorderTableCell colSpan={numColumns}>
140
      <Typography align='center'>{translations.noDataMessage}</Typography>
141
    </NoBorderTableCell>
142
  </TableRow>
143
);
144

145
interface TableHeaderCellProps {
146
  title: string;
147
}
148

149
const TableHeaderCell = React.memo(function TableHeaderCell({
32✔
150
  title,
132✔
151
}: TableHeaderCellProps) {
152
  return <TableCell>{title}</TableCell>;
132✔
153
});
154

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

197
const controlWithoutLabel = (scope: string): ControlElement => ({
142✔
198
  type: 'Control',
199
  scope: scope,
200
  label: false,
201
});
202

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

255
const NonEmptyCell = (ownProps: OwnPropsOfNonEmptyCell) => {
32✔
256
  const ctx = useJsonForms();
158✔
257
  const emptyCellProps = ctxToNonEmptyCellProps(ctx, ownProps);
158✔
258

259
  const isValid = isEmpty(emptyCellProps.errors);
158✔
260
  return <NonEmptyCellComponent {...emptyCellProps} isValid={isValid} />;
158✔
261
};
262

263
interface NonEmptyRowProps {
264
  childPath: string;
265
  schema: JsonSchema;
266
  rowIndex: number;
267
  moveUpCreator: (path: string, position: number) => () => void;
268
  moveDownCreator: (path: string, position: number) => () => void;
269
  enableUp: boolean;
270
  enableDown: boolean;
271
  showSortButtons: boolean;
272
  enabled: boolean;
273
  cells?: JsonFormsCellRendererRegistryEntry[];
274
  path: string;
275
  translations: ArrayTranslations;
276
  disableRemove?: boolean;
277
}
278

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

406
  if (isEmptyTable) {
113✔
407
    return (
12✔
408
      <EmptyTable
409
        numColumns={getValidColumnProps(schema).length + 1}
410
        translations={translations}
411
      />
412
    );
413
  }
414

415
  const appliedUiSchemaOptions = merge({}, config, uischema.options);
101✔
416

417
  return (
101✔
418
    <React.Fragment>
419
      {range(data).map((index: number) => {
420
        const childPath = Paths.compose(path, `${index}`);
119✔
421

422
        return (
119✔
423
          <NonEmptyRow
424
            key={childPath}
425
            childPath={childPath}
426
            rowIndex={index}
427
            schema={schema}
428
            openDeleteDialog={openDeleteDialog}
429
            moveUpCreator={moveUp}
430
            moveDownCreator={moveDown}
431
            enableUp={index !== 0}
432
            enableDown={index !== data - 1}
433
            showSortButtons={
434
              appliedUiSchemaOptions.showSortButtons ||
180✔
435
              appliedUiSchemaOptions.showArrayTableSortButtons
436
            }
437
            enabled={enabled}
438
            cells={cells}
439
            path={path}
440
            translations={translations}
441
            disableRemove={disableRemove}
442
          />
443
        );
444
      })}
445
    </React.Fragment>
446
  );
447
};
448

449
export class MaterialTableControl extends React.Component<
32✔
450
  ArrayLayoutProps &
451
    WithDeleteDialogSupport & { translations: ArrayTranslations },
452
  any
453
> {
454
  addItem = (path: string, value: any) => this.props.addItem(path, value);
92✔
455
  render() {
32✔
456
    const {
457
      label,
113✔
458
      description,
113✔
459
      path,
113✔
460
      schema,
113✔
461
      rootSchema,
113✔
462
      uischema,
113✔
463
      errors,
113✔
464
      openDeleteDialog,
113✔
465
      visible,
113✔
466
      enabled,
113✔
467
      cells,
113✔
468
      translations,
113✔
469
      disableAdd,
113✔
470
      disableRemove,
113✔
471
      config,
113✔
472
    } = this.props;
113✔
473

474
    const appliedUiSchemaOptions = merge({}, config, uischema.options);
113✔
475
    const doDisableAdd = disableAdd || appliedUiSchemaOptions.disableAdd;
113✔
476
    const doDisableRemove =
477
      disableRemove || appliedUiSchemaOptions.disableRemove;
113✔
478

479
    const controlElement = uischema as ControlElement;
113✔
480
    const isObjectSchema = schema.type === 'object';
113✔
481
    const headerCells: any = isObjectSchema
113✔
482
      ? generateCells(TableHeaderCell, schema, path, enabled, cells)
483
      : undefined;
484

485
    if (!visible) {
113!
UNCOV
486
      return null;
×
487
    }
488

489
    return (
113✔
490
      <Table>
491
        <TableHead>
492
          <TableToolbar
493
            errors={errors}
494
            label={label}
495
            description={description}
496
            addItem={this.addItem}
497
            numColumns={isObjectSchema ? headerCells.length : 1}
113✔
498
            path={path}
499
            uischema={controlElement}
500
            schema={schema}
501
            rootSchema={rootSchema}
502
            enabled={enabled}
503
            translations={translations}
504
            disableAdd={doDisableAdd}
505
          />
506
          {isObjectSchema && (
195✔
507
            <TableRow>
508
              {headerCells}
509
              {enabled ? <TableCell /> : null}
82✔
510
            </TableRow>
511
          )}
512
        </TableHead>
513
        <TableBody>
514
          <TableRows
515
            openDeleteDialog={openDeleteDialog}
516
            translations={translations}
517
            {...this.props}
518
            disableRemove={doDisableRemove}
519
          />
520
        </TableBody>
521
      </Table>
522
    );
523
  }
524
}
32✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc