From 57ecd18bf265f60e453640d8c7f7b9ba614cd015 Mon Sep 17 00:00:00 2001 From: kahboom Date: Mon, 23 Aug 2021 13:41:31 +0100 Subject: [PATCH 1/5] hide integration editor table if no extensions available --- .../components/editor/SaveIntegrationPage.tsx | 498 +++++++++--------- 1 file changed, 252 insertions(+), 246 deletions(-) diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx index ece87c1339d..f01cc09e8fc 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx @@ -78,272 +78,278 @@ export interface ISaveIntegrationPageProps * @todo toast notifications. * @todo redirect to the integration detail page once available. */ -export const SaveIntegrationPage: React.FunctionComponent = ({ - postPublishHref, - postSaveHref, - getBreadcrumb, - cancelHref, - ...props -}) => { - const { resource: extensionsData } = useExtensions(undefined, 'Libraries'); +export const SaveIntegrationPage: React.FunctionComponent = + ({ postPublishHref, postSaveHref, getBreadcrumb, cancelHref, ...props }) => { + const { resource: extensionsData } = useExtensions(undefined, 'Libraries'); - const [currentSelectedExtensionIds, setSelectedExtensionIds] = React.useState< - string[] - >([]); - const [error, setError] = React.useState< - false | ErrorResponse | IntegrationSaveErrorResponse - >(false); + const [currentSelectedExtensionIds, setSelectedExtensionIds] = + React.useState([]); + const [error, setError] = React.useState< + false | ErrorResponse | IntegrationSaveErrorResponse + >(false); - const { t } = useTranslation('shared'); + const { t } = useTranslation('shared'); - const extensions: IExtensionProps[] = extensionsData.items as []; - const dependencyList = React.useRef([]); + const extensions: IExtensionProps[] = extensionsData.items as []; + const dependencyList = React.useRef([]); - /** - * Here we will return an array of extension IDs later - * to be provided by the API - */ - const preSelectedExtensions: string[] = currentSelectedExtensionIds || []; + /** + * Here we will return an array of extension IDs later + * to be provided by the API + */ + const preSelectedExtensions: string[] = currentSelectedExtensionIds || []; - /** - * Updates the state based on changes in the UI - */ - const onSelect = React.useCallback( - (extensionIds: string[]) => { - setSelectedExtensionIds(extensionIds); + /** + * Updates the state based on changes in the UI + */ + const onSelect = React.useCallback( + (extensionIds: string[]) => { + setSelectedExtensionIds(extensionIds); - dependencyList.current = extensionIds.map((extension: string) => { - return { - id: extension, - type: 'EXTENSION_TAG', - }; - }); - }, - [dependencyList] - ); + dependencyList.current = extensionIds.map((extension: string) => { + return { + id: extension, + type: 'EXTENSION_TAG', + }; + }); + }, + [dependencyList] + ); - return ( - - {({ allowNavigation }) => ( - - {({ pushNotification }) => ( - > - {(params, state, { history }) => ( - - {({ deployIntegration, saveIntegration }) => { - let shouldPublish = false; + return ( + + {({ allowNavigation }) => ( + + {({ pushNotification }) => ( + > + {(params, state, { history }) => ( + + {({ deployIntegration, saveIntegration }) => { + let shouldPublish = false; - const onSave = async ( - { name, description, dependencies }: ISaveIntegrationForm, - actions: any - ) => { - setError(false); + const onSave = async ( + { + name, + description, + dependencies, + }: ISaveIntegrationForm, + actions: any + ) => { + setError(false); - try { - const updatedIntegration = setIntegrationProperties( - state.integration, - { - dependencies: dependencyList.current, - description, - name, - } - ); + try { + const updatedIntegration = setIntegrationProperties( + state.integration, + { + dependencies: dependencyList.current, + description, + name, + } + ); - const savedIntegration = await saveIntegration( - updatedIntegration - ); - if (shouldPublish) { - pushNotification( - i18n.t('integrations:PublishingIntegrationMessage'), - 'info' + const savedIntegration = await saveIntegration( + updatedIntegration ); - try { - await deployIntegration( - savedIntegration.id!, - savedIntegration.version!, - false - ); - } catch (err) { + if (shouldPublish) { pushNotification( i18n.t( - 'integrations:PublishingIntegrationFailedMessage', - { - error: err.errorMessage || err.message || err, - } + 'integrations:PublishingIntegrationMessage' ), - 'warning' + 'info' ); + try { + await deployIntegration( + savedIntegration.id!, + savedIntegration.version!, + false + ); + } catch (err) { + pushNotification( + i18n.t( + 'integrations:PublishingIntegrationFailedMessage', + { + error: + err.errorMessage || err.message || err, + } + ), + 'warning' + ); + } + } + allowNavigation(); + if (shouldPublish) { + shouldPublish = false; + history.push( + postPublishHref({ + integrationId: savedIntegration.id!, + }) + ); + } else { + history.push( + postSaveHref( + { integrationId: savedIntegration.id! }, + { ...state, integration: savedIntegration } + ) + ); + } + } catch (err) { + if (Array.isArray(err)) { + setError(err[0]); + } else { + setError(err); } } - allowNavigation(); - if (shouldPublish) { - shouldPublish = false; - history.push( - postPublishHref({ - integrationId: savedIntegration.id!, - }) - ); - } else { - history.push( - postSaveHref( - { integrationId: savedIntegration.id! }, - { ...state, integration: savedIntegration } - ) - ); - } - } catch (err) { - if (Array.isArray(err)) { - setError(err[0]); - } else { - setError(err); - } - } - actions.setSubmitting(false); - }; + actions.setSubmitting(false); + }; - const definition: IFormDefinition = { - description: { - defaultValue: '', - displayName: t('shared:Description'), - order: 1, - type: 'textarea', - }, - name: { - defaultValue: '', - displayName: t('shared:Name'), - order: 0, - required: true, - type: 'string', - }, - }; + const definition: IFormDefinition = { + description: { + defaultValue: '', + displayName: t('shared:Description'), + order: 1, + type: 'textarea', + }, + name: { + defaultValue: '', + displayName: t('shared:Name'), + order: 0, + required: true, + type: 'string', + }, + }; - const validator = (values: ISaveIntegrationForm) => - validateRequiredProperties( - definition, - (name: string) => `${name} is required`, - values - ); + const validator = (values: ISaveIntegrationForm) => + validateRequiredProperties( + definition, + (name: string) => `${name} is required`, + values + ); - return ( - <> - - i18nRequiredProperty={t( - 'shared:requiredFieldMessage' - )} - definition={definition} - initialValue={{ - dependencies: dependencyList.current, - description: state.integration.description, - name: state.integration.name, - }} - validate={validator} - validateInitial={validator} - onSave={onSave} - > - {({ - fields, - handleSubmit, - isSubmitting, - isValid, - submitForm, - }) => ( - <> - - { - shouldPublish = true; - await submitForm(); - }} - isPublishDisabled={!isValid} - isPublishLoading={isSubmitting} - i18nSave={t('shared:Save')} - i18nSaveAndPublish={t( - 'integrations:editor:save:saveAndPublish' - )} - > - <> - {error && ( - + return ( + <> + + i18nRequiredProperty={t( + 'shared:requiredFieldMessage' + )} + definition={definition} + initialValue={{ + dependencies: dependencyList.current, + description: state.integration.description, + name: state.integration.name, + }} + validate={validator} + validateInitial={validator} + onSave={onSave} + > + {({ + fields, + handleSubmit, + isSubmitting, + isValid, + submitForm, + }) => ( + <> + + { + shouldPublish = true; + await submitForm(); + }} + isPublishDisabled={!isValid} + isPublishLoading={isSubmitting} + i18nSave={t('shared:Save')} + i18nSaveAndPublish={t( + 'integrations:editor:save:saveAndPublish' )} - {fields} - + <> + {error && ( + )} - i18nTableDescription={t( - 'integrations:editor:extensions:tableDescription' + {fields} + {extensions.length > 0 && ( + )} - i18nTableName={t( - 'integrations:editor:extensions:tableName' - )} - onSelect={onSelect} - preSelectedExtensionIds={ - preSelectedExtensions - } - /> - - - } - cancelHref={cancelHref(params, state)} - /> - - )} - - - ); - }} - - )} - - )} - - )} - - ); -}; + + + } + cancelHref={cancelHref(params, state)} + /> + + )} + + + ); + }} + + )} + + )} + + )} + + ); + }; From 8f40920c8182f0be5b486a6719cde6e9fe4443ed Mon Sep 17 00:00:00 2001 From: kahboom Date: Mon, 23 Aug 2021 16:48:01 +0100 Subject: [PATCH 2/5] add support for labels in SaveIntegrationPage add IntegrationEditorLabels component add story for IntegrationEditorLabels component --- .../Editor/IntegrationEditorLabels.tsx | 12 ++++++++++++ .../packages/ui/src/Integration/Editor/index.ts | 1 + .../Editor/IntegrationEditorLabels.stories.tsx | 15 +++++++++++++++ .../components/editor/SaveIntegrationPage.tsx | 10 ++++++++++ 4 files changed, 38 insertions(+) create mode 100644 app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx create mode 100644 app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx diff --git a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx new file mode 100644 index 00000000000..0b8c1f078e4 --- /dev/null +++ b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; + +interface IIntegrationEditorLabelsProps { + labels: { [key: string]: string }; +} + +export const IntegrationEditorLabels: React.FunctionComponent = + ({ labels }) => { + // tslint:disable:no-console + console.log('labels: ' + JSON.stringify(labels)); + return <>; + }; diff --git a/app/ui-react/packages/ui/src/Integration/Editor/index.ts b/app/ui-react/packages/ui/src/Integration/Editor/index.ts index c527fa93150..a0894634e1a 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/index.ts +++ b/app/ui-react/packages/ui/src/Integration/Editor/index.ts @@ -8,6 +8,7 @@ export * from './IntegrationEditorChooseAction'; export * from './IntegrationEditorExtensionTable'; export * from './IntegrationEditorExtensionTableRows'; export * from './IntegrationEditorForm'; +export * from './IntegrationEditorLabels'; export * from './IntegrationEditorLayout'; export * from './IntegrationEditorNothingToConfigure'; export * from './IntegrationEditorNothingToConfigureAlert'; diff --git a/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx b/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx new file mode 100644 index 00000000000..a87530291de --- /dev/null +++ b/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx @@ -0,0 +1,15 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; + +const stories = storiesOf('Integration/Editor/IntegrationEditorLabels', module); + +import { IntegrationEditorLabels } from '../../../src/Integration/Editor/IntegrationEditorLabels'; + +const labels = { + key1: 'value1', + key2: 'value2', +}; + +stories.add('Integration Editor Labels', () => ( + +)); diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx index f01cc09e8fc..ddd945dde75 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx @@ -12,6 +12,7 @@ import { } from '@syndesis/models'; import { IntegrationEditorExtensionTable, + IntegrationEditorLabels, IntegrationEditorLayout, IntegrationSaveForm, SyndesisAlert, @@ -50,6 +51,7 @@ export interface ISaveIntegrationForm { * extensions for this integration */ dependencies?: Dependency[]; + labels?: { [key: string]: string }; } export interface ISaveIntegrationPageProps @@ -135,6 +137,7 @@ export const SaveIntegrationPage: React.FunctionComponent { @@ -146,6 +149,7 @@ export const SaveIntegrationPage: React.FunctionComponent )} {fields} + {state.integration.labels && ( + + )} {extensions.length > 0 && ( Date: Tue, 24 Aug 2021 17:13:33 +0100 Subject: [PATCH 3/5] map labels, add pf component, handle create functionality rename onSelect for extension list table to prevent conflict set callback for labels add arrayToObj and objToArray helper functions for labels --- .../IntegrationEditorExtensionTable.tsx | 124 +++++++++--------- .../Editor/IntegrationEditorLabels.tsx | 71 +++++++++- .../components/editor/SaveIntegrationPage.tsx | 56 ++++++-- 3 files changed, 179 insertions(+), 72 deletions(-) diff --git a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorExtensionTable.tsx b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorExtensionTable.tsx index af7053b21e7..42fc1f2982c 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorExtensionTable.tsx +++ b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorExtensionTable.tsx @@ -16,7 +16,7 @@ export interface IIntegrationEditorExtensionTableProps { i18nHeaderName: string; i18nTableDescription: string; i18nTableName: string; - onSelect: (extensionIds: string[]) => void; + onSelectExtensions: (extensionIds: string[]) => void; /** * These are provided by the API, and determine * which rows will be selected on page load. @@ -24,77 +24,83 @@ export interface IIntegrationEditorExtensionTableProps { preSelectedExtensionIds: string[]; } -export const IntegrationEditorExtensionTable: React.FunctionComponent = ( - { +export const IntegrationEditorExtensionTable: React.FunctionComponent = + ({ extensionsAvailable, i18nHeaderDescription, i18nHeaderLastUpdated, i18nHeaderName, i18nTableDescription, i18nTableName, - onSelect, - preSelectedExtensionIds + onSelectExtensions, + preSelectedExtensionIds, }) => { - /** - * Table state for array of IDs for selected extensions, - * starting with the preselected list - */ - const [selectedExtensionIds, setSelectedExtensionIds] = React.useState( - preSelectedExtensionIds - ); - - const handleSelectExtension = (extensionId: string) => { /** - * Make a shallow copy of selectedExtensions array + * Table state for array of IDs for selected extensions, + * starting with the preselected list */ - const tempArray = selectedExtensionIds.slice(); - if (!tempArray.includes(extensionId)) { - tempArray.push(extensionId); - } + const [selectedExtensionIds, setSelectedExtensionIds] = React.useState< + string[] + >(preSelectedExtensionIds); - setSelectedExtensionIds(tempArray); - }; + const handleSelectExtension = (extensionId: string) => { + /** + * Make a shallow copy of selectedExtensions array + */ + const tempArray = selectedExtensionIds.slice(); + if (!tempArray.includes(extensionId)) { + tempArray.push(extensionId); + } - const handleDeselectExtension = (extensionId: string) => { - /** - * Make a shallow copy of selectedExtensions array, - * then find the index of the selected id - */ - const tempArray = selectedExtensionIds.filter(id => id !== extensionId); - setSelectedExtensionIds(tempArray); - }; + setSelectedExtensionIds(tempArray); + }; - const onTableChange = React.useCallback((extensionId: string, isSelected: boolean) => { - if (isSelected) { - handleSelectExtension(extensionId); - } else { - handleDeselectExtension(extensionId); - } - }, [handleSelectExtension, handleDeselectExtension]); + const handleDeselectExtension = (extensionId: string) => { + /** + * Make a shallow copy of selectedExtensions array, + * then find the index of the selected id + */ + const tempArray = selectedExtensionIds.filter((id) => id !== extensionId); + setSelectedExtensionIds(tempArray); + }; - const onTableChangeAll = React.useCallback((newList: string[]) => { - setSelectedExtensionIds(newList); - }, [setSelectedExtensionIds]); + const onTableChange = React.useCallback( + (extensionId: string, isSelected: boolean) => { + if (isSelected) { + handleSelectExtension(extensionId); + } else { + handleDeselectExtension(extensionId); + } + }, + [handleSelectExtension, handleDeselectExtension] + ); - const callOnSelect = () => { - onSelect(selectedExtensionIds); - }; + const onTableChangeAll = React.useCallback( + (newList: string[]) => { + setSelectedExtensionIds(newList); + }, + [setSelectedExtensionIds] + ); - useEffect(() => { - callOnSelect(); - }, [callOnSelect]); + const callOnSelect = () => { + onSelectExtensions(selectedExtensionIds); + }; - return ( - - ); -} + useEffect(() => { + callOnSelect(); + }, [callOnSelect]); + + return ( + + ); + }; diff --git a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx index 0b8c1f078e4..822b3f3debf 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx +++ b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx @@ -1,12 +1,75 @@ +import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'; import * as React from 'react'; interface IIntegrationEditorLabelsProps { - labels: { [key: string]: string }; + labels?: { [key: string]: string }; + onSelect: (something?: any) => void; } export const IntegrationEditorLabels: React.FunctionComponent = ({ labels }) => { - // tslint:disable:no-console - console.log('labels: ' + JSON.stringify(labels)); - return <>; + const initialArray: string[] = []; + + if (labels) { + Object.entries(labels).map((l, k) => { + initialArray.push(l[0] + '=' + l[1]); + }); + } + + const [customLabels, setCustomLabels] = React.useState(initialArray); + const [isOpen, setIsOpen] = React.useState(false); + + const onCreateOption = (newValue: string) => { + setCustomLabels([...customLabels, newValue]); + }; + + const onToggle = (isOpenNew: boolean) => { + setIsOpen(isOpenNew); + }; + + const onSelect = ( + event: React.MouseEvent | React.ChangeEvent, + value: any + ) => { + if (customLabels.includes(value)) { + setCustomLabels(customLabels.filter((item) => item !== value)); + } else { + setCustomLabels([...customLabels, value]); + } + }; + + const clearSelection = () => { + setCustomLabels([]); + setIsOpen(false); + }; + + const titleId = 'integration-editor-select'; + const placeholderText = 'Specify a label in this format: key=value'; + + return ( + <> +
+ + +
+ + ); }; diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx index ddd945dde75..321a5e2172d 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx @@ -68,6 +68,29 @@ export interface ISaveIntegrationPageProps postPublishHref: (p: IPostPublishRouteParams) => H.LocationDescriptorObject; } +const convertLabelObjectToArray = (labels: { [s: string]: string }) => { + const initialArray: string[] = []; + + Object.entries(labels).map((l, k) => { + initialArray.push(l[0] + '=' + l[1]); + }); + + return initialArray; +}; + +const convertLabelArrayIntoObject = ( + labels: string[] +): { [s: string]: string } => { + let newObj: { [s: string]: string } = {}; + + labels.map((label) => { + const splitLabel = label.split('='); + const labelKey = splitLabel[0]; + newObj = { ...newObj, [labelKey]: splitLabel[1] }; + }); + return newObj; +}; + /** * This page asks for the details of the integration, and saves it. * @@ -86,6 +109,9 @@ export const SaveIntegrationPage: React.FunctionComponent([]); + const [currentSelectLabels, setCurrentSelectLabels] = React.useState<{ + [key: string]: string; + }>({}); const [error, setError] = React.useState< false | ErrorResponse | IntegrationSaveErrorResponse >(false); @@ -94,6 +120,7 @@ export const SaveIntegrationPage: React.FunctionComponent([]); + const labelList = React.useRef<{ [key: string]: string }>({}); /** * Here we will return an array of extension IDs later @@ -104,7 +131,7 @@ export const SaveIntegrationPage: React.FunctionComponent { setSelectedExtensionIds(extensionIds); @@ -118,6 +145,15 @@ export const SaveIntegrationPage: React.FunctionComponent { + // parse into key/value pairs + + labelList.current = convertLabelArrayIntoObject(labels); + }, + [labelList] + ); + return ( {({ allowNavigation }) => ( @@ -149,7 +185,7 @@ export const SaveIntegrationPage: React.FunctionComponent )} {fields} - {state.integration.labels && ( - - )} + {extensions.length > 0 && ( Date: Wed, 25 Aug 2021 11:28:29 +0100 Subject: [PATCH 4/5] move data transformation to SaveIntegrationPage rm label state from SaveIntegrationPage to prevent deep re-rendering, handle changes to label selection update story add regex improvements --- .../Editor/IntegrationEditorLabels.tsx | 66 ++++++++++++------- .../IntegrationEditorLabels.stories.tsx | 11 ++-- .../components/editor/SaveIntegrationPage.tsx | 28 ++++---- 3 files changed, 65 insertions(+), 40 deletions(-) diff --git a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx index 822b3f3debf..685df8c33fe 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx +++ b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx @@ -2,25 +2,42 @@ import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'; import * as React from 'react'; interface IIntegrationEditorLabelsProps { - labels?: { [key: string]: string }; - onSelect: (something?: any) => void; + initialLabels: string[]; + onSelectLabels: (labels: string[]) => void; } -export const IntegrationEditorLabels: React.FunctionComponent = - ({ labels }) => { - const initialArray: string[] = []; - - if (labels) { - Object.entries(labels).map((l, k) => { - initialArray.push(l[0] + '=' + l[1]); - }); - } +/** + * Valid format: alphanumeric=alphanumeric + * e.g. Rachel=pizza, lex=hotdogs123 + * @param input + */ +const validateLabel = (input: string): boolean => { + const regexIncludeEqual = /(^\w+)(=)(\w+$)/g; + return regexIncludeEqual.test(input); +}; - const [customLabels, setCustomLabels] = React.useState(initialArray); +export const IntegrationEditorLabels: React.FunctionComponent = + ({ initialLabels, onSelectLabels }) => { + const [labels, setLabels] = React.useState(initialLabels); const [isOpen, setIsOpen] = React.useState(false); + const labelRef = React.useRef(labels); + const isValid = React.useRef(true); + + React.useEffect(() => { + if (labelRef.current === labels) { + return; + } else { + labelRef.current = labels; + onSelectLabels(labelRef.current); + } + }, [labels]); const onCreateOption = (newValue: string) => { - setCustomLabels([...customLabels, newValue]); + // don't create if it's invalid + if (!validateLabel(newValue)) { + isValid.current = false; + return; + } }; const onToggle = (isOpenNew: boolean) => { @@ -31,15 +48,17 @@ export const IntegrationEditorLabels: React.FunctionComponent { - if (customLabels.includes(value)) { - setCustomLabels(customLabels.filter((item) => item !== value)); - } else { - setCustomLabels([...customLabels, value]); + if (labels.includes(value)) { + setLabels(labels.filter((item) => item !== value)); + } else if (validateLabel(value)) { + setLabels([...labels, value]); } + + setIsOpen(false); }; const clearSelection = () => { - setCustomLabels([]); + setLabels([]); setIsOpen(false); }; @@ -61,14 +80,17 @@ export const IntegrationEditorLabels: React.FunctionComponent - {initialArray.map((option, index) => ( - - ))} + {initialLabels && + initialLabels.map((option, index) => ( + + ))} + {!isValid &&

Please use the following format: key=value

} ); diff --git a/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx b/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx index a87530291de..42c7766c1cd 100644 --- a/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx +++ b/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx @@ -1,3 +1,4 @@ +import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import * as React from 'react'; @@ -5,11 +6,11 @@ const stories = storiesOf('Integration/Editor/IntegrationEditorLabels', module); import { IntegrationEditorLabels } from '../../../src/Integration/Editor/IntegrationEditorLabels'; -const labels = { - key1: 'value1', - key2: 'value2', -}; +const labels = ['rachel=pizza', 'lex=hotdogs']; stories.add('Integration Editor Labels', () => ( - + )); diff --git a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx index 321a5e2172d..67d7f086ef4 100644 --- a/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx +++ b/app/ui-react/syndesis/src/modules/integrations/components/editor/SaveIntegrationPage.tsx @@ -68,11 +68,13 @@ export interface ISaveIntegrationPageProps postPublishHref: (p: IPostPublishRouteParams) => H.LocationDescriptorObject; } -const convertLabelObjectToArray = (labels: { [s: string]: string }) => { +const convertLabelObjectToArray = (labels: { + [s: string]: string; +}): string[] => { const initialArray: string[] = []; Object.entries(labels).map((l, k) => { - initialArray.push(l[0] + '=' + l[1]); + return initialArray.push(l[0] + '=' + l[1]); }); return initialArray; @@ -86,7 +88,7 @@ const convertLabelArrayIntoObject = ( labels.map((label) => { const splitLabel = label.split('='); const labelKey = splitLabel[0]; - newObj = { ...newObj, [labelKey]: splitLabel[1] }; + return (newObj = { ...newObj, [labelKey]: splitLabel[1] }); }); return newObj; }; @@ -109,15 +111,11 @@ export const SaveIntegrationPage: React.FunctionComponent([]); - const [currentSelectLabels, setCurrentSelectLabels] = React.useState<{ - [key: string]: string; - }>({}); const [error, setError] = React.useState< false | ErrorResponse | IntegrationSaveErrorResponse >(false); const { t } = useTranslation('shared'); - const extensions: IExtensionProps[] = extensionsData.items as []; const dependencyList = React.useRef([]); const labelList = React.useRef<{ [key: string]: string }>({}); @@ -148,7 +146,6 @@ export const SaveIntegrationPage: React.FunctionComponent { // parse into key/value pairs - labelList.current = convertLabelArrayIntoObject(labels); }, [labelList] @@ -185,7 +182,7 @@ export const SaveIntegrationPage: React.FunctionComponent @@ -278,9 +279,7 @@ export const SaveIntegrationPage: React.FunctionComponent {extensions.length > 0 && ( Date: Wed, 25 Aug 2021 20:55:20 +0100 Subject: [PATCH 5/5] add form components to format styling properly fix for select option add tests add story for no pre-existing labels remove user-event and relevant tests for now --- .../IntegrationEditorLabels.spec.tsx | 26 +++++++++++++++++++ app/ui-react/packages/ui/package.json | 2 +- .../Editor/IntegrationEditorLabels.tsx | 26 +++++++++++++++---- .../IntegrationEditorLabels.stories.tsx | 9 ++++++- 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 app/ui-react/packages/ui/__tests__/Integration/IntegrationEditorLabels.spec.tsx diff --git a/app/ui-react/packages/ui/__tests__/Integration/IntegrationEditorLabels.spec.tsx b/app/ui-react/packages/ui/__tests__/Integration/IntegrationEditorLabels.spec.tsx new file mode 100644 index 00000000000..4d1a0f112a1 --- /dev/null +++ b/app/ui-react/packages/ui/__tests__/Integration/IntegrationEditorLabels.spec.tsx @@ -0,0 +1,26 @@ +import '@testing-library/dom'; +import '@testing-library/jest-dom/extend-expect'; +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { IntegrationEditorLabels } from '../../src/Integration'; + +function renderComponent(props) { + const utils = render(); + const labelSelector = utils.getByTestId('integration-label-select'); + return { ...utils, labelSelector }; +} + +export default describe('IntegrationEditorLabels.tsx', () => { + // when there are no pre-existing labels + it('the selector should load without pre-existing labels', () => { + const labels = []; + const onSelectLabels = jest.fn(); + const { labelSelector } = renderComponent({ + initialLabels: labels, + onSelectLabels, + }); + + // the component is initialized and loads without pre-existing labels + expect(labelSelector).toBeInTheDocument(); + }); +}); diff --git a/app/ui-react/packages/ui/package.json b/app/ui-react/packages/ui/package.json index 27da34bb3d1..64c69be7a33 100644 --- a/app/ui-react/packages/ui/package.json +++ b/app/ui-react/packages/ui/package.json @@ -32,8 +32,8 @@ "@storybook/addon-viewport": "^5.3.8", "@storybook/react": "^5.3.8", "@storybook/theming": "^5.3.8", - "@testing-library/react": "^9.3.2", "@testing-library/jest-dom": "^4.0.0", + "@testing-library/react": "^9.3.2", "@types/classnames": "^2.2.6", "@types/codemirror": "^0.0.85", "@types/expect": "^24.3.0", diff --git a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx index 685df8c33fe..d0d835a7703 100644 --- a/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx +++ b/app/ui-react/packages/ui/src/Integration/Editor/IntegrationEditorLabels.tsx @@ -1,7 +1,12 @@ -import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'; +import { + FormGroup, + Select, + SelectOption, + SelectVariant, +} from '@patternfly/react-core'; import * as React from 'react'; -interface IIntegrationEditorLabelsProps { +export interface IIntegrationEditorLabelsProps { initialLabels: string[]; onSelectLabels: (labels: string[]) => void; } @@ -66,7 +71,11 @@ export const IntegrationEditorLabels: React.FunctionComponent +
- +
); }; diff --git a/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx b/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx index 42c7766c1cd..e5493edaa42 100644 --- a/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx +++ b/app/ui-react/packages/ui/stories/Integration/Editor/IntegrationEditorLabels.stories.tsx @@ -8,9 +8,16 @@ import { IntegrationEditorLabels } from '../../../src/Integration/Editor/Integra const labels = ['rachel=pizza', 'lex=hotdogs']; -stories.add('Integration Editor Labels', () => ( +stories.add('Pre-Existing Labels', () => ( )); + +stories.add('No Labels', () => ( + +));