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

eclipsesource / jsonforms / 20269596867

16 Dec 2025 01:28PM UTC coverage: 82.573% (-0.2%) from 82.798%
20269596867

Pull #2470

github

web-flow
Apply suggestions from code review
Pull Request #2470: dev: add project information for Theia AI

10587 of 28486 branches covered (37.17%)

18294 of 22155 relevant lines covered (82.57%)

27.14 hits per line

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

99.2
/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 DeleteIcon from '@mui/icons-material/Delete';
32✔
61
import ArrowDownward from '@mui/icons-material/ArrowDownward';
32✔
62
import ArrowUpward from '@mui/icons-material/ArrowUpward';
32✔
63

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

488
    if (!visible) {
113!
489
      return null;
×
490
    }
491

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

© 2026 Coveralls, Inc