import * as React from 'react'; import I18n from 'i18n-js'; import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form'; import { IWebhook, WEBHOOK_HTTP_METHOD_DELETE, WEBHOOK_HTTP_METHOD_PATCH, WEBHOOK_HTTP_METHOD_POST, WEBHOOK_HTTP_METHOD_PUT, WEBHOOK_TRIGGER_DELETED_POST, WEBHOOK_TRIGGER_NEW_COMMENT, WEBHOOK_TRIGGER_NEW_POST, WEBHOOK_TRIGGER_NEW_POST_PENDING_APPROVAL, WEBHOOK_TRIGGER_NEW_USER, WEBHOOK_TRIGGER_NEW_VOTE, WEBHOOK_TRIGGER_POST_STATUS_CHANGE, WebhookHttpMethod, WebhookTrigger } from '../../../interfaces/IWebhook'; import { WebhookPages } from './WebhooksSiteSettingsP'; import ActionLink from '../../common/ActionLink'; import { AddIcon, BackIcon, DeleteIcon, LiquidIcon, PreviewIcon } from '../../common/Icons'; import { getLabel, getValidationMessage } from '../../../helpers/formUtils'; import { DangerText } from '../../common/CustomTexts'; import Button from '../../common/Button'; import { URL_REGEX_WHITESPACE_ALLOWED } from '../../../constants/regex'; import Spinner from '../../common/Spinner'; import buildRequestHeaders from '../../../helpers/buildRequestHeaders'; import HttpStatus from '../../../constants/http_status'; import { useRef, useState } from 'react'; import TemplateVariablesSelector from './TemplateVariablesSelector'; interface Props { isSubmitting: boolean; submitError: string; selectedWebhook: IWebhook; page: WebhookPages; setPage: React.Dispatch>; handleSubmitWebhook(webhook: IWebhook): void; handleUpdateWebhook(id: number, form: ISiteSettingsWebhookFormUpdate): void; authenticityToken: string; } interface ISiteSettingsWebhookFormBase { name: string; description: string; trigger: string; url: string; httpBody: string; httpMethod: string; } interface ISiteSettingsWebhookForm extends ISiteSettingsWebhookFormBase { httpHeaders: Array<{ key: string, value: string }>; } export interface ISiteSettingsWebhookFormUpdate extends ISiteSettingsWebhookFormBase { httpHeaders: string; } // This method tries to parse httpHeaders JSON, otherwise returns [{ key: '', value: '' }] const parseHttpHeaders = (httpHeaders: string) => { try { return JSON.parse(httpHeaders); } catch (e) { return [{ key: '', value: '' }]; } } const WebhookFormPage = ({ isSubmitting, submitError, selectedWebhook, page, setPage, handleSubmitWebhook, handleUpdateWebhook, authenticityToken, }: Props) => { const { register, handleSubmit, control, formState: { errors, isDirty }, watch, getValues, setValue, } = useForm({ defaultValues: page === 'new' ? { name: '', description: '', trigger: WEBHOOK_TRIGGER_NEW_POST, url: '', httpBody: '', httpMethod: WEBHOOK_HTTP_METHOD_POST, httpHeaders: [{ key: '', value: '' }], } : { name: selectedWebhook.name, description: selectedWebhook.description, trigger: selectedWebhook.trigger, url: selectedWebhook.url, httpBody: selectedWebhook.httpBody, httpMethod: selectedWebhook.httpMethod, httpHeaders: parseHttpHeaders(selectedWebhook.httpHeaders), } }); const { fields, append, remove } = useFieldArray({ control, name: 'httpHeaders', // The name of the httpHeaders field }); const onSubmit: SubmitHandler = data => { // Remove empty headers let httpHeaders = data.httpHeaders.filter(header => header.key !== '' && header.value !== ''); const webhook = { isEnabled: false, name: data.name, description: data.description, trigger: data.trigger as WebhookTrigger, url: data.url.replace(/\s/g, ''), httpBody: data.httpBody, httpMethod: data.httpMethod as WebhookHttpMethod, httpHeaders: JSON.stringify(httpHeaders), }; if (page === 'new') { handleSubmitWebhook(webhook); } else if (page === 'edit') { handleUpdateWebhook(selectedWebhook.id, webhook); } }; const trigger = watch('trigger'); const url = watch('url'); const httpBody = watch('httpBody'); const httpBodyTextAreaRef = useRef(null); const [cursorPosition, setCursorPosition] = React.useState(0); const handleCursorPosition = e => { setCursorPosition(e.target.selectionStart); }; // Insert custom string at the last cursor position const insertString = (stringToInsert: string) => { const currentValue = getValues('httpBody'); // Get the current textarea value const start = currentValue.slice(0, cursorPosition); const end = currentValue.slice(cursorPosition); const newValue = start + stringToInsert + end; // Update textarea value with react-hook-form setValue('httpBody', newValue, { shouldDirty: true }); setIsPreviewOutdated(true); // Update cursor position after the custom string const newCursorPosition = cursorPosition + stringToInsert.length; setCursorPosition(newCursorPosition); // Update the DOM to reflect the cursor position if (httpBodyTextAreaRef.current) { setTimeout(() => { httpBodyTextAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition); httpBodyTextAreaRef.current.focus(); }, 0); } }; // State for URL and body preview const [isPreviewVisible, setIsPreviewVisible] = useState(false); const [previewContent, setPreviewContent] = useState(''); const [isPreviewOutdated, setIsPreviewOutdated] = useState(true); return ( <> { let confirmation = true; if (isDirty) confirmation = confirm(I18n.t('common.unsaved_changes') + ' ' + I18n.t('common.confirmation')); if (confirmation) setPage('index'); }} icon={} customClass="backButton" > {I18n.t('common.buttons.back')}

{ I18n.t(`site_settings.webhooks.form.title_${page}`) }

{errors.name?.type === 'required' && getValidationMessage(errors.name.type, 'webhook', 'name')} {errors.name?.type === 'maxLength' && (getLabel('webhook', 'name') + ' ' + I18n.t('activerecord.errors.messages.too_long', { count: 255 }))}
{errors.description?.type === 'maxLength' && (getLabel('webhook', 'description') + ' ' + I18n.t('activerecord.errors.messages.too_long', { count: 255 }))}
{errors.trigger && getValidationMessage(errors.trigger.type, 'webhook', 'trigger')}
setIsPreviewOutdated(true), })} autoComplete="off" id="url" className="formControl" /> {errors.url?.type === 'required' && getValidationMessage(errors.url.type, 'webhook', 'url')} {errors.url?.type === 'pattern' && I18n.t('common.validations.url')}