mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: project actions UI form (#6115)
## About the changes Add, delete, and update actions is already working. The UI still needs some love, but it's functional  --------- Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
		
							parent
							
								
									260ef70309
								
							
						
					
					
						commit
						924ea39ea2
					
				| @ -26,7 +26,7 @@ export const ProjectActionsActionsCell = ({ | ||||
|     action, | ||||
|     onCreateAction, | ||||
| }: IProjectActionsActionsCellProps) => { | ||||
|     const { actions } = action; | ||||
|     const { id: actionSetId, actions } = action; | ||||
| 
 | ||||
|     if (actions.length === 0) { | ||||
|         if (!onCreateAction) return <TextCell>0 actions</TextCell>; | ||||
| @ -38,21 +38,25 @@ export const ProjectActionsActionsCell = ({ | ||||
|             <TooltipLink | ||||
|                 tooltip={ | ||||
|                     <StyledActionItems> | ||||
|                         {actions.map(({ id, action, executionParams }) => ( | ||||
|                             <div key={id}> | ||||
|                                 <strong>{action}</strong> | ||||
|                                 <StyledParameterList> | ||||
|                                     {Object.entries(executionParams).map( | ||||
|                                         ([param, value]) => ( | ||||
|                                             <li key={param}> | ||||
|                                                 <strong>{param}</strong>:{' '} | ||||
|                                                 {value} | ||||
|                                             </li> | ||||
|                                         ), | ||||
|                                     )} | ||||
|                                 </StyledParameterList> | ||||
|                             </div> | ||||
|                         ))} | ||||
|                         {actions.map( | ||||
|                             ({ action, executionParams, sortOrder }) => ( | ||||
|                                 <div | ||||
|                                     key={`${actionSetId}/${sortOrder}_${action}`} | ||||
|                                 > | ||||
|                                     <strong>{action}</strong> | ||||
|                                     <StyledParameterList> | ||||
|                                         {Object.entries(executionParams).map( | ||||
|                                             ([param, value]) => ( | ||||
|                                                 <li key={param}> | ||||
|                                                     <strong>{param}</strong>:{' '} | ||||
|                                                     {value} | ||||
|                                                 </li> | ||||
|                                             ), | ||||
|                                         )} | ||||
|                                     </StyledParameterList> | ||||
|                                 </div> | ||||
|                             ), | ||||
|                         )} | ||||
|                     </StyledActionItems> | ||||
|                 } | ||||
|             > | ||||
|  | ||||
| @ -0,0 +1,134 @@ | ||||
| import { IconButton, Tooltip } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { IAction } from 'interfaces/action'; | ||||
| import { Fragment } from 'react'; | ||||
| import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; | ||||
| import { Delete } from '@mui/icons-material'; | ||||
| import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import mapValues from 'lodash.mapvalues'; | ||||
| import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; | ||||
| import { | ||||
|     BoxSeparator, | ||||
|     Col, | ||||
|     InnerBoxHeader, | ||||
|     Row, | ||||
|     StyledInnerBox, | ||||
| } from './InnerContainerBox'; | ||||
| 
 | ||||
| export type UIAction = Omit<IAction, 'id' | 'createdAt' | 'createdByUserId'> & { | ||||
|     id: string; | ||||
| }; | ||||
| 
 | ||||
| export const ActionItem = ({ | ||||
|     action, | ||||
|     index, | ||||
|     stateChanged, | ||||
|     onDelete, | ||||
| }: { | ||||
|     action: UIAction; | ||||
|     index: number; | ||||
|     stateChanged: (action: UIAction) => void; | ||||
|     onDelete: () => void; | ||||
| }) => { | ||||
|     const { id, action: actionName } = action; | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const environments = useProjectEnvironments(projectId); | ||||
|     const { features } = useFeatureSearch( | ||||
|         mapValues( | ||||
|             { | ||||
|                 project: `IS:${projectId}`, | ||||
|             }, | ||||
|             (value) => (value ? `${value}` : undefined), | ||||
|         ), | ||||
|         {}, | ||||
|     ); | ||||
|     return ( | ||||
|         <Fragment> | ||||
|             <ConditionallyRender | ||||
|                 condition={index > 0} | ||||
|                 show={<BoxSeparator>THEN</BoxSeparator>} | ||||
|             /> | ||||
|             <StyledInnerBox> | ||||
|                 <Row> | ||||
|                     <span>Action {index + 1}</span> | ||||
|                     <InnerBoxHeader> | ||||
|                         <Tooltip title='Delete action' arrow> | ||||
|                             <IconButton onClick={onDelete}> | ||||
|                                 <Delete /> | ||||
|                             </IconButton> | ||||
|                         </Tooltip> | ||||
|                     </InnerBoxHeader> | ||||
|                 </Row> | ||||
|                 <Row> | ||||
|                     <Col> | ||||
|                         <GeneralSelect | ||||
|                             label='Action' | ||||
|                             name='action' | ||||
|                             options={[ | ||||
|                                 { | ||||
|                                     label: 'Enable flag', | ||||
|                                     key: 'TOGGLE_FEATURE_ON', | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     label: 'Disable flag', | ||||
|                                     key: 'TOGGLE_FEATURE_OFF', | ||||
|                                 }, | ||||
|                             ]} | ||||
|                             value={actionName} | ||||
|                             onChange={(selected) => | ||||
|                                 stateChanged({ | ||||
|                                     ...action, | ||||
|                                     action: selected, | ||||
|                                 }) | ||||
|                             } | ||||
|                             fullWidth | ||||
|                         /> | ||||
|                     </Col> | ||||
|                     <Col> | ||||
|                         <GeneralSelect | ||||
|                             label='Environment' | ||||
|                             name='environment' | ||||
|                             options={environments.environments.map((env) => ({ | ||||
|                                 label: env.name, | ||||
|                                 key: env.name, | ||||
|                             }))} | ||||
|                             value={action.executionParams.environment as string} | ||||
|                             onChange={(selected) => | ||||
|                                 stateChanged({ | ||||
|                                     ...action, | ||||
|                                     executionParams: { | ||||
|                                         ...action.executionParams, | ||||
|                                         environment: selected, | ||||
|                                     }, | ||||
|                                 }) | ||||
|                             } | ||||
|                             fullWidth | ||||
|                         /> | ||||
|                     </Col> | ||||
|                     <Col> | ||||
|                         <GeneralSelect | ||||
|                             label='Flag name' | ||||
|                             name='flag' | ||||
|                             options={features.map((feature) => ({ | ||||
|                                 label: feature.name, | ||||
|                                 key: feature.name, | ||||
|                             }))} | ||||
|                             value={action.executionParams.featureName as string} | ||||
|                             onChange={(selected) => | ||||
|                                 stateChanged({ | ||||
|                                     ...action, | ||||
|                                     executionParams: { | ||||
|                                         ...action.executionParams, | ||||
|                                         featureName: selected, | ||||
|                                     }, | ||||
|                                 }) | ||||
|                             } | ||||
|                             fullWidth | ||||
|                         /> | ||||
|                     </Col> | ||||
|                 </Row> | ||||
|             </StyledInnerBox> | ||||
|         </Fragment> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,80 @@ | ||||
| import { Badge, IconButton, Tooltip, styled } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { IActionFilter } from './useProjectActionsForm'; | ||||
| import { Fragment } from 'react'; | ||||
| import { Delete } from '@mui/icons-material'; | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import { | ||||
|     BoxSeparator, | ||||
|     InnerBoxHeader, | ||||
|     Row, | ||||
|     StyledInnerBox, | ||||
| } from './InnerContainerBox'; | ||||
| 
 | ||||
| const StyledInput = styled(Input)(() => ({ | ||||
|     width: '100%', | ||||
| })); | ||||
| 
 | ||||
| const StyledBadge = styled(Badge)(({ theme }) => ({ | ||||
|     color: 'primary', | ||||
|     margin: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| export const FilterItem = ({ | ||||
|     filter, | ||||
|     index, | ||||
|     stateChanged, | ||||
|     onDelete, | ||||
| }: { | ||||
|     filter: IActionFilter; | ||||
|     index: number; | ||||
|     stateChanged: (updatedFilter: IActionFilter) => void; | ||||
|     onDelete: () => void; | ||||
| }) => { | ||||
|     const { id, parameter, value } = filter; | ||||
|     return ( | ||||
|         <Fragment> | ||||
|             <ConditionallyRender | ||||
|                 condition={index > 0} | ||||
|                 show={<BoxSeparator>AND</BoxSeparator>} | ||||
|             /> | ||||
|             <StyledInnerBox> | ||||
|                 <Row> | ||||
|                     <span>Filter {index + 1}</span> | ||||
|                     <InnerBoxHeader> | ||||
|                         <Tooltip title='Delete filter' arrow> | ||||
|                             <IconButton type='button' onClick={onDelete}> | ||||
|                                 <Delete /> | ||||
|                             </IconButton> | ||||
|                         </Tooltip> | ||||
|                     </InnerBoxHeader> | ||||
|                 </Row> | ||||
|                 <Row> | ||||
|                     <StyledInput | ||||
|                         label='Parameter' | ||||
|                         value={parameter} | ||||
|                         onChange={(e) => | ||||
|                             stateChanged({ | ||||
|                                 id, | ||||
|                                 parameter: e.target.value, | ||||
|                                 value, | ||||
|                             }) | ||||
|                         } | ||||
|                     /> | ||||
|                     <StyledBadge>=</StyledBadge> | ||||
|                     <StyledInput | ||||
|                         label='Value' | ||||
|                         value={value} | ||||
|                         onChange={(e) => | ||||
|                             stateChanged({ | ||||
|                                 id, | ||||
|                                 parameter, | ||||
|                                 value: e.target.value, | ||||
|                             }) | ||||
|                         } | ||||
|                     /> | ||||
|                 </Row> | ||||
|             </StyledInnerBox> | ||||
|         </Fragment> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,57 @@ | ||||
| import { Box, styled } from '@mui/material'; | ||||
| 
 | ||||
| export const StyledInnerBox = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     backgroundColor: theme.palette.background.default, | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     padding: theme.spacing(2), | ||||
|     borderRadius: `${theme.shape.borderRadiusMedium}px`, | ||||
| })); | ||||
| 
 | ||||
| export const InnerBoxHeader = styled('div')(({ theme }) => ({ | ||||
|     marginLeft: 'auto', | ||||
|     whiteSpace: 'nowrap', | ||||
|     [theme.breakpoints.down('sm')]: { | ||||
|         display: 'none', | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| // row for inner containers
 | ||||
| export const Row = styled('div')({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'row', | ||||
|     width: '100%', | ||||
| }); | ||||
| 
 | ||||
| export const Col = styled('div')({ | ||||
|     flex: 1, | ||||
|     margin: '0 4px', | ||||
| }); | ||||
| 
 | ||||
| export const BoxSeparator: React.FC = ({ children }) => { | ||||
|     const StyledBoxContent = styled('div')(({ theme }) => ({ | ||||
|         padding: theme.spacing(0.75, 1), | ||||
|         color: theme.palette.text.primary, | ||||
|         fontSize: theme.fontSizes.smallerBody, | ||||
|         backgroundColor: theme.palette.seen.primary, | ||||
|         borderRadius: theme.shape.borderRadius, | ||||
|         position: 'absolute', | ||||
|         zIndex: theme.zIndex.fab, | ||||
|         top: '50%', | ||||
|         left: theme.spacing(2), | ||||
|         transform: 'translateY(-50%)', | ||||
|         lineHeight: 1, | ||||
|     })); | ||||
|     return ( | ||||
|         <Box | ||||
|             sx={{ | ||||
|                 height: 1.5, | ||||
|                 position: 'relative', | ||||
|                 width: '100%', | ||||
|             }} | ||||
|         > | ||||
|             <StyledBoxContent>{children}</StyledBoxContent> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -1,11 +1,24 @@ | ||||
| import { Alert, Link, styled } from '@mui/material'; | ||||
| import { Alert, Box, Button, Link, styled } from '@mui/material'; | ||||
| import { Link as RouterLink } from 'react-router-dom'; | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { FormSwitch } from 'component/common/FormSwitch/FormSwitch'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { IAction, IActionSet } from 'interfaces/action'; | ||||
| import { ProjectActionsFormErrors } from './useProjectActionsForm'; | ||||
| import { IActionSet } from 'interfaces/action'; | ||||
| import { | ||||
|     IActionFilter, | ||||
|     ProjectActionsFormErrors, | ||||
| } from './useProjectActionsForm'; | ||||
| import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts'; | ||||
| import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | ||||
| import { useMemo } from 'react'; | ||||
| import GeneralSelect, {} from 'component/common/GeneralSelect/GeneralSelect'; | ||||
| import { Add } from '@mui/icons-material'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { Row } from './InnerContainerBox'; | ||||
| import { ActionItem, UIAction } from './ActionItem'; | ||||
| import { FilterItem } from './FilterItem'; | ||||
| 
 | ||||
| const StyledServiceAccountAlert = styled(Alert)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(4), | ||||
| @ -27,30 +40,31 @@ const StyledInputDescription = styled('p')(({ theme }) => ({ | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledInputSecondaryDescription = styled('p')(({ theme }) => ({ | ||||
|     color: theme.palette.text.secondary, | ||||
|     marginBottom: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledInput = styled(Input)(({ theme }) => ({ | ||||
| const StyledInput = styled(Input)(() => ({ | ||||
|     width: '100%', | ||||
|     maxWidth: theme.spacing(50), | ||||
| })); | ||||
| 
 | ||||
| const StyledSecondarySection = styled('div')(({ theme }) => ({ | ||||
|     padding: theme.spacing(3), | ||||
|     backgroundColor: theme.palette.background.elevation2, | ||||
| const StyledBadge = styled(Badge)(({ theme }) => ({ | ||||
|     color: 'primary', | ||||
|     margin: 'auto', | ||||
|     marginBottom: theme.spacing(1.5), | ||||
| })); | ||||
| 
 | ||||
| const StyledBox = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     backgroundColor: theme.palette.background.elevation1, | ||||
|     marginTop: theme.spacing(2), | ||||
|     padding: theme.spacing(2), | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     marginTop: theme.spacing(4), | ||||
|     marginBottom: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledInlineContainer = styled('div')(({ theme }) => ({ | ||||
|     padding: theme.spacing(0, 4), | ||||
|     '& > p:not(:first-of-type)': { | ||||
|         marginTop: theme.spacing(2), | ||||
|     }, | ||||
| })); | ||||
| const Step = ({ name, children }: any) => ( | ||||
|     <StyledBox> | ||||
|         <StyledBadge color='secondary'>{name}</StyledBadge> | ||||
|         {children} | ||||
|     </StyledBox> | ||||
| ); | ||||
| 
 | ||||
| interface IProjectActionsFormProps { | ||||
|     action?: IActionSet; | ||||
| @ -60,12 +74,12 @@ interface IProjectActionsFormProps { | ||||
|     setName: React.Dispatch<React.SetStateAction<string>>; | ||||
|     sourceId: number; | ||||
|     setSourceId: React.Dispatch<React.SetStateAction<number>>; | ||||
|     filters: Record<string, unknown>; | ||||
|     setFilters: React.Dispatch<React.SetStateAction<Record<string, unknown>>>; | ||||
|     filters: IActionFilter[]; | ||||
|     setFilters: React.Dispatch<React.SetStateAction<IActionFilter[]>>; | ||||
|     actorId: number; | ||||
|     setActorId: React.Dispatch<React.SetStateAction<number>>; | ||||
|     actions: IAction[]; | ||||
|     setActions: React.Dispatch<React.SetStateAction<IAction[]>>; | ||||
|     actions: UIAction[]; | ||||
|     setActions: React.Dispatch<React.SetStateAction<UIAction[]>>; | ||||
|     errors: ProjectActionsFormErrors; | ||||
|     validateName: (name: string) => boolean; | ||||
|     validated: boolean; | ||||
| @ -89,16 +103,83 @@ export const ProjectActionsForm = ({ | ||||
|     validateName, | ||||
|     validated, | ||||
| }: IProjectActionsFormProps) => { | ||||
|     const { serviceAccounts } = useServiceAccounts(); | ||||
|     const { serviceAccounts, loading: serviceAccountsLoading } = | ||||
|         useServiceAccounts(); | ||||
|     const { incomingWebhooks, loading: incomingWebhooksLoading } = | ||||
|         useIncomingWebhooks(); | ||||
| 
 | ||||
|     const handleOnBlur = (callback: Function) => { | ||||
|         setTimeout(() => callback(), 300); | ||||
|     }; | ||||
| 
 | ||||
|     const addFilter = () => { | ||||
|         const id = uuidv4(); | ||||
|         setFilters((filters) => [ | ||||
|             ...filters, | ||||
|             { | ||||
|                 id, | ||||
|                 parameter: '', | ||||
|                 value: '', | ||||
|             }, | ||||
|         ]); | ||||
|     }; | ||||
| 
 | ||||
|     const updateInFilters = (updatedFilter: IActionFilter) => { | ||||
|         setFilters((filters) => | ||||
|             filters.map((filter) => | ||||
|                 filter.id === updatedFilter.id ? updatedFilter : filter, | ||||
|             ), | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     const addAction = (projectId: string) => { | ||||
|         const id = uuidv4(); | ||||
|         const action: UIAction = { | ||||
|             id, | ||||
|             action: '', | ||||
|             sortOrder: | ||||
|                 actions | ||||
|                     .map((a) => a.sortOrder) | ||||
|                     .reduce((a, b) => Math.max(a, b), 0) + 1, | ||||
|             executionParams: { | ||||
|                 project: projectId, | ||||
|             }, | ||||
|         }; | ||||
|         setActions([...actions, action]); | ||||
|     }; | ||||
| 
 | ||||
|     const updateInActions = (updatedAction: UIAction) => { | ||||
|         setActions((actions) => | ||||
|             actions.map((action) => | ||||
|                 action.id === updatedAction.id ? updatedAction : action, | ||||
|             ), | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     const incomingWebhookOptions = useMemo(() => { | ||||
|         if (incomingWebhooksLoading) { | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         return incomingWebhooks.map((webhook) => ({ | ||||
|             label: webhook.name, | ||||
|             key: `${webhook.id}`, | ||||
|         })); | ||||
|     }, [incomingWebhooksLoading, incomingWebhooks]); | ||||
| 
 | ||||
|     const serviceAccountOptions = useMemo(() => { | ||||
|         if (serviceAccountsLoading) { | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         return serviceAccounts.map((sa) => ({ | ||||
|             label: sa.name, | ||||
|             key: `${sa.id}`, | ||||
|         })); | ||||
|     }, [serviceAccountsLoading, serviceAccounts]); | ||||
| 
 | ||||
|     const showErrors = validated && Object.values(errors).some(Boolean); | ||||
| 
 | ||||
|     // TODO: Need to add the remaining fields. Refer to the design
 | ||||
| 
 | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     return ( | ||||
|         <div> | ||||
|             <ConditionallyRender | ||||
| @ -138,6 +219,100 @@ export const ProjectActionsForm = ({ | ||||
|                 onBlur={(e) => handleOnBlur(() => validateName(e.target.value))} | ||||
|                 autoComplete='off' | ||||
|             /> | ||||
| 
 | ||||
|             <Step name='Trigger'> | ||||
|                 <StyledInputDescription> | ||||
|                     Create incoming webhooks from  | ||||
|                     <RouterLink to='/integrations/incoming-webhooks'> | ||||
|                         integrations section | ||||
|                     </RouterLink> | ||||
|                     . | ||||
|                 </StyledInputDescription> | ||||
|                 <GeneralSelect | ||||
|                     label='Incoming webhook' | ||||
|                     name='incoming-webhook' | ||||
|                     options={incomingWebhookOptions} | ||||
|                     value={`${sourceId}`} | ||||
|                     onChange={(v) => { | ||||
|                         setSourceId(parseInt(v)); | ||||
|                     }} | ||||
|                 /> | ||||
|             </Step> | ||||
| 
 | ||||
|             <Step name='When this'> | ||||
|                 {filters.map((filter, index) => ( | ||||
|                     <FilterItem | ||||
|                         key={filter.id} | ||||
|                         index={index} | ||||
|                         filter={filter} | ||||
|                         stateChanged={updateInFilters} | ||||
|                         onDelete={() => | ||||
|                             setFilters((filters) => | ||||
|                                 filters.filter((f) => f.id !== filter.id), | ||||
|                             ) | ||||
|                         } | ||||
|                     /> | ||||
|                 ))} | ||||
| 
 | ||||
|                 <hr /> | ||||
|                 <Row> | ||||
|                     <Button | ||||
|                         type='button' | ||||
|                         startIcon={<Add />} | ||||
|                         onClick={addFilter} | ||||
|                         variant='outlined' | ||||
|                         color='primary' | ||||
|                     > | ||||
|                         Add filter | ||||
|                     </Button> | ||||
|                 </Row> | ||||
|             </Step> | ||||
| 
 | ||||
|             <Step name='Do these action(s)'> | ||||
|                 <StyledInputDescription> | ||||
|                     Create service accounts from  | ||||
|                     <RouterLink to='/admin/service-accounts'> | ||||
|                         service accounts section | ||||
|                     </RouterLink> | ||||
|                     . | ||||
|                 </StyledInputDescription> | ||||
|                 <GeneralSelect | ||||
|                     label='Service account' | ||||
|                     name='service-account' | ||||
|                     options={serviceAccountOptions} | ||||
|                     value={`${actorId}`} | ||||
|                     onChange={(v) => { | ||||
|                         setActorId(parseInt(v)); | ||||
|                     }} | ||||
|                 /> | ||||
|                 <hr /> | ||||
|                 {actions.map((action, index) => ( | ||||
|                     <ActionItem | ||||
|                         index={index} | ||||
|                         key={action.id} | ||||
|                         action={action} | ||||
|                         stateChanged={updateInActions} | ||||
|                         onDelete={() => | ||||
|                             setActions((actions) => | ||||
|                                 actions.filter((a) => a.id !== action.id), | ||||
|                             ) | ||||
|                         } | ||||
|                     /> | ||||
|                 ))} | ||||
|                 <hr /> | ||||
|                 <Row> | ||||
|                     <Button | ||||
|                         type='button' | ||||
|                         startIcon={<Add />} | ||||
|                         onClick={() => addAction(projectId)} | ||||
|                         variant='outlined' | ||||
|                         color='primary' | ||||
|                     > | ||||
|                         Add action | ||||
|                     </Button> | ||||
|                 </Row> | ||||
|             </Step> | ||||
| 
 | ||||
|             <ConditionallyRender | ||||
|                 condition={showErrors} | ||||
|                 show={() => ( | ||||
|  | ||||
| @ -1,14 +1,22 @@ | ||||
| import { useActions } from 'hooks/api/getters/useActions/useActions'; | ||||
| import { IAction, IActionSet } from 'interfaces/action'; | ||||
| import { IActionSet } from 'interfaces/action'; | ||||
| import { useEffect, useState } from 'react'; | ||||
| import { UIAction } from './ActionItem'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | ||||
| 
 | ||||
| enum ErrorField { | ||||
| export enum ErrorField { | ||||
|     NAME = 'name', | ||||
|     TRIGGER = 'trigger', | ||||
|     ACTOR = 'actor', | ||||
|     ACTIONS = 'actions', | ||||
| } | ||||
| 
 | ||||
| export interface IActionFilter { | ||||
|     id: string; | ||||
|     parameter: string; | ||||
|     value: string; | ||||
| } | ||||
| 
 | ||||
| const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = { | ||||
|     [ErrorField.NAME]: undefined, | ||||
|     [ErrorField.TRIGGER]: undefined, | ||||
| @ -24,14 +32,38 @@ export const useProjectActionsForm = (action?: IActionSet) => { | ||||
|     const [enabled, setEnabled] = useState(false); | ||||
|     const [name, setName] = useState(''); | ||||
|     const [sourceId, setSourceId] = useState<number>(0); | ||||
|     const [filters, setFilters] = useState<Record<string, unknown>>({}); | ||||
|     const [filters, setFilters] = useState<IActionFilter[]>([]); | ||||
|     const [actorId, setActorId] = useState<number>(0); | ||||
|     const [actions, setActions] = useState<IAction[]>([]); | ||||
|     const [actions, setActions] = useState<UIAction[]>([]); | ||||
| 
 | ||||
|     const reloadForm = () => { | ||||
|         setEnabled(action?.enabled ?? true); | ||||
|         setName(action?.name || ''); | ||||
|         setValidated(false); | ||||
|         if (action?.actorId) { | ||||
|             setActorId(action?.actorId); | ||||
|         } | ||||
|         if (action?.match) { | ||||
|             const { sourceId, payload } = action.match; | ||||
|             setSourceId(sourceId); | ||||
|             setFilters( | ||||
|                 Object.entries(payload).map(([parameter, value]) => ({ | ||||
|                     id: uuidv4(), | ||||
|                     parameter, | ||||
|                     value: value as string, | ||||
|                 })), | ||||
|             ); | ||||
|         } | ||||
|         if (action?.actions) { | ||||
|             setActions( | ||||
|                 action.actions.map((action) => ({ | ||||
|                     id: uuidv4(), | ||||
|                     action: action.action, | ||||
|                     sortOrder: action.sortOrder, | ||||
|                     executionParams: action.executionParams, | ||||
|                 })), | ||||
|             ); | ||||
|         } | ||||
|         setErrors(DEFAULT_PROJECT_ACTIONS_FORM_ERRORS); | ||||
|     }; | ||||
| 
 | ||||
| @ -94,7 +126,7 @@ export const useProjectActionsForm = (action?: IActionSet) => { | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     const validateActions = (actions: IAction[]) => { | ||||
|     const validateActions = (actions: UIAction[]) => { | ||||
|         if (actions.length === 0) { | ||||
|             setError(ErrorField.ACTIONS, 'At least one action is required.'); | ||||
|             return false; | ||||
|  | ||||
| @ -83,10 +83,22 @@ export const ProjectActionsModal = ({ | ||||
|         match: { | ||||
|             source: 'incoming-webhook', | ||||
|             sourceId, | ||||
|             payload: filters, | ||||
|             payload: filters | ||||
|                 .filter((f) => f.parameter.length > 0) | ||||
|                 .reduce( | ||||
|                     (acc, filter) => ({ | ||||
|                         ...acc, | ||||
|                         [filter.parameter]: filter.value, | ||||
|                     }), | ||||
|                     {}, | ||||
|                 ), | ||||
|         }, | ||||
|         actorId, | ||||
|         actions, | ||||
|         actions: actions.map(({ action, sortOrder, executionParams }) => ({ | ||||
|             action, | ||||
|             sortOrder, | ||||
|             executionParams, | ||||
|         })), | ||||
|     }; | ||||
| 
 | ||||
|     const formatApiCode = () => `curl --location --request ${ | ||||
|  | ||||
| @ -19,10 +19,7 @@ export interface IMatch { | ||||
| } | ||||
| 
 | ||||
| export interface IAction { | ||||
|     id: number; | ||||
|     action: string; | ||||
|     sortOrder: number; | ||||
|     executionParams: Record<string, unknown>; | ||||
|     createdAt: string; | ||||
|     createdByUserId: number; | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user