From 9ffed80d80d9ac4fbff22fa47d464391c1c14233 Mon Sep 17 00:00:00 2001 From: Tushar Balar <62089106+tbalar-splunk@users.noreply.github.com> Date: Wed, 6 Sep 2023 20:09:09 +0530 Subject: [PATCH] feat: ADDON-61556 Introduced checkbox group component (#394) * feat: ADDON-61556 Introduced checkbox group component * feat: ADDON-61556 Developed checkbox groups component * feat: ADDON-61556 Validation part has updated * feat: ADDON-61556 Added select all and clear all buttons * feat: ADDON-61556 Revert unwanted code * feat: ADDON-61556 Fix minor css issue * feat: ADDON-61556 Fix issue * refactor: ADDON-61556 Addressed review comments --- .../main/webapp/components/BaseFormView.jsx | 356 ++++++++++++++---- .../components/CheckboxGroupsComponent.jsx | 71 ++++ .../main/webapp/components/ControlWrapper.jsx | 38 +- .../webapp/components/StyledComponent.jsx | 143 +++++++ .../main/webapp/constants/ControlTypeMap.js | 2 + ui/src/main/webapp/schema/schema.json | 130 ++++--- ui/src/main/webapp/util/util.js | 13 + 7 files changed, 587 insertions(+), 166 deletions(-) create mode 100644 ui/src/main/webapp/components/CheckboxGroupsComponent.jsx create mode 100644 ui/src/main/webapp/components/StyledComponent.jsx diff --git a/ui/src/main/webapp/components/BaseFormView.jsx b/ui/src/main/webapp/components/BaseFormView.jsx index cc944c322..27b2b9216 100644 --- a/ui/src/main/webapp/components/BaseFormView.jsx +++ b/ui/src/main/webapp/components/BaseFormView.jsx @@ -1,15 +1,13 @@ import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; +import Message from '@splunk/react-ui/Message'; import update from 'immutability-helper'; +import PropTypes from 'prop-types'; import { v4 as uuidv4 } from 'uuid'; - -import CollapsiblePanel from '@splunk/react-ui/CollapsiblePanel'; -import Message from '@splunk/react-ui/Message'; -import styled from 'styled-components'; +import Link from '@splunk/react-ui/Link'; import ControlWrapper from './ControlWrapper'; import Validator, { SaveValidator } from '../util/Validator'; -import { getUnifiedConfigs, generateToast } from '../util/util'; +import { getUnifiedConfigs, generateToast, isTrue, populateKeyValueDict } from '../util/util'; import { MODE_CLONE, MODE_CREATE, MODE_EDIT, MODE_CONFIG } from '../constants/modes'; import { PAGE_INPUT, PAGE_CONF } from '../constants/pages'; import { axiosCallWrapper } from '../util/axiosCallWrapper'; @@ -24,30 +22,18 @@ import { ERROR_STATE_MISSING_TRY_AGAIN, } from '../constants/oAuthErrorMessage'; import TableContext from '../context/TableContext'; - -const CollapsiblePanelWrapper = styled(CollapsiblePanel)` - span { - button { - background-color: #f2f4f5; - font-size: 16px; - margin: 15px 0; - - &:hover:not([disabled]), - &:focus:not([disabled]), - &:active:not([disabled]) { - background-color: #f2f4f5; - box-shadow: none; - } - } - } -`; - -const CustomGroupLabel = styled.div` - padding: 6px 10px; - background-color: #f2f4f5; - margin: 0 0 15px 0; - font-size: 16px; -`; +import { + CollapsiblePanelWrapper, + CustomGroupLabel, + CheckboxLabelContainer, + CheckboxGroupPanelWrapper, + CustomCheckboxGroupsLabel, + CheckboxGroupContainer, + CheckboxGroupsToggleButtonWrapper, + StyledPadding4, +} from './StyledComponent'; + +const CHECKBOX_GROUPS = 'checkboxGroups'; class BaseFormView extends PureComponent { static contextType = TableContext; @@ -61,6 +47,7 @@ class BaseFormView extends PureComponent { const globalConfig = getUnifiedConfigs(); this.appName = globalConfig.meta.name; this.groupEntities = []; + this.checkboxEntities = []; this.endpoint = props.mode === MODE_EDIT || props.mode === MODE_CONFIG ? `${this.props.serviceName}/${encodeURIComponent(this.props.stanzaName)}` @@ -87,7 +74,19 @@ class BaseFormView extends PureComponent { globalConfig.pages.inputs.services.forEach((service) => { if (service.name === props.serviceName) { this.groups = service.groups; + this.checkboxGroupsMetadata = service.CheckboxGroupsMetadata; + this.checkboxGroups = service.CheckboxGroupsMetadata?.groups; this.entities = service.entity; + + // Removing entity based validation + // Validation will be done using main validator from CheckboxGroupsMetadata + this.entities.forEach((e) => { + if (e.type === CHECKBOX_GROUPS) { + delete e.validators; + this.checkboxEntities.push(e); + } + }); + this.updateGroupEntities(); this.options = service.options; if (service.hook) { @@ -99,6 +98,20 @@ class BaseFormView extends PureComponent { } if (props.mode === MODE_EDIT || props.mode === MODE_CLONE) { this.currentInput = context.rowData[props.serviceName][props.stanzaName]; + + const checkboxGroupFieldValue = + this.currentInput[this.checkboxGroupsMetadata?.field]; + + // This conversion is performed while reading from the conf file. + // Next time (without refreshing the page), it is expected that the data will already be in dictionary format, + // and this conversion step will not be necessary. + if ( + this.checkboxGroupsMetadata?.field && + typeof checkboxGroupFieldValue === 'string' + ) { + this.currentInput[this.checkboxGroupsMetadata.field] = + populateKeyValueDict(checkboxGroupFieldValue); + } } } }); @@ -257,19 +270,40 @@ class BaseFormView extends PureComponent { e.encrypted = typeof e.encrypted !== 'undefined' ? e.encrypted : false; if (props.mode === MODE_CREATE) { - tempEntity.value = - typeof e.defaultValue !== 'undefined' ? e.defaultValue : null; + if (e.type === CHECKBOX_GROUPS) { + // - Set the checkboxTextFieldValue to the provided defaultValue if defined; otherwise, set to null. + // This value is used to populate the associated Text field. + // - Determine the value for the Checkbox field's enable/disable state: + // If the 'enable' option is explicitly defined in the options object, use that value; + // otherwise, default to enabling the Checkbox field (true). + tempEntity.checkboxTextFieldValue = + typeof e.defaultValue !== 'undefined' ? e.defaultValue : null; + tempEntity.value = + typeof e?.options?.enable !== 'undefined' ? e.options.enable : true; + } else { + tempEntity.value = + typeof e.defaultValue !== 'undefined' ? e.defaultValue : null; + } tempEntity.display = typeof e?.options?.display !== 'undefined' ? e.options.display : true; tempEntity.error = false; tempEntity.disabled = false; temState[e.field] = tempEntity; } else if (props.mode === MODE_EDIT) { - tempEntity.value = - typeof this.currentInput[e.field] !== 'undefined' - ? this.currentInput[e.field] - : null; - tempEntity.value = e.encrypted ? '' : tempEntity.value; + if (e.type === CHECKBOX_GROUPS) { + const checkboxTextFieldValue = + this.currentInput[this.checkboxGroupsMetadata?.field]?.[e.field]; + tempEntity.value = typeof checkboxTextFieldValue !== 'undefined'; + tempEntity.checkboxTextFieldValue = tempEntity.value + ? checkboxTextFieldValue + : e.defaultValue; + } else { + tempEntity.value = + typeof this.currentInput[e.field] !== 'undefined' + ? this.currentInput[e.field] + : null; + tempEntity.value = e.encrypted ? '' : tempEntity.value; + } tempEntity.display = typeof e?.options?.display !== 'undefined' ? e.options.display : true; tempEntity.error = false; @@ -281,8 +315,17 @@ class BaseFormView extends PureComponent { } temState[e.field] = tempEntity; } else if (props.mode === MODE_CLONE) { - tempEntity.value = - e.field === 'name' || e.encrypted ? '' : this.currentInput[e.field]; + if (e.type === CHECKBOX_GROUPS) { + const checkboxTextFieldValue = + this.currentInput[this.checkboxGroupsMetadata?.field]?.[e.field]; + tempEntity.value = typeof checkboxTextFieldValue !== 'undefined'; + tempEntity.checkboxTextFieldValue = tempEntity.value + ? checkboxTextFieldValue + : e.defaultValue; + } else { + tempEntity.value = + e.field === 'name' || e.encrypted ? '' : this.currentInput[e.field]; + } tempEntity.display = typeof e?.options?.display !== 'undefined' ? e.options.display : true; tempEntity.error = false; @@ -399,9 +442,41 @@ class BaseFormView extends PureComponent { this.datadict = {}; - Object.keys(this.state.data).forEach((field) => { - this.datadict[field] = this.state.data[field].value; - }); + const updateDataDict = () => { + // If checkboxGroupsMetadata is defined, set an empty value in the datadict + // using the field name specified in checkboxGroupsMetadata. + // This key is used to store the selected values for the checkbox group. + if (this.checkboxGroupsMetadata) { + this.datadict[this.checkboxGroupsMetadata.field] = ''; + } + + // Iterate through each field in the state's data object + Object.keys(this.state.data).forEach((field) => { + const fieldData = this.state.data[field]; + + // Check if the field contains 'checkboxTextFieldValue' key, indicating a checkboxGroups component + if (fieldData.checkboxTextFieldValue) { + // For selected checkboxGroups components, append the field-value pair to the datadict + if (fieldData.value) { + this.datadict[ + this.checkboxGroupsMetadata?.field + ] += `${field}/${fieldData.checkboxTextFieldValue},`; + } + } else { + // For non-checkboxGroups components, update datadict with the field's value + this.datadict[field] = fieldData.value; + } + }); + + // If there are checkboxGroups selections in datadict, remove trailing comma + if (this.datadict[this.checkboxGroupsMetadata?.field]) { + this.datadict[this.checkboxGroupsMetadata.field] = this.datadict[ + this.checkboxGroupsMetadata.field + ].slice(0, -1); + } + }; + + updateDataDict(); if (this.hook && typeof this.hook.onSave === 'function') { const validationPass = this.hook.onSave(this.datadict); @@ -410,10 +485,9 @@ class BaseFormView extends PureComponent { return; } } + const executeValidationSubmit = () => { - Object.keys(this.state.data).forEach((field) => { - this.datadict[field] = this.state.data[field].value; - }); + updateDataDict(); // validation for unique name if ([MODE_CREATE, MODE_CLONE].includes(this.props.mode)) { @@ -453,13 +527,33 @@ class BaseFormView extends PureComponent { }); } else { temEntities = this.entities; + + if (this.checkboxGroupsMetadata?.validators) { + const checkboxGroupField = { + type: 'text', + field: this.checkboxGroupsMetadata.field, + label: this.checkboxGroupsMetadata.label, + validators: this.checkboxGroupsMetadata.validators, + }; + temEntities.push(checkboxGroupField); + } } // Validation of form fields on Submit const validator = new Validator(temEntities); let error = validator.doValidation(this.datadict); + + // - If the error pertains to a checkboxGroups component, display the error message at the top of the form. + // - For other validation errors, display the error message and highlight the corresponding field in the form. + // If no validation error occurs, and a saveValidator is specified in the options: + // - Apply the saveValidator to validate the entire dataset before saving. + // - If an error occurs, display the associated error message. if (error) { - this.setErrorFieldMsg(error.errorField, error.errorMsg); + if (error.errorField === this.checkboxGroupsMetadata?.field) { + this.setErrorMsg(error.errorMsg); + } else { + this.setErrorFieldMsg(error.errorField, error.errorMsg); + } } else if (this.options && this.options.saveValidator) { error = SaveValidator(this.options.saveValidator, this.datadict); if (error) { @@ -669,7 +763,7 @@ class BaseFormView extends PureComponent { }); }; - handleChange = (field, targetValue) => { + handleChange = (field, targetValue, componentType = null) => { const changes = {}; if (field === 'auth_type') { Object.keys(this.authMap).forEach((type) => { @@ -713,7 +807,14 @@ class BaseFormView extends PureComponent { }); } - changes[field] = { value: { $set: targetValue } }; + // If the component type is a checkboxGroups component, update the 'checkboxTextFieldValue' field + // to reflect the user's typed input, ensuring real-time synchronization with the textbox content. + // For other component types, update the 'value' field with the new target value. + if (componentType === CHECKBOX_GROUPS) { + changes[field] = { checkboxTextFieldValue: { $set: targetValue } }; + } else { + changes[field] = { value: { $set: targetValue } }; + } const newFields = update(this.state, { data: changes }); const tempState = this.clearAllErrorMsg(newFields); @@ -728,6 +829,22 @@ class BaseFormView extends PureComponent { } }; + handleCheckboxToggleAll = (selectAll) => { + const allFields = []; + const changes = {}; + + this.checkboxEntities.forEach((item) => { + allFields.push(item.field); + }); + + allFields.forEach((field) => { + changes[field] = { value: { $set: selectAll } }; + }); + + const newFields = update(this.state, { data: changes }); + this.setState(newFields); + }; + addCustomValidator = (field, validatorFunc) => { const index = this.entities.findIndex((x) => x.field === field); const validator = [{ type: 'custom', validatorFunc }]; @@ -969,54 +1086,139 @@ class BaseFormView extends PureComponent { // eslint-disable-next-line class-methods-use-this timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // eslint-disable-line no-promise-executor-return - renderGroupElements = () => { + getCheckedCheckboxCount = (fields) => { + let count = 0; + const entitiesList = this.entities.filter((e) => fields.includes(e.field)); + entitiesList.forEach((e) => { + const temState = this.state.data[e.field]; + if (isTrue(temState.value)) { + count += 1; + } + }); + return count; + }; + + renderCheckboxGroupElements = (group, collpsibleElement) => { + const checkboxGroupTitle = ( + + {group.label} + + {this.getCheckedCheckboxCount(group.fields)} of {group.fields?.length} + + + ); + + return ( + + {collpsibleElement} + + ); + }; + + getControls = (e) => { + const temState = this.state.data[e.field]; + return ( + + ); + }; + + renderGroupElements = (isGroupTypeCheckbox) => { let el = null; - if (this.groups && this.groups.length) { - el = this.groups.map((group) => { + const groups = isGroupTypeCheckbox ? this.checkboxGroups : this.groups; + + if (groups && groups.length) { + el = groups.map((group) => { const collpsibleElement = group.fields?.length && group.fields.map((fieldName) => this.entities.map((e) => { if (e.field === fieldName) { - const temState = this.state.data[e.field]; - return ( - - ); + return this.getControls(e); } return null; }) ); - return group.options?.isExpandable ? ( + if (!group.options?.isExpandable) { + return ( + <> + {group.label} +
{collpsibleElement}
+ + ); + } + + if (isGroupTypeCheckbox) { + return ( + + {this.renderCheckboxGroupElements(group, collpsibleElement)} + + ); + } + + return ( -
{collpsibleElement}
+ {collpsibleElement}
- ) : ( - <> - {group.label} -
{collpsibleElement}
- ); }); } return el; }; + renderNonGroupCheckboxEntities = () => this.checkboxEntities.map((e) => this.getControls(e)); + + renderGroups = () => ( + <> + + {this.checkboxGroupsMetadata && ( + + {this.checkboxGroupsMetadata.label} + + )} + {this.checkboxGroups + ? this.renderGroupElements(true) + : this.renderNonGroupCheckboxEntities()} + {this.checkboxGroupsMetadata && ( + + this.handleCheckboxToggleAll(true)}> + Select All + + this.handleCheckboxToggleAll(0)} + > + Clear All + + + )} + + {this.renderGroupElements(false)} + + ); + render() { // onRender method of Hook if (this.flag) { @@ -1056,11 +1258,13 @@ class BaseFormView extends PureComponent { {this.generateErrorMessage()} {this.entities.map((e) => { // Return null if we need to show element in a group - if (this.groupEntities.includes(e.field)) { + if (e.type === CHECKBOX_GROUPS || this.groupEntities.includes(e.field)) { return null; } const temState = this.state.data[e.field]; + if (!temState) return null; + if (temState.placeholder) { // eslint-disable-next-line no-param-reassign e = { @@ -1099,7 +1303,7 @@ class BaseFormView extends PureComponent { /> ); })} - {this.renderGroupElements()} + {this.renderGroups()} ); diff --git a/ui/src/main/webapp/components/CheckboxGroupsComponent.jsx b/ui/src/main/webapp/components/CheckboxGroupsComponent.jsx new file mode 100644 index 000000000..9fb2dca7c --- /dev/null +++ b/ui/src/main/webapp/components/CheckboxGroupsComponent.jsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import ColumnLayout from '@splunk/react-ui/ColumnLayout'; +import Text from '@splunk/react-ui/Text'; +import { isFalse, isTrue } from '../util/util'; +import { StyledColumnLayout, StyledSwitch } from './StyledComponent'; + +function CheckboxGroupsComponent(props) { + const { field, label, value, checkboxTextFieldValue, handleChange } = props; + + const [isDisabled, setIsDisabled] = useState(!value); + + useEffect(() => { + setIsDisabled(!value); + }, [value]); + + const handleChangeCheckbox = () => { + if (value && isTrue(value)) { + handleChange(field, 0); + setIsDisabled(true); + } else { + handleChange(field, 1); + setIsDisabled(false); + } + }; + + const handleChangeTextBox = (event) => { + handleChange(field, event.target.value, 'checkboxGroups'); + }; + + return ( + + + + + {label} + + + + + + + + ); +} + +CheckboxGroupsComponent.propTypes = { + value: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + checkboxTextFieldValue: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.number, + PropTypes.string, + ]), + handleChange: PropTypes.func.isRequired, + label: PropTypes.string, + field: PropTypes.string, +}; + +export default CheckboxGroupsComponent; diff --git a/ui/src/main/webapp/components/ControlWrapper.jsx b/ui/src/main/webapp/components/ControlWrapper.jsx index ae58a20d6..8b2b3f5ce 100644 --- a/ui/src/main/webapp/components/ControlWrapper.jsx +++ b/ui/src/main/webapp/components/ControlWrapper.jsx @@ -1,35 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ControlGroup from '@splunk/react-ui/ControlGroup'; -import styled from 'styled-components'; import MarkdownMessage from './MarkdownMessage'; import CONTROL_TYPE_MAP from '../constants/ControlTypeMap'; +import { CustomElement, CheckboxElement, ControlGroupWrapper } from './StyledComponent'; -const CustomElement = styled.div` - margin-left: 30px; -`; - -const ControlGroupWrapper = styled(ControlGroup).attrs((props) => ({ - 'data-name': props.dataName, -}))` - width: 100%; - max-width: 100%; - - > * { - &:first-child { - width: 240px !important; - } - &:nth-child(3) { - margin-left: 270px !important; - width: 320px; - } - } - - span[class*='ControlGroupStyles__StyledAsterisk-'] { - color: red; - } -`; +const CHECKBOX_GROUPS = 'checkboxGroups'; class ControlWrapper extends React.PureComponent { static isString = (str) => !!(typeof str === 'string' || str instanceof String); @@ -78,7 +54,9 @@ class ControlWrapper extends React.PureComponent { rowView = this.controlType ? React.createElement(this.controlType, { handleChange, + label: this.props.entity.label, value: this.props.value, + checkboxTextFieldValue: this.props.checkboxTextFieldValue, field, controlOptions: this.props.entity.options, error: this.props.error, @@ -105,7 +83,10 @@ class ControlWrapper extends React.PureComponent { ); return ( - this.props.display && ( + this.props.display && + (this.props.entity.type === CHECKBOX_GROUPS ? ( + {rowView} + ) : ( {rowView} - ) + )) ); } } @@ -125,6 +106,7 @@ ControlWrapper.propTypes = { mode: PropTypes.string, utilityFuncts: PropTypes.object, value: PropTypes.any, + checkboxTextFieldValue: PropTypes.any, display: PropTypes.bool, error: PropTypes.bool, entity: PropTypes.object, diff --git a/ui/src/main/webapp/components/StyledComponent.jsx b/ui/src/main/webapp/components/StyledComponent.jsx new file mode 100644 index 000000000..b884c6459 --- /dev/null +++ b/ui/src/main/webapp/components/StyledComponent.jsx @@ -0,0 +1,143 @@ +import styled from 'styled-components'; +import CollapsiblePanel from '@splunk/react-ui/CollapsiblePanel'; +import ColumnLayout from '@splunk/react-ui/ColumnLayout'; +import Switch from '@splunk/react-ui/Switch'; +import ControlGroup from '@splunk/react-ui/ControlGroup'; + +const CollapsiblePanelWrapper = styled(CollapsiblePanel)` + span { + button { + background-color: #f2f4f5; + font-size: 16px; + margin: 15px 0; + + &:hover:not([disabled]), + &:focus:not([disabled]), + &:active:not([disabled]) { + background-color: #f2f4f5; + box-shadow: none; + } + } + } +`; + +const CustomGroupLabel = styled.div` + padding: 6px 10px; + background-color: #f2f4f5; + margin: 0 0 15px 0; + font-size: 16px; +`; + +const CheckboxGroupContainer = styled.div` + position: relative; +`; + +const CheckboxLabelContainer = styled.div` + display: flex; + width: 280px; + justify-content: space-between; +`; + +const CheckboxGroupPanelWrapper = styled(CollapsiblePanel)` + span { + button { + background-color: #f2f4f5; + margin: 1px 0 1px 270px; + width: 320px; + + &:hover:not([disabled]), + &:focus:not([disabled]), + &:active:not([disabled]) { + background-color: #f2f4f5; + box-shadow: none; + } + } + button > span:nth-child(2) { + width: 280px; + } + } +`; + +const CustomCheckboxGroupsLabel = styled.div` + display: inline-flex; + position: absolute; + width: 240px !important; + padding: 6px 0; + justify-content: flex-end; +`; + +const StyledColumnLayout = styled(ColumnLayout)` + width: 320px !important; +`; + +const StyledSwitch = styled(Switch)` + padding-left: 10px !important; + > * { + &:nth-child(2) { + margin-left: 8px; + } + } + + span { + color: red; + font-weight: bold; + margin-left: 5px; + } +`; + +const CustomElement = styled.div` + margin-left: 30px; +`; + +const CheckboxElement = styled.div` + margin-left: 270px; + margin-top: 2px; +`; + +const ControlGroupWrapper = styled(ControlGroup).attrs((props) => ({ + 'data-name': props.dataName, +}))` + width: 100%; + max-width: 100%; + + > * { + &:first-child { + width: 240px !important; + } + &:nth-child(3) { + margin-left: 270px !important; + width: 320px; + } + } + + span[class*='ControlGroupStyles__StyledAsterisk-'] { + color: red; + } +`; + +const CheckboxGroupsToggleButtonWrapper = styled.div` + position: relative; + margin-left: 270px; + margin-top: 10px; +`; + +const StyledPadding4 = styled.div` + padding-top: 4px; + padding-bottom: 4px; +`; + +export { + CollapsiblePanelWrapper, + CustomGroupLabel, + CheckboxLabelContainer, + CheckboxGroupPanelWrapper, + CustomCheckboxGroupsLabel, + StyledColumnLayout, + StyledSwitch, + CheckboxGroupContainer, + CustomElement, + CheckboxElement, + ControlGroupWrapper, + CheckboxGroupsToggleButtonWrapper, + StyledPadding4, +}; diff --git a/ui/src/main/webapp/constants/ControlTypeMap.js b/ui/src/main/webapp/constants/ControlTypeMap.js index eaa8b9225..531a3ac16 100644 --- a/ui/src/main/webapp/constants/ControlTypeMap.js +++ b/ui/src/main/webapp/constants/ControlTypeMap.js @@ -8,6 +8,7 @@ import RadioComponent from '../components/RadioComponent'; import PlaceholderComponent from '../components/PlaceholderComponent'; import CustomControl from '../components/CustomControl'; import FileInputComponent from '../components/FileInputComponent'; +import CheckboxGroupsComponent from '../components/CheckboxGroupsComponent'; export default { text: TextComponent, @@ -19,5 +20,6 @@ export default { radio: RadioComponent, file: FileInputComponent, placeholder: PlaceholderComponent, + checkboxGroups: CheckboxGroupsComponent, custom: CustomControl, }; diff --git a/ui/src/main/webapp/schema/schema.json b/ui/src/main/webapp/schema/schema.json index 9962fdd44..6e3f1d505 100644 --- a/ui/src/main/webapp/schema/schema.json +++ b/ui/src/main/webapp/schema/schema.json @@ -158,7 +158,8 @@ "placeholder", "oauth", "helpLink", - "file" + "file", + "checkboxGroups" ] }, "help": { @@ -513,6 +514,62 @@ }, "additionalProperties": false }, + "Groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "options": { + "type": "object", + "properties": { + "isExpandable": { + "type": "boolean" + }, + "expand": { + "type": "boolean" + } + } + }, + "label": { + "type": "string", + "maxLength": 100 + }, + "fields": { + "type": "array", + "items": { + "type": "string", + "pattern": "^\\w+$" + } + } + }, + "required": ["label", "fields"], + "additionalProperties": false + } + }, + "CheckboxGroupsMetadata": { + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 60 + }, + "field": { + "type": "string", + "maxLength": 60 + }, + "validators": { + "type": "array", + "items": { + "$ref": "#/definitions/RegexValidator" + } + }, + "groups": { + "$ref": "#/definitions/Groups" + } + }, + "required": ["label", "field"], + "additionalProperties": false + }, "InputsEntity": { "type": "object", "properties": { @@ -537,7 +594,8 @@ "placeholder", "oauth", "helpLink", - "file" + "file", + "checkboxGroups" ] }, "help": { @@ -828,36 +886,10 @@ "$ref": "#/definitions/Hooks" }, "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "options": { - "type": "object", - "properties": { - "isExpandable": { - "type": "boolean" - }, - "expand": { - "type": "boolean" - } - } - }, - "label": { - "type": "string", - "maxLength": 100 - }, - "fields": { - "type": "array", - "items": { - "type": "string", - "pattern": "^\\w+$" - } - } - }, - "required": ["label", "fields"], - "additionalProperties": false - } + "$ref": "#/definitions/Groups" + }, + "CheckboxGroupsMetadata": { + "$ref": "#/definitions/CheckboxGroupsMetadata" }, "style": { "type": "string", @@ -935,36 +967,10 @@ "$ref": "#/definitions/Hooks" }, "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "options": { - "type": "object", - "properties": { - "isExpandable": { - "type": "boolean" - }, - "expand": { - "type": "boolean" - } - } - }, - "label": { - "type": "string", - "maxLength": 100 - }, - "fields": { - "type": "array", - "items": { - "type": "string", - "pattern": "^\\w+$" - } - } - }, - "required": ["label", "fields"], - "additionalProperties": false - } + "$ref": "#/definitions/Groups" + }, + "CheckboxGroupsMetadata": { + "$ref": "#/definitions/CheckboxGroupsMetadata" }, "style": { "type": "string", diff --git a/ui/src/main/webapp/util/util.js b/ui/src/main/webapp/util/util.js index 00eb36187..e781ba11e 100644 --- a/ui/src/main/webapp/util/util.js +++ b/ui/src/main/webapp/util/util.js @@ -86,3 +86,16 @@ export function filterResponse(items, labelField, allowList, denyList) { return newItems; } + +// Convert a comma-separated string of key-value pairs into a dictionary. +export function populateKeyValueDict(items) { + const keyValuePairs = items.split(',').map((item) => item.trim()); + + const resultDict = {}; + + keyValuePairs.forEach((pair) => { + const [key, value] = pair.split('/'); + resultDict[key] = value; + }); + return resultDict; +}