fix: Input fields to have expected behaviour in case of empty / only whitespaces string (#6736)

# ISSUE
- Closes #6734
- Closes #6633
- Closes #6733
- Closes #6816

# Description

- [x] Don't allow Empty (whitespaces) Objects to Create, all the
keyboard shortcuts are also handled for this.



https://github.com/user-attachments/assets/1c9add4e-f13f-458b-8f76-63bd868413a2



https://github.com/user-attachments/assets/e72b6ee3-74e4-4517-a230-3eb10db80dc7

Note: we do have one other issue with FullName field #6740 Inorder to
test use **shift**.




- [x] Api Keys Input Name Field -> New and Detail View Name field
shouldn't be empty or string with whitespaces, we won't able to have
whitespaces in both. **Try Entering just spaces**



https://github.com/user-attachments/assets/b521b49f-648c-4585-9d15-8ff4faed3c3a


- [x] Similar to above, Empty webhook endpoint url under
**/settings/developers/webhooks/new** won't be created. **Try Entering
just spaces**

- [x] New Functions or Updating Functions will not able to have
whitespaces, empty string as Name. **Try Entering just spaces**



https://github.com/user-attachments/assets/09fcf394-c6d9-4080-8efd-462b054a22d0



- [x] under **settings/workspace-members** changes will lead and solve
that user won't be able to enter Invite by email as just whitespaces +
button is now getting disabled when there is no correct email. **Try
Entering just spaces**



https://github.com/user-attachments/assets/b352edfa-113b-4645-80fd-db6f120ab5db



- [x] Text Input Field, will not allow to start entering with
whitespaces and won't take just whitespaces as value spaces between
words will work.


https://github.com/user-attachments/assets/8c1a0812-45be-4ed2-bd3d-bb4f92147976

- [x] Similarly Number works as per above including shortcuts. 


https://github.com/user-attachments/assets/9f69cc87-5c3c-43ee-93c4-fa887bc0d7ee


- [x] Similarly FullName field works as per above including shortcuts



https://github.com/user-attachments/assets/7bb006b2-abf7-44cd-a214-7a2fc68df169

- [x] Pasting fullName is been Improved. 

- Case 1 (Two Words): If there are exactly two words, return them as is.
- Case 2 (More than Two Words): If there are more than two words, return
the first two words only.
- Case 3 (One Word): If there is only one word, return it as the first
name, with an empty string as the last name.
- WhiteSpaces have been handled. 

```
console.log(splitFullName("John     Doe")); // ["John", "Doe"]
console.log(splitFullName("        ")); // ["", ""]
console.log(splitFullName("John")); // ["John", ""]
console.log(splitFullName("  John   Doe  ")); // ["John", "Doe"]
console.log(splitFullName("John Michael  Andrew Doe")); // ["John", "Michael"]
```

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Nabhag Motivaras
2024-09-21 00:55:50 +05:30
committed by GitHub
parent c8c1890ad7
commit c97c71762e
22 changed files with 249 additions and 187 deletions

View File

@ -3,8 +3,7 @@ import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleT
import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput';
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
import { usePersistField } from '../../../hooks/usePersistField';
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
import { FieldInputEvent } from './DateTimeFieldInput';
const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
@ -28,46 +27,55 @@ export const FullNameFieldInput = ({
onTab,
onShiftTab,
}: FullNameFieldInputProps) => {
const { hotkeyScope, draftValue, setDraftValue } = useFullNameField();
const persistField = usePersistField();
const { hotkeyScope, draftValue, setDraftValue, persistFullNameField } =
useFullNameField();
const convertToFullName = (newDoubleText: FieldDoubleText) => {
return {
firstName: newDoubleText.firstValue,
lastName: newDoubleText.secondValue,
firstName: newDoubleText.firstValue.trim(),
lastName: newDoubleText.secondValue.trim(),
};
};
const getRequiredDraftValueFromDoubleText = (
newDoubleText: FieldDoubleText,
) => {
return isDoubleTextFieldEmpty(newDoubleText)
? undefined
: convertToFullName(newDoubleText);
};
const handleEnter = (newDoubleText: FieldDoubleText) => {
onEnter?.(() => persistField(convertToFullName(newDoubleText)));
onEnter?.(() => persistFullNameField(convertToFullName(newDoubleText)));
};
const handleEscape = (newDoubleText: FieldDoubleText) => {
onEscape?.(() => persistField(convertToFullName(newDoubleText)));
onEscape?.(() => persistFullNameField(convertToFullName(newDoubleText)));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newDoubleText: FieldDoubleText,
) => {
onClickOutside?.(() => persistField(convertToFullName(newDoubleText)));
onClickOutside?.(() =>
persistFullNameField(convertToFullName(newDoubleText)),
);
};
const handleTab = (newDoubleText: FieldDoubleText) => {
onTab?.(() => persistField(convertToFullName(newDoubleText)));
onTab?.(() => persistFullNameField(convertToFullName(newDoubleText)));
};
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
onShiftTab?.(() => persistField(convertToFullName(newDoubleText)));
onShiftTab?.(() => persistFullNameField(convertToFullName(newDoubleText)));
};
const handleChange = (newDoubleText: FieldDoubleText) => {
setDraftValue(convertToFullName(newDoubleText));
setDraftValue(getRequiredDraftValueFromDoubleText(newDoubleText));
};
const handlePaste = (newDoubleText: FieldDoubleText) => {
setDraftValue(convertToFullName(newDoubleText));
setDraftValue(getRequiredDraftValueFromDoubleText(newDoubleText));
};
return (

View File

@ -16,6 +16,7 @@ import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/Men
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
const StyledRelationPickerContainer = styled.div`
left: -1px;
@ -109,7 +110,11 @@ export const MultiSelectFieldInput = ({
<DropdownMenu data-select-disable>
<DropdownMenuSearchInput
value={searchFilter}
onChange={(event) => setSearchFilter(event.currentTarget.value)}
onChange={(event) =>
setSearchFilter(
turnIntoEmptyStringIfWhitespacesOnly(event.currentTarget.value),
)
}
autoFocus
/>
<DropdownMenuSeparator />

View File

@ -60,7 +60,7 @@ export const NumberFieldInput = ({
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={draftValue ?? ''}
value={draftValue?.toString() ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}

View File

@ -4,6 +4,7 @@ import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
import { FieldInputEvent } from './DateTimeFieldInput';
export type TextFieldInputProps = {
@ -25,32 +26,31 @@ export const TextFieldInput = ({
useTextField();
const persistField = usePersistField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistField(newText));
onEnter?.(() => persistField(newText.trim()));
};
const handleEscape = (newText: string) => {
onEscape?.(() => persistField(newText));
onEscape?.(() => persistField(newText.trim()));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistField(newText));
onClickOutside?.(() => persistField(newText.trim()));
};
const handleTab = (newText: string) => {
onTab?.(() => persistField(newText));
onTab?.(() => persistField(newText.trim()));
};
const handleShiftTab = (newText: string) => {
onShiftTab?.(() => persistField(newText));
onShiftTab?.(() => persistField(newText.trim()));
};
const handleChange = (newText: string) => {
setDraftValue(newText);
setDraftValue(turnIntoUndefinedIfWhitespacesOnly(newText));
};
return (

View File

@ -0,0 +1,9 @@
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
export const isDoubleTextFieldEmpty = (doubleText: FieldDoubleText) => {
const { firstValue, secondValue } = doubleText;
const totalLength = firstValue.trim().length + secondValue.trim().length;
return totalLength > 0 ? false : true;
};

View File

@ -25,7 +25,7 @@ import {
} from '@/object-record/record-field/types/FieldMetadata';
export type FieldTextDraftValue = string;
export type FieldNumberDraftValue = string;
export type FieldNumberDraftValue = number;
export type FieldDateTimeDraftValue = string;
export type FieldPhoneDraftValue = string;
export type FieldPhonesDraftValue = {

View File

@ -1,5 +1,5 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useRef } from 'react';
import { Fragment, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconComponent, IconPlus } from 'twenty-ui';
@ -158,16 +158,15 @@ export const SingleEntitySelectMenuItems = ({
switch (entity.id) {
case 'add-new': {
return (
<>
<Fragment key={entity.id}>
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
<CreateNewButton
key={entity.id}
onClick={onCreate}
LeftIcon={IconPlus}
text="Add New"
hovered={isSelectedAddNewButton}
/>
</>
</Fragment>
);
}
case 'select-none': {

View File

@ -1,10 +1,11 @@
import { useCallback, useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { useCallback, useEffect, useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { TextInput } from '@/ui/input/components/TextInput';
import isEmpty from 'lodash.isempty';
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -40,6 +41,7 @@ export const NameField = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedUpdate = useCallback(
useDebouncedCallback(async (name: string) => {
if (isEmpty(name)) return;
// update local recoil state when workspace name is updated
setCurrentWorkspace((currentValue) => {
if (currentValue === null) {

View File

@ -13,6 +13,8 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
import { splitFullName } from '~/utils/format/spiltFullName';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { StyledTextInput } from './TextInput';
const StyledContainer = styled.div`
@ -167,9 +169,12 @@ export const DoubleTextInput = ({
const name = event.clipboardData.getData('Text');
const splittedName = name.split(' ');
const splittedName = splitFullName(name);
onPaste?.({ firstValue: splittedName[0], secondValue: splittedName[1] });
onPaste?.({
firstValue: splittedName[0],
secondValue: splittedName[1],
});
};
const handleClickToPreventParentClickEvents = (
@ -189,7 +194,10 @@ export const DoubleTextInput = ({
placeholder={firstValuePlaceholder}
value={firstInternalValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
handleChange(event.target.value, secondInternalValue);
handleChange(
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
secondInternalValue,
);
}}
onPaste={(event: ClipboardEvent<HTMLInputElement>) =>
handleOnPaste(event)
@ -203,7 +211,10 @@ export const DoubleTextInput = ({
placeholder={secondValuePlaceholder}
value={secondInternalValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
handleChange(firstInternalValue, event.target.value);
handleChange(
firstInternalValue,
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
);
}}
onClick={handleClickToPreventParentClickEvents}
/>

View File

@ -1,11 +1,12 @@
import styled from '@emotion/styled';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import styled from '@emotion/styled';
import { TEXT_INPUT_STYLE } from 'twenty-ui';
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { isDefined } from '~/utils/isDefined';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
export type TextAreaInputProps = {
disabled?: boolean;
@ -67,10 +68,12 @@ export const TextAreaInput = ({
copyButton = true,
}: TextAreaInputProps) => {
const [internalText, setInternalText] = useState(value);
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setInternalText(event.target.value);
onChange?.(event.target.value);
const targetValue = turnIntoEmptyStringIfWhitespacesOnly(
event.target.value,
);
setInternalText(targetValue);
onChange?.(targetValue);
};
const wrapperRef = useRef<HTMLTextAreaElement>(null);

View File

@ -1,5 +1,5 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { TEXT_INPUT_STYLE } from 'twenty-ui';
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
@ -44,12 +44,11 @@ export const TextInput = ({
const copyRef = useRef<HTMLDivElement>(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setInternalText(event.target.value);
onChange?.(event.target.value);
setInternalText(event.target.value.trim());
onChange?.(event.target.value.trim());
};
useEffect(() => {
setInternalText(value);
setInternalText(value.trim());
}, [value]);
useRegisterInputEvents({

View File

@ -1,9 +1,10 @@
import styled from '@emotion/styled';
import { FocusEventHandler } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import styled from '@emotion/styled';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { InputHotkeyScope } from '../types/InputHotkeyScope';
const MAX_ROWS = 5;
@ -75,7 +76,9 @@ export const TextArea = ({
maxRows={MAX_ROWS}
minRows={computedMinRows}
value={value}
onChange={(event) => onChange?.(event.target.value)}
onChange={(event) =>
onChange?.(turnIntoEmptyStringIfWhitespacesOnly(event.target.value))
}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}

View File

@ -11,6 +11,7 @@ import {
} from 'react';
import { IconComponent, IconEye, IconEyeOff } from 'twenty-ui';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
const StyledContainer = styled.div<
Pick<TextInputV2ComponentProps, 'fullWidth'>
@ -180,7 +181,7 @@ const TextInputV2Component = (
</StyledLeftIconContainer>
)}
<StyledInput
data-testId={dataTestId}
data-testid={dataTestId}
autoComplete={autoComplete || 'off'}
ref={combinedRef}
tabIndex={tabIndex ?? 0}
@ -188,7 +189,9 @@ const TextInputV2Component = (
onBlur={onBlur}
type={passwordVisible ? 'text' : type}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange?.(event.target.value);
onChange?.(
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
);
}}
onKeyDown={onKeyDown}
{...{

View File

@ -30,7 +30,6 @@ const StyledTopBarContainer = styled.div<{ width?: number }>`
padding: ${({ theme }) => theme.spacing(2)};
padding-left: 0;
padding-right: ${({ theme }) => theme.spacing(3)};
z-index: 20;
width: ${({ width }) => width + 'px' || '100%'};
@media (max-width: ${MOBILE_VIEWPORT}px) {

View File

@ -72,13 +72,16 @@ export const WorkspaceInviteTeam = () => {
const { enqueueSnackBar } = useSnackBar();
const { sendInvitation } = useCreateWorkspaceInvitation();
const { reset, handleSubmit, control, formState } = useForm<FormInput>({
mode: 'onSubmit',
resolver: zodResolver(validationSchema()),
defaultValues: {
emails: '',
const { reset, handleSubmit, control, formState, watch } = useForm<FormInput>(
{
mode: 'onSubmit',
resolver: zodResolver(validationSchema()),
defaultValues: {
emails: '',
},
},
});
);
const isEmailsEmpty = !watch('emails');
const submit = handleSubmit(async ({ emails }) => {
const emailsList = sanitizeEmailList(emails.split(','));
@ -109,7 +112,7 @@ export const WorkspaceInviteTeam = () => {
}
};
const { isSubmitSuccessful } = formState;
const { isSubmitSuccessful, errors } = formState;
useEffect(() => {
if (isSubmitSuccessful) {
@ -144,6 +147,7 @@ export const WorkspaceInviteTeam = () => {
accent="blue"
title="Invite"
type="submit"
disabled={isEmailsEmpty || !!errors.emails}
/>
</StyledContainer>
</form>

View File

@ -87,125 +87,116 @@ export const SettingsDevelopersWebhooksDetail = () => {
navigate(developerPath);
};
if (!webhookData?.targetUrl) {
return <></>;
}
return (
<>
{webhookData?.targetUrl && (
<SubMenuTopBarContainer
Icon={IconCode}
title={webhookData.targetUrl}
links={[
{
children: 'Workspace',
href: getSettingsPagePath(SettingsPath.Workspace),
},
{
children: 'Developers',
href: developerPath,
},
{ children: 'Webhook' },
]}
actionButton={
<SaveAndCancelButtons
isSaveDisabled={!isDirty}
onCancel={() => {
navigate(developerPath);
<SubMenuTopBarContainer
Icon={IconCode}
title={webhookData.targetUrl}
links={[
{
children: 'Workspace',
href: getSettingsPagePath(SettingsPath.Workspace),
},
{
children: 'Developers',
href: developerPath,
},
{ children: 'Webhook' },
]}
actionButton={
<SaveAndCancelButtons
isSaveDisabled={!isDirty}
onCancel={() => {
navigate(developerPath);
}}
onSave={handleSave}
/>
}
>
<SettingsPageContainer>
<Section>
<H2Title
title="Endpoint URL"
description="We will send POST requests to this endpoint for every new event"
/>
<TextInput
placeholder="URL"
value={webhookData.targetUrl}
disabled
fullWidth
/>
</Section>
<Section>
<H2Title title="Description" description="An optional description" />
<TextArea
placeholder="Write a description"
minRows={4}
value={description}
onChange={(description) => {
setDescription(description);
setIsDirty(true);
}}
/>
</Section>
<Section>
<H2Title
title="Filters"
description="Select the event you wish to send to this endpoint"
/>
<StyledFilterRow>
<Select
fullWidth
dropdownId="object-webhook-type-select"
value={operationObjectSingularName}
onChange={(objectSingularName) => {
setIsDirty(true);
setOperationObjectSingularName(objectSingularName);
}}
onSave={handleSave}
options={fieldTypeOptions}
/>
}
>
<SettingsPageContainer>
<Section>
<H2Title
title="Endpoint URL"
description="We will send POST requests to this endpoint for every new event"
/>
<TextInput
placeholder="URL"
value={webhookData.targetUrl}
disabled
fullWidth
/>
</Section>
<Section>
<H2Title
title="Description"
description="An optional description"
/>
<TextArea
placeholder="Write a description"
minRows={4}
value={description}
onChange={(description) => {
setDescription(description);
setIsDirty(true);
}}
/>
</Section>
<Section>
<H2Title
title="Filters"
description="Select the event you wish to send to this endpoint"
/>
<StyledFilterRow>
<Select
fullWidth
dropdownId="object-webhook-type-select"
value={operationObjectSingularName}
onChange={(objectSingularName) => {
setIsDirty(true);
setOperationObjectSingularName(objectSingularName);
}}
options={fieldTypeOptions}
/>
<Select
fullWidth
dropdownId="operation-webhook-type-select"
value={operationAction}
onChange={(operationAction) => {
setIsDirty(true);
setOperationAction(operationAction);
}}
options={[
{ value: '*', label: 'All Actions' },
{ value: 'create', label: 'Create' },
{ value: 'update', label: 'Update' },
{ value: 'delete', label: 'Delete' },
]}
/>
</StyledFilterRow>
</Section>
<Section>
<H2Title
title="Danger zone"
description="Delete this integration"
/>
<Button
accent="danger"
variant="secondary"
title="Delete"
Icon={IconTrash}
onClick={() => setIsDeleteWebhookModalOpen(true)}
/>
<ConfirmationModal
confirmationPlaceholder="yes"
confirmationValue="yes"
isOpen={isDeleteWebhookModalOpen}
setIsOpen={setIsDeleteWebhookModalOpen}
title="Delete webhook"
subtitle={
<>
Please type "yes" to confirm you want to delete this
webhook.
</>
}
onConfirmClick={deleteWebhook}
deleteButtonText="Delete webhook"
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
)}
</>
<Select
fullWidth
dropdownId="operation-webhook-type-select"
value={operationAction}
onChange={(operationAction) => {
setIsDirty(true);
setOperationAction(operationAction);
}}
options={[
{ value: '*', label: 'All Actions' },
{ value: 'create', label: 'Create' },
{ value: 'update', label: 'Update' },
{ value: 'delete', label: 'Delete' },
]}
/>
</StyledFilterRow>
</Section>
<Section>
<H2Title title="Danger zone" description="Delete this integration" />
<Button
accent="danger"
variant="secondary"
title="Delete"
Icon={IconTrash}
onClick={() => setIsDeleteWebhookModalOpen(true)}
/>
<ConfirmationModal
confirmationPlaceholder="yes"
confirmationValue="yes"
isOpen={isDeleteWebhookModalOpen}
setIsOpen={setIsDeleteWebhookModalOpen}
title="Delete webhook"
subtitle={
<>Please type "yes" to confirm you want to delete this webhook.</>
}
onConfirmClick={deleteWebhook}
deleteButtonText="Delete webhook"
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { H2Title, IconCode } from 'twenty-ui';
import { H2Title, IconCode, isDefined } from 'twenty-ui';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
@ -45,7 +45,22 @@ export const SettingsDevelopersWebhooksNew = () => {
};
const canSave =
!!formValues.targetUrl && isTargetUrlValid && createOneWebhook;
!!formValues.targetUrl && isTargetUrlValid && isDefined(createOneWebhook);
// TODO: refactor use useScopedHotkeys
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && canSave) {
handleSave();
}
};
const handleChange = (value: string) => {
setFormValues((prevState) => ({
...prevState,
targetUrl: value,
}));
handleValidate(value);
};
return (
<SubMenuTopBarContainer
@ -82,18 +97,8 @@ export const SettingsDevelopersWebhooksNew = () => {
placeholder="URL"
value={formValues.targetUrl}
error={!isTargetUrlValid ? 'Please enter a valid URL' : undefined}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSave();
}
}}
onChange={(value) => {
setFormValues((prevState) => ({
...prevState,
targetUrl: value,
}));
handleValidate(value);
}}
onKeyDown={handleKeyDown}
onChange={handleChange}
fullWidth
/>
</Section>

View File

@ -72,7 +72,7 @@ export const SettingsServerlessFunctionDetail = () => {
const handleSave = usePreventOverlapCallback(save, 1000);
const onChange = (key: string) => {
return async (value: string | undefined) => {
return async (value: string) => {
setFormValues((prevState) => ({
...prevState,
[key]: value,

View File

@ -45,7 +45,7 @@ export const SettingsServerlessFunctionsNew = () => {
};
const onChange = (key: string) => {
return (value: string | undefined) => {
return (value: string) => {
setFormValues((prevState) => ({
...prevState,
[key]: value,

View File

@ -0,0 +1,13 @@
export const splitFullName = (name: string) => {
const splittedName = name.trim().split(/\s+/);
if (splittedName.length === 2) {
return splittedName;
}
if (splittedName.length > 2) {
return [splittedName[0].trim(), splittedName[1].trim()];
}
return [name.trim(), ''];
};

View File

@ -0,0 +1,3 @@
export const turnIntoEmptyStringIfWhitespacesOnly = (value: string): string => {
return value.trim().length > 0 ? value : '';
};

View File

@ -0,0 +1,5 @@
export const turnIntoUndefinedIfWhitespacesOnly = (
value: string,
): string | undefined => {
return value.trim() === '' ? undefined : value.trim();
};