|
| 1 | +/* |
| 2 | + The MIT License |
| 3 | +
|
| 4 | + Copyright (c) 2017-2019 EclipseSource Munich |
| 5 | + https://github.com/eclipsesource/jsonforms |
| 6 | +
|
| 7 | + Copyright (c) 2020 headwire.com, Inc |
| 8 | + https://github.com/headwirecom/jsonforms-react-spectrum-renderers |
| 9 | +
|
| 10 | + Permission is hereby granted, free of charge, to any person obtaining a copy |
| 11 | + of this software and associated documentation files (the "Software"), to deal |
| 12 | + in the Software without restriction, including without limitation the rights |
| 13 | + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 14 | + copies of the Software, and to permit persons to whom the Software is |
| 15 | + furnished to do so, subject to the following conditions: |
| 16 | +
|
| 17 | + The above copyright notice and this permission notice shall be included in |
| 18 | + all copies or substantial portions of the Software. |
| 19 | +
|
| 20 | + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 21 | + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 22 | + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 23 | + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 24 | + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 25 | + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| 26 | + THE SOFTWARE. |
| 27 | +*/ |
| 28 | +import React from 'react'; |
| 29 | +import startCase from 'lodash/startCase'; |
| 30 | +import { |
| 31 | + ArrayControlProps, |
| 32 | + ControlElement, |
| 33 | + createDefaultValue, |
| 34 | + Helpers, |
| 35 | + isPlainLabel, |
| 36 | + Paths, |
| 37 | + RankedTester, |
| 38 | + Resolve, |
| 39 | + Test, |
| 40 | +} from '@jsonforms/core'; |
| 41 | +import { DispatchCell, withJsonFormsArrayControlProps } from '@jsonforms/react'; |
| 42 | +import { |
| 43 | + ActionButton, |
| 44 | + AlertDialog, |
| 45 | + DialogTrigger, |
| 46 | + Flex, |
| 47 | + Text, |
| 48 | + Tooltip, |
| 49 | + TooltipTrigger, |
| 50 | + View, |
| 51 | + Grid, |
| 52 | +} from '@adobe/react-spectrum'; |
| 53 | + |
| 54 | +import Delete from '@spectrum-icons/workflow/Delete'; |
| 55 | +import { |
| 56 | + getUIOptions, |
| 57 | + getChildError, |
| 58 | + ArrayHeader, |
| 59 | + ArrayFooter, |
| 60 | +} from './array/utils'; |
| 61 | + |
| 62 | +const { createLabelDescriptionFrom } = Helpers; |
| 63 | + |
| 64 | +const { |
| 65 | + or, |
| 66 | + isObjectArrayControl, |
| 67 | + isPrimitiveArrayControl, |
| 68 | + rankWith, |
| 69 | + and, |
| 70 | +} = Test; |
| 71 | + |
| 72 | +const isTableOptionNotTrue: Test.Tester = (uischema) => |
| 73 | + !uischema.options?.table; |
| 74 | + |
| 75 | +export const spectrumArrayControlGridTester: RankedTester = rankWith( |
| 76 | + 3, |
| 77 | + or( |
| 78 | + and(isObjectArrayControl, isTableOptionNotTrue), |
| 79 | + and(isPrimitiveArrayControl, isTableOptionNotTrue) |
| 80 | + ) |
| 81 | +); |
| 82 | + |
| 83 | +const errorFontSize = 0.8; |
| 84 | +const errorStyle = { |
| 85 | + color: 'var(--spectrum-semantic-negative-color-default)', |
| 86 | + lineHeight: 1.3, |
| 87 | + fontSize: `${errorFontSize * 100}%`, |
| 88 | +}; |
| 89 | + |
| 90 | +// Calculate minimum row height so that it does not change no matter if a call has an error message or not |
| 91 | +const rowMinHeight = `calc(var(--spectrum-alias-font-size-default) * ${errorFontSize} * ${errorStyle.lineHeight} + var(--spectrum-alias-single-line-height))`; |
| 92 | + |
| 93 | +function SpectrumArrayControlGrid(props: ArrayControlProps) { |
| 94 | + const { |
| 95 | + addItem, |
| 96 | + uischema, |
| 97 | + schema, |
| 98 | + rootSchema, |
| 99 | + path, |
| 100 | + data, |
| 101 | + visible, |
| 102 | + label, |
| 103 | + childErrors, |
| 104 | + removeItems, |
| 105 | + } = props; |
| 106 | + |
| 107 | + const controlElement = uischema as ControlElement; |
| 108 | + const createControlElement = (key?: string): ControlElement => ({ |
| 109 | + type: 'Control', |
| 110 | + label: false, |
| 111 | + scope: schema.type === 'object' ? `#/properties/${key}` : '#', |
| 112 | + }); |
| 113 | + |
| 114 | + const labelObject = createLabelDescriptionFrom(controlElement, schema); |
| 115 | + |
| 116 | + const uioptions = getUIOptions(uischema, labelObject.text); |
| 117 | + const spacing: number[] = uischema.options?.spacing ?? []; |
| 118 | + const add = addItem(path, createDefaultValue(schema)); |
| 119 | + const fields = schema.properties ? Object.keys(schema.properties) : ['items']; |
| 120 | + |
| 121 | + return ( |
| 122 | + <View |
| 123 | + isHidden={visible === undefined || visible === null ? false : !visible} |
| 124 | + > |
| 125 | + <ArrayHeader |
| 126 | + {...uioptions} |
| 127 | + add={add} |
| 128 | + allErrorsMessages={childErrors.map((e) => e.message)} |
| 129 | + labelText={isPlainLabel(label) ? label : label.default} |
| 130 | + /> |
| 131 | + {data && Array.isArray(data) && data.length > 0 && ( |
| 132 | + <Grid |
| 133 | + columns={ |
| 134 | + spacing.length |
| 135 | + ? `${fields.map((_, i) => `${spacing[i] || 1}fr`).join(' ')} 0fr` |
| 136 | + : `repeat(${fields.length}, 1fr) 0fr` |
| 137 | + } |
| 138 | + rows='auto' |
| 139 | + autoRows={`minmax(${rowMinHeight}, auto)`} |
| 140 | + columnGap='size-100' |
| 141 | + > |
| 142 | + {fields |
| 143 | + .map((prop) => ( |
| 144 | + <View |
| 145 | + paddingBottom='size-50' |
| 146 | + justifySelf={ |
| 147 | + schema.properties?.[prop]?.type === 'boolean' |
| 148 | + ? 'center' |
| 149 | + : undefined |
| 150 | + } |
| 151 | + key={prop} |
| 152 | + > |
| 153 | + {startCase(prop)} |
| 154 | + </View> |
| 155 | + )) |
| 156 | + .concat(<View key='spacer' />)} |
| 157 | + {data.map((_, index) => { |
| 158 | + const childPath = Paths.compose(path, `${index}`); |
| 159 | + const rowCells: JSX.Element[] = schema.properties |
| 160 | + ? fields |
| 161 | + .filter((prop) => schema.properties[prop].type !== 'array') |
| 162 | + .map((prop) => { |
| 163 | + const childPropPath = Paths.compose( |
| 164 | + childPath, |
| 165 | + prop.toString() |
| 166 | + ); |
| 167 | + const isCheckbox = |
| 168 | + schema.properties[prop].type === 'boolean'; |
| 169 | + return ( |
| 170 | + <View |
| 171 | + key={childPropPath} |
| 172 | + paddingStart={isCheckbox ? 'size-200' : undefined} |
| 173 | + > |
| 174 | + <Flex |
| 175 | + direction='column' |
| 176 | + width='100%' |
| 177 | + alignItems={isCheckbox ? 'center' : 'start'} |
| 178 | + > |
| 179 | + <DispatchCell |
| 180 | + schema={Resolve.schema( |
| 181 | + schema, |
| 182 | + `#/properties/${prop}`, |
| 183 | + rootSchema |
| 184 | + )} |
| 185 | + uischema={ |
| 186 | + isCheckbox |
| 187 | + ? { |
| 188 | + ...createControlElement(prop), |
| 189 | + options: { trim: true }, |
| 190 | + } |
| 191 | + : createControlElement(prop) |
| 192 | + } |
| 193 | + path={childPath + '.' + prop} |
| 194 | + /> |
| 195 | + <View |
| 196 | + UNSAFE_style={errorStyle} |
| 197 | + isHidden={ |
| 198 | + getChildError(childErrors, childPropPath) === '' |
| 199 | + } |
| 200 | + > |
| 201 | + <Text> |
| 202 | + {getChildError(childErrors, childPropPath)} |
| 203 | + </Text> |
| 204 | + </View> |
| 205 | + </Flex> |
| 206 | + </View> |
| 207 | + ); |
| 208 | + }) |
| 209 | + : [ |
| 210 | + <View key={Paths.compose(childPath, index.toString())}> |
| 211 | + <Flex direction='column' width='100%'> |
| 212 | + <DispatchCell |
| 213 | + schema={schema} |
| 214 | + uischema={createControlElement()} |
| 215 | + path={childPath} |
| 216 | + /> |
| 217 | + <View |
| 218 | + UNSAFE_style={errorStyle} |
| 219 | + isHidden={getChildError(childErrors, childPath) === ''} |
| 220 | + > |
| 221 | + <Text>{getChildError(childErrors, childPath)}</Text> |
| 222 | + </View> |
| 223 | + </Flex> |
| 224 | + </View>, |
| 225 | + ]; |
| 226 | + return ( |
| 227 | + <React.Fragment key={index}> |
| 228 | + {rowCells} |
| 229 | + <DeleteButton |
| 230 | + index={index} |
| 231 | + path={childPath} |
| 232 | + removeItems={removeItems} |
| 233 | + /> |
| 234 | + </React.Fragment> |
| 235 | + ); |
| 236 | + })} |
| 237 | + </Grid> |
| 238 | + )} |
| 239 | + <ArrayFooter {...uioptions} add={add} /> |
| 240 | + </View> |
| 241 | + ); |
| 242 | +} |
| 243 | + |
| 244 | +function DeleteButton(props: { |
| 245 | + removeItems: ArrayControlProps['removeItems']; |
| 246 | + index: number; |
| 247 | + path: string; |
| 248 | +}) { |
| 249 | + const { removeItems, path, index } = props; |
| 250 | + const remove = React.useCallback(() => { |
| 251 | + const p = path.substring(0, path.lastIndexOf('.')); |
| 252 | + removeItems(p, [index])(); |
| 253 | + }, [removeItems, path, index]); |
| 254 | + |
| 255 | + return ( |
| 256 | + <View key={`delete-row-${index}`}> |
| 257 | + <DialogTrigger> |
| 258 | + <TooltipTrigger delay={0}> |
| 259 | + <ActionButton aria-label={`Delete row at ${index}`}> |
| 260 | + <Delete /> |
| 261 | + </ActionButton> |
| 262 | + <Tooltip>Delete</Tooltip> |
| 263 | + </TooltipTrigger> |
| 264 | + <AlertDialog |
| 265 | + variant='confirmation' |
| 266 | + title='Delete' |
| 267 | + primaryActionLabel='Delete' |
| 268 | + cancelLabel='Cancel' |
| 269 | + autoFocusButton='primary' |
| 270 | + onPrimaryAction={remove} |
| 271 | + > |
| 272 | + Are you sure you wish to delete this item? |
| 273 | + </AlertDialog> |
| 274 | + </DialogTrigger> |
| 275 | + </View> |
| 276 | + ); |
| 277 | +} |
| 278 | + |
| 279 | +export default withJsonFormsArrayControlProps(SpectrumArrayControlGrid); |
0 commit comments