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

eclipsesource / jsonforms / 8687813307

15 Apr 2024 10:48AM UTC coverage: 84.383% (-0.007%) from 84.39%
8687813307

push

github

web-flow
react-material: Remove usage of Material's Hidden component (#2315)

Removes the deprecated <Hidden> component and instead returns null if the hidden property is set.

Closes #2107

10062 of 22976 branches covered (43.79%)

52 of 60 new or added lines in 21 files covered. (86.67%)

1 existing line in 1 file now uncovered.

17512 of 20753 relevant lines covered (84.38%)

27.86 hits per line

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

99.14
/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
  encode,
58
  ArrayTranslations,
59
} from '@jsonforms/core';
60
import {
31✔
61
  Delete as DeleteIcon,
62
  ArrowDownward,
63
  ArrowUpward,
64
} from '@mui/icons-material';
65

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

406
  if (isEmptyTable) {
93✔
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);
81✔
416

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

422
        return (
85✔
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 ||
127✔
435
              appliedUiSchemaOptions.showArrayTableSortButtons
436
            }
437
            enabled={enabled}
438
            cells={cells}
439
            path={path}
440
            translations={translations}
441
          />
442
        );
443
      })}
444
    </React.Fragment>
445
  );
446
};
447

448
export class MaterialTableControl extends React.Component<
71✔
449
  ArrayLayoutProps & WithDeleteDialogSupport,
450
  any
451
> {
452
  addItem = (path: string, value: any) => this.props.addItem(path, value);
91✔
453
  render() {
31✔
454
    const {
455
      label,
93✔
456
      description,
93✔
457
      path,
93✔
458
      schema,
93✔
459
      rootSchema,
93✔
460
      uischema,
93✔
461
      errors,
93✔
462
      openDeleteDialog,
93✔
463
      visible,
93✔
464
      enabled,
93✔
465
      cells,
93✔
466
      translations,
93✔
467
    } = this.props;
93✔
468

469
    const controlElement = uischema as ControlElement;
93✔
470
    const isObjectSchema = schema.type === 'object';
93✔
471
    const headerCells: any = isObjectSchema
93✔
472
      ? generateCells(TableHeaderCell, schema, path, enabled, cells)
473
      : undefined;
474

475
    if (!visible) {
93!
NEW
476
      return null;
×
477
    }
478

479
    return (
93✔
480
      <Table>
481
        <TableHead>
482
          <TableToolbar
483
            errors={errors}
484
            label={label}
485
            description={description}
486
            addItem={this.addItem}
487
            numColumns={isObjectSchema ? headerCells.length : 1}
93✔
488
            path={path}
489
            uischema={controlElement}
490
            schema={schema}
491
            rootSchema={rootSchema}
492
            enabled={enabled}
493
            translations={translations}
494
          />
495
          {isObjectSchema && (
159✔
496
            <TableRow>
497
              {headerCells}
498
              {enabled ? <TableCell /> : null}
66!
499
            </TableRow>
500
          )}
501
        </TableHead>
502
        <TableBody>
503
          <TableRows
504
            openDeleteDialog={openDeleteDialog}
505
            translations={translations}
506
            {...this.props}
507
          />
508
        </TableBody>
509
      </Table>
510
    );
511
  }
512
}
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