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

eclipsesource / jsonforms / 16975799101

14 Aug 2025 08:21PM UTC coverage: 82.791% (-0.005%) from 82.796%
16975799101

push

github

jsonforms-publish
v3.7.0-alpha.1

10800 of 28948 branches covered (37.31%)

18638 of 22512 relevant lines covered (82.79%)

32.96 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
            justifyContent='flex-end'
314
            alignItems='center'
315
          >
316
            {showSortButtons ? (
101✔
317
              <Fragment>
318
                <Grid>
319
                  <Tooltip
320
                    id='tooltip-up'
321
                    title={translations.up}
322
                    placement='bottom'
323
                    open={enableUp ? undefined : false}
46✔
324
                  >
325
                    <IconButton
326
                      aria-label={translations.upAriaLabel}
327
                      onClick={moveUp}
328
                      disabled={!enableUp}
329
                      size='large'
330
                    >
331
                      <ArrowUpward />
332
                    </IconButton>
333
                  </Tooltip>
334
                </Grid>
335
                <Grid>
336
                  <Tooltip
337
                    id='tooltip-down'
338
                    title={translations.down}
339
                    placement='bottom'
340
                    open={enableDown ? undefined : false}
46✔
341
                  >
342
                    <IconButton
343
                      aria-label={translations.downAriaLabel}
344
                      onClick={moveDown}
345
                      disabled={!enableDown}
346
                      size='large'
347
                    >
348
                      <ArrowDownward />
349
                    </IconButton>
350
                  </Tooltip>
351
                </Grid>
352
              </Fragment>
353
            ) : null}
354
            {!disableRemove ? (
101✔
355
              <Grid>
356
                <Tooltip
357
                  id='tooltip-remove'
358
                  title={translations.removeTooltip}
359
                  placement='bottom'
360
                >
361
                  <IconButton
362
                    aria-label={translations.removeAriaLabel}
363
                    onClick={() => openDeleteDialog(childPath, rowIndex)}
1✔
364
                    size='large'
365
                  >
366
                    <Delete />
367
                  </IconButton>
368
                </Tooltip>
369
              </Grid>
370
            ) : null}
371
          </Grid>
372
        </NoBorderTableCell>
373
      ) : null}
374
    </TableRow>
375
  );
376
};
377
export const NonEmptyRow = React.memo(NonEmptyRowComponent);
32✔
378
interface TableRowsProp {
379
  data: number;
380
  path: string;
381
  schema: JsonSchema;
382
  uischema: ControlElement;
383
  config?: any;
384
  enabled: boolean;
385
  cells?: JsonFormsCellRendererRegistryEntry[];
386
  moveUp?(path: string, toMove: number): () => void;
387
  moveDown?(path: string, toMove: number): () => void;
388
  translations: ArrayTranslations;
389
  disableRemove?: boolean;
390
}
391
const TableRows = ({
32✔
392
  data,
113✔
393
  path,
113✔
394
  schema,
113✔
395
  openDeleteDialog,
113✔
396
  moveUp,
113✔
397
  moveDown,
113✔
398
  uischema,
113✔
399
  config,
113✔
400
  enabled,
113✔
401
  cells,
113✔
402
  translations,
113✔
403
  disableRemove,
113✔
404
}: TableRowsProp & WithDeleteDialogSupport) => {
405
  const isEmptyTable = data === 0;
113✔
406

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

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

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

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

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

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

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

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

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