mirror of
https://github.com/harness/drone.git
synced 2025-05-04 13:40:38 +08:00
Added Multimap (#633)
This commit is contained in:
parent
560fb961bf
commit
c5e5d33e4d
@ -39,7 +39,7 @@ interface MultiListProps {
|
|||||||
- <field-value-1>,
|
- <field-value-1>,
|
||||||
- <field-value-2>,
|
- <field-value-2>,
|
||||||
...
|
...
|
||||||
*/
|
*/
|
||||||
export const MultiList = ({ name, label, readOnly, formik }: MultiListConnectedProps): JSX.Element => {
|
export const MultiList = ({ name, label, readOnly, formik }: MultiListConnectedProps): JSX.Element => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const [valueMap, setValueMap] = useState<Map<string, string>>(new Map<string, string>([]))
|
const [valueMap, setValueMap] = useState<Map<string, string>>(new Map<string, string>([]))
|
||||||
@ -51,7 +51,7 @@ export const MultiList = ({ name, label, readOnly, formik }: MultiListConnectedP
|
|||||||
const counter = useRef<number>(0)
|
const counter = useRef<number>(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const values = Array.from(valueMap.values())
|
const values = Array.from(valueMap.values() || []).filter((value: string) => !!value)
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
formik?.setFieldValue(name, values)
|
formik?.setFieldValue(name, values)
|
||||||
} else {
|
} else {
|
||||||
|
31
web/src/components/MultiMap/MultiMap.module.scss
Normal file
31
web/src/components/MultiMap/MultiMap.module.scss
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.addBtn {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteRowBtn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rowError {
|
||||||
|
:global {
|
||||||
|
.bp3-form-group {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
web/src/components/MultiMap/MultiMap.module.scss.d.ts
vendored
Normal file
21
web/src/components/MultiMap/MultiMap.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
// This is an auto-generated file
|
||||||
|
export declare const addBtn: string
|
||||||
|
export declare const deleteRowBtn: string
|
||||||
|
export declare const rowError: string
|
292
web/src/components/MultiMap/MultiMap.tsx
Normal file
292
web/src/components/MultiMap/MultiMap.tsx
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import { debounce, has, omit, set } from 'lodash'
|
||||||
|
import { FormikContextType, connect } from 'formik'
|
||||||
|
import { Layout, Text, FormInput, Button, ButtonVariation, ButtonSize, Container } from '@harnessio/uicore'
|
||||||
|
import { Color, FontVariation } from '@harnessio/design-system'
|
||||||
|
import { Icon } from '@harnessio/icons'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
|
|
||||||
|
import css from './MultiMap.module.scss'
|
||||||
|
|
||||||
|
interface MultiMapConnectedProps extends MultiMapProps {
|
||||||
|
formik?: FormikContextType<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MultiMapProps {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
readOnly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allows user to create following structure:
|
||||||
|
<field-name>:
|
||||||
|
<field-name-1> : <field-value-1>,
|
||||||
|
<field-name-2> : <field-value-2>,
|
||||||
|
...
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface KVPair {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultKVPair: KVPair = {
|
||||||
|
key: '',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KVPairProperty {
|
||||||
|
KEY = 'key',
|
||||||
|
VALUE = 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiMap = ({ name, label, readOnly, formik }: MultiMapConnectedProps): JSX.Element => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const [rowValues, setRowValues] = useState<Map<string, KVPair>>(new Map<string, KVPair>([]))
|
||||||
|
const [formErrors, setFormErrors] = useState<Map<string, string>>(new Map<string, string>([]))
|
||||||
|
/*
|
||||||
|
<field-name-1>: {key: <field-name-1-key>, value: <field-name-1-value>},
|
||||||
|
<field-name-2>: {key: <field-name-2-key>, value: <field-name-2-value>},
|
||||||
|
...
|
||||||
|
*/
|
||||||
|
const counter = useRef<number>(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const values = Array.from(rowValues.values()).filter((value: KVPair) => !!value.key && !!value.value)
|
||||||
|
if (values.length > 0) {
|
||||||
|
formik?.setFieldValue(name, createKVMap(values))
|
||||||
|
} else {
|
||||||
|
cleanupField()
|
||||||
|
}
|
||||||
|
}, [rowValues])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rowValues.forEach((value: KVPair, rowIdentifier: string) => {
|
||||||
|
validateEntry({ rowIdentifier, kvPair: value })
|
||||||
|
})
|
||||||
|
}, [rowValues])
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert
|
||||||
|
[
|
||||||
|
{key: <field-name-1-key>, value: <field-name-1-value>},
|
||||||
|
{key: <field-name-2-key>, value: <field-name-2-value>}
|
||||||
|
]
|
||||||
|
to
|
||||||
|
{
|
||||||
|
<field-name-1-key>: <field-name-1-value>,
|
||||||
|
<field-name-2-key>: <field-name-2-value>
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const createKVMap = useCallback((values: KVPair[]): { [key: string]: string } => {
|
||||||
|
const map: { [key: string]: string } = values.reduce(function (map, obj: KVPair) {
|
||||||
|
set(map, obj.key, obj.value)
|
||||||
|
return map
|
||||||
|
}, {})
|
||||||
|
return map
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const cleanupField = useCallback((): void => {
|
||||||
|
formik?.setValues(omit({ ...formik?.values }, name))
|
||||||
|
}, [formik?.values])
|
||||||
|
|
||||||
|
const getFieldName = useCallback(
|
||||||
|
(index: number): string => {
|
||||||
|
return `${name}-${index}`
|
||||||
|
},
|
||||||
|
[name]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFormikNameForRowKey = useCallback((rowIdentifier: string): string => {
|
||||||
|
return `${rowIdentifier}-key`
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleAddRowToList = useCallback((): void => {
|
||||||
|
setRowValues((existingValueMap: Map<string, KVPair>) => {
|
||||||
|
const rowKeyToAdd = getFieldName(counter.current)
|
||||||
|
if (!existingValueMap.has(rowKeyToAdd)) {
|
||||||
|
const existingValueMapClone = new Map(existingValueMap)
|
||||||
|
/* Add key with default kv pair
|
||||||
|
<field-name-1> : {key: '', value: ''},
|
||||||
|
<field-name-2> : {key: '', value: ''},
|
||||||
|
...
|
||||||
|
*/
|
||||||
|
existingValueMapClone.set(rowKeyToAdd, DefaultKVPair)
|
||||||
|
counter.current++ /* this counter always increases, even if a row is removed. This ensures no key collision in the existing value map. */
|
||||||
|
return existingValueMapClone
|
||||||
|
}
|
||||||
|
return existingValueMap
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleRemoveRowFromList = useCallback((removedRowKey: string): void => {
|
||||||
|
setRowValues((existingValueMap: Map<string, KVPair>) => {
|
||||||
|
if (existingValueMap.has(removedRowKey)) {
|
||||||
|
const existingValueMapClone = new Map(existingValueMap)
|
||||||
|
existingValueMapClone.delete(removedRowKey)
|
||||||
|
return existingValueMapClone
|
||||||
|
}
|
||||||
|
return existingValueMap
|
||||||
|
})
|
||||||
|
/* remove <field-name-1>, <field-name-2>, ... from formik values, if exist */
|
||||||
|
if (removedRowKey && has(formik?.values, removedRowKey)) {
|
||||||
|
formik?.setValues(omit({ ...formik?.values }, removedRowKey))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const validateEntry = useCallback(({ rowIdentifier, kvPair }: { rowIdentifier: string; kvPair: KVPair }) => {
|
||||||
|
setFormErrors((existingFormErrors: Map<string, string>) => {
|
||||||
|
const fieldNameKey = getFormikNameForRowKey(rowIdentifier)
|
||||||
|
const existingFormErrorsClone = new Map(existingFormErrors)
|
||||||
|
if (kvPair.value && !kvPair.key) {
|
||||||
|
existingFormErrorsClone.set(fieldNameKey, kvPair.key ? '' : getString('validation.key'))
|
||||||
|
} else {
|
||||||
|
existingFormErrorsClone.set(fieldNameKey, '')
|
||||||
|
}
|
||||||
|
return existingFormErrorsClone
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleAddItemToRow = useCallback(
|
||||||
|
({
|
||||||
|
rowIdentifier,
|
||||||
|
insertedValue,
|
||||||
|
property
|
||||||
|
}: {
|
||||||
|
rowIdentifier: string
|
||||||
|
insertedValue: string
|
||||||
|
property: KVPairProperty
|
||||||
|
}): void => {
|
||||||
|
setRowValues((existingValueMap: Map<string, KVPair>) => {
|
||||||
|
if (existingValueMap.has(rowIdentifier)) {
|
||||||
|
const existingValueMapClone = new Map(existingValueMap)
|
||||||
|
const existingPair = existingValueMapClone.get(rowIdentifier)
|
||||||
|
if (existingPair) {
|
||||||
|
if (property === KVPairProperty.KEY) {
|
||||||
|
existingValueMapClone.set(rowIdentifier, { key: insertedValue, value: existingPair.value })
|
||||||
|
} else if (property === KVPairProperty.VALUE) {
|
||||||
|
existingValueMapClone.set(rowIdentifier, { key: existingPair.key, value: insertedValue })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return existingValueMapClone
|
||||||
|
}
|
||||||
|
return existingValueMap
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const debouncedAddItemToList = useCallback(debounce(handleAddItemToRow, 500), [handleAddItemToRow])
|
||||||
|
|
||||||
|
const renderRow = useCallback(
|
||||||
|
(rowIdentifier: string): React.ReactElement => {
|
||||||
|
const rowValidationError = formErrors.get(getFormikNameForRowKey(rowIdentifier))
|
||||||
|
return (
|
||||||
|
<Layout.Vertical spacing="xsmall">
|
||||||
|
<Layout.Horizontal
|
||||||
|
margin={{ bottom: 'none' }}
|
||||||
|
flex={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Layout.Horizontal width="90%" flex={{ justifyContent: 'flex-start' }} spacing="medium">
|
||||||
|
<Container width="50%" className={cx({ [css.rowError]: rowValidationError })}>
|
||||||
|
<FormInput.Text
|
||||||
|
name={getFormikNameForRowKey(rowIdentifier)}
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={event => {
|
||||||
|
const value = (event.target as HTMLInputElement).value
|
||||||
|
debouncedAddItemToList({ rowIdentifier, insertedValue: value, property: KVPairProperty.KEY })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
<Container width="50%" className={cx({ [css.rowError]: rowValidationError })}>
|
||||||
|
<FormInput.Text
|
||||||
|
name={`${rowIdentifier}-value`}
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={event => {
|
||||||
|
const value = (event.target as HTMLInputElement).value
|
||||||
|
debouncedAddItemToList({ rowIdentifier, insertedValue: value, property: KVPairProperty.VALUE })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
<Icon
|
||||||
|
name="code-delete"
|
||||||
|
size={25}
|
||||||
|
padding={rowValidationError ? {} : { bottom: 'medium' }}
|
||||||
|
className={css.deleteRowBtn}
|
||||||
|
onClick={event => {
|
||||||
|
event.preventDefault()
|
||||||
|
handleRemoveRowFromList(rowIdentifier)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
{rowValidationError && (
|
||||||
|
<Text font={{ variation: FontVariation.SMALL }} color={Color.RED_500}>
|
||||||
|
{rowValidationError}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Layout.Vertical>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[formErrors]
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderMap = useCallback((): React.ReactElement => {
|
||||||
|
return (
|
||||||
|
<Layout.Vertical width="100%">
|
||||||
|
<Layout.Horizontal width="90%" flex={{ justifyContent: 'flex-start' }} spacing="medium">
|
||||||
|
<Text width="50%" font={{ variation: FontVariation.SMALL }}>
|
||||||
|
{getString('key')}
|
||||||
|
</Text>
|
||||||
|
<Text width="50%" font={{ variation: FontVariation.SMALL }}>
|
||||||
|
{getString('value')}
|
||||||
|
</Text>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
<Container padding={{ top: 'small' }}>{renderRows()}</Container>
|
||||||
|
</Layout.Vertical>
|
||||||
|
)
|
||||||
|
}, [rowValues, formErrors])
|
||||||
|
|
||||||
|
const renderRows = useCallback((): React.ReactElement => {
|
||||||
|
const rows: React.ReactElement[] = []
|
||||||
|
rowValues.forEach((_value: KVPair, key: string) => {
|
||||||
|
rows.push(renderRow(key))
|
||||||
|
})
|
||||||
|
return <Layout.Vertical>{rows}</Layout.Vertical>
|
||||||
|
}, [rowValues, formErrors])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Vertical spacing="small">
|
||||||
|
<Layout.Vertical>
|
||||||
|
<Text font={{ variation: FontVariation.FORM_LABEL }}>{label}</Text>
|
||||||
|
{rowValues.size > 0 && <Container padding={{ top: 'small' }}>{renderMap()}</Container>}
|
||||||
|
</Layout.Vertical>
|
||||||
|
<Button
|
||||||
|
text={getString('addLabel')}
|
||||||
|
rightIcon="plus"
|
||||||
|
variation={ButtonVariation.PRIMARY}
|
||||||
|
size={ButtonSize.SMALL}
|
||||||
|
className={css.addBtn}
|
||||||
|
onClick={handleAddRowToList}
|
||||||
|
/>
|
||||||
|
</Layout.Vertical>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(MultiMap)
|
@ -37,6 +37,7 @@ import {
|
|||||||
import type { TypesPlugin } from 'services/code'
|
import type { TypesPlugin } from 'services/code'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { MultiList } from 'components/MultiList/MultiList'
|
import { MultiList } from 'components/MultiList/MultiList'
|
||||||
|
import MultiMap from 'components/MultiMap/MultiMap'
|
||||||
|
|
||||||
import css from './PluginsPanel.module.scss'
|
import css from './PluginsPanel.module.scss'
|
||||||
|
|
||||||
@ -360,6 +361,16 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
|
|||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
case ValueType.OBJECT:
|
||||||
|
return (
|
||||||
|
<Container margin={{ bottom: 'large' }}>
|
||||||
|
<MultiMap
|
||||||
|
name={name}
|
||||||
|
label={generateLabelForPluginField({ name, properties }) as string}
|
||||||
|
formik={formikRef.current}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <></>
|
return <></>
|
||||||
@ -468,8 +479,7 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
|
|||||||
false,
|
false,
|
||||||
constructPayloadForYAMLInsertion(sanitizeFormData(formData, pluginInputs), plugin)
|
constructPayloadForYAMLInsertion(sanitizeFormData(formData, pluginInputs), plugin)
|
||||||
)
|
)
|
||||||
}}
|
}}>
|
||||||
validate={(formData: PluginForm) => console.log(formData)}>
|
|
||||||
{formik => {
|
{formik => {
|
||||||
formikRef.current = formik
|
formikRef.current = formik
|
||||||
return (
|
return (
|
||||||
|
@ -293,6 +293,7 @@ export interface StringsMap {
|
|||||||
in: string
|
in: string
|
||||||
inactiveBranches: string
|
inactiveBranches: string
|
||||||
isRequired: string
|
isRequired: string
|
||||||
|
key: string
|
||||||
killed: string
|
killed: string
|
||||||
leaveAComment: string
|
leaveAComment: string
|
||||||
license: string
|
license: string
|
||||||
@ -715,6 +716,7 @@ export interface StringsMap {
|
|||||||
'validation.expirationDateRequired': string
|
'validation.expirationDateRequired': string
|
||||||
'validation.gitBranchNameInvalid': string
|
'validation.gitBranchNameInvalid': string
|
||||||
'validation.gitTagNameInvalid': string
|
'validation.gitTagNameInvalid': string
|
||||||
|
'validation.key': string
|
||||||
'validation.nameInvalid': string
|
'validation.nameInvalid': string
|
||||||
'validation.nameIsRequired': string
|
'validation.nameIsRequired': string
|
||||||
'validation.nameLogic': string
|
'validation.nameLogic': string
|
||||||
|
@ -130,6 +130,7 @@ validation:
|
|||||||
nameLogic: Name must start with a letter or _ and only contain [a-zA-Z0-9-_.]
|
nameLogic: Name must start with a letter or _ and only contain [a-zA-Z0-9-_.]
|
||||||
nameTooLong: Name is too long
|
nameTooLong: Name is too long
|
||||||
nameTooShort: Name is too short
|
nameTooShort: Name is too short
|
||||||
|
key: Key cannot be empty
|
||||||
commitMessage: Commit message
|
commitMessage: Commit message
|
||||||
optionalExtendedDescription: Optional extended description
|
optionalExtendedDescription: Optional extended description
|
||||||
optional: Optional
|
optional: Optional
|
||||||
@ -827,3 +828,4 @@ enterGithubPlaceholder: https://api.github.com/
|
|||||||
changeRepoVis: Change repository visibility
|
changeRepoVis: Change repository visibility
|
||||||
changeRepoVisContent: Are you sure you want to make this repository {repoVis}? {repoText}
|
changeRepoVisContent: Are you sure you want to make this repository {repoVis}? {repoText}
|
||||||
repoVisibility: Repository Visibility
|
repoVisibility: Repository Visibility
|
||||||
|
key: Key
|
||||||
|
Loading…
Reference in New Issue
Block a user