Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent creating a custom field with an existing name #6094

Closed
Tracked by #6218
lucasbordeau opened this issue Jul 2, 2024 · 1 comment · Fixed by #6100
Closed
Tracked by #6218

Prevent creating a custom field with an existing name #6094

lucasbordeau opened this issue Jul 2, 2024 · 1 comment · Fixed by #6100
Labels
good first issue Good for newcomers scope: front Issues that are affecting the frontend side only size: short

Comments

@lucasbordeau
Copy link
Contributor

Scope & Context

Field creation in data model.

Current behavior

We can input whatever name we want in the name input in field creation form.

Expected behavior

We want the field to become in error state with a message displayed that says that this name is already taken.

We should also deactivate the Save button to prevent from sending anything to the backend which would throw an error anyway.

@lucasbordeau lucasbordeau added good first issue Good for newcomers scope: front Issues that are affecting the frontend side only size: short labels Jul 2, 2024
Copy link
Contributor

greptile-apps bot commented Jul 2, 2024

To prevent creating a custom field with an existing name, follow these steps:

  1. Add validation logic in SettingsObjectNewFieldStep2.tsx:
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import { useApolloClient } from '@apollo/client';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import pick from 'lodash.pick';
import { H2Title, IconSettings } from 'twenty-ui';
import { z } from 'zod';

import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { View } from '@/views/types/View';
import { ViewType } from '@/views/types/ViewType';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';

const StyledSettingsObjectFieldTypeSelect = styled(
  SettingsDataModelFieldTypeSelect,
)`
  margin-bottom: ${({ theme }) => theme.spacing(4)};
`;

export const SettingsObjectNewFieldStep2 = () => {
  const navigate = useNavigate();
  const { objectSlug = '' } = useParams();
  const { enqueueSnackBar } = useSnackBar();

  const { findActiveObjectMetadataItemBySlug } =
    useFilteredObjectMetadataItems();

  const activeObjectMetadataItem =
    findActiveObjectMetadataItemBySlug(objectSlug);
  const { createMetadataField } = useFieldMetadataItem();

  const formConfig = useForm<SettingsDataModelNewFieldFormValues>({
    mode: 'onTouched',
    resolver: zodResolver(settingsFieldFormSchema),
  });

  useEffect(() => {
    if (!activeObjectMetadataItem) {
      navigate(AppPath.NotFound);
    }
  }, [activeObjectMetadataItem, navigate]);

  const [existingFieldNames, setExistingFieldNames] = useState<string[]>([]);

  useEffect(() => {
    if (activeObjectMetadataItem) {
      setExistingFieldNames(activeObjectMetadataItem.fields.map(field => field.label));
    }
  }, [activeObjectMetadataItem]);

  const validateFieldName = (name: string) => {
    return !existingFieldNames.includes(name) || 'This name is already taken';
  };

  const canSave =
    formConfig.formState.isValid && !formConfig.formState.isSubmitting;

  const handleSave = async (
    formValues: SettingsDataModelNewFieldFormValues,
  ) => {
    try {
      if (
        formValues.type === FieldMetadataType.Relation &&
        'relation' in formValues
      ) {
        const { relation: relationFormValues, ...fieldFormValues } = formValues;

        await createOneRelationMetadata({
          relationType: relationFormValues.type,
          field: pick(fieldFormValues, ['icon', 'label', 'description']),
          objectMetadataId: activeObjectMetadataItem.id,
          connect: {
            field: {
              icon: relationFormValues.field.icon,
              label: relationFormValues.field.label,
            },
            objectMetadataId: relationFormValues.objectMetadataId,
          },
        });

        await apolloClient.refetchQueries({
          include: ['FindManyViews', 'CombinedFindManyRecords'],
        });
      } else {
        await createMetadataField({
          ...formValues,
          objectMetadataId: activeObjectMetadataItem.id,
        });

        await apolloClient.refetchQueries({
          include: ['FindManyViews', 'CombinedFindManyRecords'],
        });
      }

      navigate(`/settings/objects/${objectSlug}`);
    } catch (error) {
      enqueueSnackBar((error as Error).message, {
        variant: SnackBarVariant.Error,
      });
    }
  };

  const excludedFieldTypes: SettingsSupportedFieldType[] = (
    [
      FieldMetadataType.Numeric,
      FieldMetadataType.Probability,
    ] as const
  ).filter(isDefined);

  return (
    <RecordFieldValueSelectorContextProvider>
      <FormProvider {...formConfig}>
        <SubMenuTopBarContainer Icon={IconSettings} title="Settings">
          <SettingsPageContainer>
            <SettingsHeaderContainer>
              <Breadcrumb
                links={[
                  { children: 'Objects', href: '/settings/objects' },
                  {
                    children: activeObjectMetadataItem.labelPlural,
                    href: `/settings/objects/${objectSlug}`,
                  },
                  { children: 'New Field' },
                ]}
              />
              {!activeObjectMetadataItem.isRemote && (
                <SaveAndCancelButtons
                  isSaveDisabled={!canSave}
                  onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
                  onSave={formConfig.handleSubmit(handleSave)}
                />
              )}
            </SettingsHeaderContainer>
            <Section>
              <H2Title
                title="Name and description"
                description="The name and description of this field"
              />
              <SettingsDataModelFieldAboutForm
                validateFieldName={validateFieldName}
              />
            </Section>
            <Section>
              <H2Title
                title="Type and values"
                description="The field's type and values."
              />
              <StyledSettingsObjectFieldTypeSelect
                excludedFieldTypes={excludedFieldTypes}
              />
              <SettingsDataModelFieldSettingsFormCard
                fieldMetadataItem={{
                  icon: formConfig.watch('icon'),
                  label: formConfig.watch('label') || 'Employees',
                  type: formConfig.watch('type'),
                }}
                objectMetadataItem={activeObjectMetadataItem}
              />
            </Section>
          </SettingsPageContainer>
        </SubMenuTopBarContainer>
      </FormProvider>
    </RecordFieldValueSelectorContextProvider>
  );
};
  1. Update SettingsDataModelFieldAboutForm to accept validateFieldName prop and use it for validation:
import { useFormContext } from 'react-hook-form';

export const SettingsDataModelFieldAboutForm = ({ validateFieldName }) => {
  const { register, formState: { errors } } = useFormContext();

  return (
    <div>
      <input
        {...register('label', { validate: validateFieldName })}
        placeholder="Field Name"
      />
      {errors.label && <span>{errors.label.message}</span>}
    </div>
  );
};

This will ensure that the form displays an error message if the field name already exists and disables the Save button to prevent submission.

References

/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx
/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField
/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx

About Greptile

This response provides a starting point for your research, not a precise solution.

Help us improve! Please leave a 👍 if this is helpful and 👎 if it is irrelevant.

Ask Greptile · Edit Issue Bot Settings

ijreilly added a commit that referenced this issue Jul 17, 2024
Fixes #6094 
Description: Added logic inside SettingsObjectNewFieldStep2.tsx to
prevent form submission
Current Behaviours:
<img width="947" alt="Screenshot 2024-07-03 at 1 45 31 PM"
src="https://github.com/twentyhq/twenty/assets/95612797/bef54bc4-fc83-48f3-894a-34445ec64723">

---------

Co-authored-by: Marie Stoppa <[email protected]>
@github-project-automation github-project-automation bot moved this from 🆕 New to ✅ Done in Product development ✅ Jul 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers scope: front Issues that are affecting the frontend side only size: short
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

1 participant