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:
@ -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 (
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -60,7 +60,7 @@ export const NumberFieldInput = ({
|
||||
<TextInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={draftValue ?? ''}
|
||||
value={draftValue?.toString() ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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 = {
|
||||
|
||||
@ -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': {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
{...{
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -45,7 +45,7 @@ export const SettingsServerlessFunctionsNew = () => {
|
||||
};
|
||||
|
||||
const onChange = (key: string) => {
|
||||
return (value: string | undefined) => {
|
||||
return (value: string) => {
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
[key]: value,
|
||||
|
||||
13
packages/twenty-front/src/utils/format/spiltFullName.ts
Normal file
13
packages/twenty-front/src/utils/format/spiltFullName.ts
Normal 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(), ''];
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export const turnIntoEmptyStringIfWhitespacesOnly = (value: string): string => {
|
||||
return value.trim().length > 0 ? value : '';
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
export const turnIntoUndefinedIfWhitespacesOnly = (
|
||||
value: string,
|
||||
): string | undefined => {
|
||||
return value.trim() === '' ? undefined : value.trim();
|
||||
};
|
||||
Reference in New Issue
Block a user