diff --git a/front/src/modules/metadata/hooks/useFieldMetadata.ts b/front/src/modules/metadata/hooks/useFieldMetadata.ts index c174a7bbb..7e91e5d19 100644 --- a/front/src/modules/metadata/hooks/useFieldMetadata.ts +++ b/front/src/modules/metadata/hooks/useFieldMetadata.ts @@ -1,12 +1,28 @@ +import { ObjectFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType'; import { Field } from '~/generated/graphql'; +import { formatMetadataFieldInput } from '../utils/formatMetadataFieldInput'; + +import { useCreateOneMetadataField } from './useCreateOneMetadataField'; import { useDeleteOneMetadataField } from './useDeleteOneMetadataField'; import { useUpdateOneMetadataField } from './useUpdateOneMetadataField'; export const useFieldMetadata = () => { + const { createOneMetadataField } = useCreateOneMetadataField(); const { updateOneMetadataField } = useUpdateOneMetadataField(); const { deleteOneMetadataField } = useDeleteOneMetadataField(); + const createField = ( + input: Pick & { + objectId: string; + type: ObjectFieldDataType; + }, + ) => + createOneMetadataField({ + ...formatMetadataFieldInput(input), + objectId: input.objectId, + }); + const activateField = (metadataField: Field) => updateOneMetadataField({ fieldIdToUpdate: metadataField.id, @@ -24,6 +40,7 @@ export const useFieldMetadata = () => { return { activateField, + createField, disableField, eraseField, }; diff --git a/front/src/modules/metadata/utils/formatMetadataFieldInput.ts b/front/src/modules/metadata/utils/formatMetadataFieldInput.ts new file mode 100644 index 000000000..5e5ae8802 --- /dev/null +++ b/front/src/modules/metadata/utils/formatMetadataFieldInput.ts @@ -0,0 +1,17 @@ +import toCamelCase from 'lodash.camelcase'; +import upperFirst from 'lodash.upperfirst'; + +import { ObjectFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType'; +import { Field } from '~/generated-metadata/graphql'; + +export const formatMetadataFieldInput = ( + input: Pick & { + type: ObjectFieldDataType; + }, +) => ({ + description: input.description?.trim() ?? null, + icon: input.icon, + label: input.label.trim(), + name: upperFirst(toCamelCase(input.label.trim())), + type: input.type, +}); diff --git a/front/src/modules/metadata/utils/validateMetadataLabel.ts b/front/src/modules/metadata/utils/validateMetadataLabel.ts new file mode 100644 index 000000000..3aef4b866 --- /dev/null +++ b/front/src/modules/metadata/utils/validateMetadataLabel.ts @@ -0,0 +1,4 @@ +const metadataLabelValidationPattern = /^[a-zA-Z][a-zA-Z0-9 ]*$/; + +export const validateMetadataLabel = (value: string) => + !!value.match(metadataLabelValidationPattern); diff --git a/front/src/modules/metadata/utils/validateMetadataObjectLabel.ts b/front/src/modules/metadata/utils/validateMetadataObjectLabel.ts deleted file mode 100644 index be5c6560c..000000000 --- a/front/src/modules/metadata/utils/validateMetadataObjectLabel.ts +++ /dev/null @@ -1,4 +0,0 @@ -const metadataObjectLabelValidationPattern = /^[a-zA-Z][a-zA-Z0-9 ]*$/; - -export const validateMetadataObjectLabel = (value: string) => - !!value.match(metadataObjectLabelValidationPattern); diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldFormSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldFormSection.tsx index e01117548..9d13b73b9 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldFormSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldFormSection.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; +import { validateMetadataLabel } from '@/metadata/utils/validateMetadataLabel'; import { H2Title } from '@/ui/display/typography/components/H2Title'; import { IconPicker } from '@/ui/input/components/IconPicker'; import { TextArea } from '@/ui/input/components/TextArea'; @@ -13,8 +14,8 @@ type SettingsObjectFieldFormSectionProps = { iconKey?: string; onChange?: ( formValues: Partial<{ - iconKey: string; - name: string; + icon: string; + label: string; description: string; }>, ) => void; @@ -42,13 +43,17 @@ export const SettingsObjectFieldFormSection = ({ onChange?.({ iconKey: value.iconKey })} + onChange={(value) => onChange?.({ icon: value.iconKey })} variant="primary" /> onChange?.({ name: value })} + onChange={(value) => { + if (!value || validateMetadataLabel(value)) { + onChange?.({ label: value }); + } + }} disabled={disabled} fullWidth /> diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx index 3b9bf1b10..f13fabd67 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx @@ -10,6 +10,9 @@ type SettingsObjectFieldTypeSelectSectionProps = { onChange: (value: ObjectFieldDataType) => void; }; +// TODO: remove "relation" type for now, add it back when the backend is ready. +const { relation: _, ...dataTypesWithoutRelation } = dataTypes; + export const SettingsObjectFieldTypeSelectSection = ({ type, onChange, @@ -23,10 +26,12 @@ export const SettingsObjectFieldTypeSelectSection = ({ dropdownScopeId="object-field-type-select" value={type} onChange={onChange} - options={Object.entries(dataTypes).map(([key, dataType]) => ({ - value: key as ObjectFieldDataType, - ...dataType, - }))} + options={Object.entries(dataTypesWithoutRelation).map( + ([key, dataType]) => ({ + value: key as ObjectFieldDataType, + ...dataType, + }), + )} /> ); diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFormSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFormSection.tsx index 2d30fea59..741bac433 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFormSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFormSection.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { validateMetadataObjectLabel } from '@/metadata/utils/validateMetadataObjectLabel'; +import { validateMetadataLabel } from '@/metadata/utils/validateMetadataLabel'; import { H2Title } from '@/ui/display/typography/components/H2Title'; import { TextArea } from '@/ui/input/components/TextArea'; import { TextInput } from '@/ui/input/components/TextInput'; @@ -45,7 +45,7 @@ export const SettingsObjectFormSection = ({ placeholder="Investor" value={singularName} onChange={(value) => { - if (!value || validateMetadataObjectLabel(value)) { + if (!value || validateMetadataLabel(value)) { onChange?.({ labelSingular: value }); } }} @@ -57,7 +57,7 @@ export const SettingsObjectFormSection = ({ placeholder="Investors" value={pluralName} onChange={(value) => { - if (!value || validateMetadataObjectLabel(value)) { + if (!value || validateMetadataLabel(value)) { onChange?.({ labelPlural: value }); } }} diff --git a/front/src/modules/settings/data-model/constants/dataTypes.ts b/front/src/modules/settings/data-model/constants/dataTypes.ts index a74df5c44..05cbd200a 100644 --- a/front/src/modules/settings/data-model/constants/dataTypes.ts +++ b/front/src/modules/settings/data-model/constants/dataTypes.ts @@ -3,9 +3,7 @@ import { IconLink, IconNumbers, IconPlug, - IconSocial, IconTextSize, - IconUserCircle, } from '@/ui/display/icon'; import { IconComponent } from '@/ui/display/icon/types/IconComponent'; @@ -17,9 +15,7 @@ export const dataTypes: Record< > = { number: { label: 'Number', Icon: IconNumbers }, text: { label: 'Text', Icon: IconTextSize }, - link: { label: 'Link', Icon: IconLink }, - teammate: { label: 'Team member', Icon: IconUserCircle }, + url: { label: 'Link', Icon: IconLink }, boolean: { label: 'True/False', Icon: IconCheck }, relation: { label: 'Relation', Icon: IconPlug }, - social: { label: 'Social', Icon: IconSocial }, }; diff --git a/front/src/modules/settings/data-model/constants/mockObjects.ts b/front/src/modules/settings/data-model/constants/mockObjects.ts index bf1543809..8344b4f1b 100644 --- a/front/src/modules/settings/data-model/constants/mockObjects.ts +++ b/front/src/modules/settings/data-model/constants/mockObjects.ts @@ -1,42 +1,4 @@ -import { - IconBrandLinkedin, - IconBrandTwitter, - IconBuildingSkyscraper, - IconCurrencyDollar, - IconFreeRights, - IconGraph, - IconHeadphones, - IconLink, - IconLuggage, - IconMouse2, - IconPlane, - IconTarget, - IconUser, - IconUserCircle, - IconUsers, -} from '@/ui/display/icon'; - -import { ObjectFieldItem } from '../types/ObjectFieldItem'; - -export const activeObjectItems = [ - { - name: 'Companies', - singularName: 'Company', - Icon: IconBuildingSkyscraper, - type: 'standard', - fields: 23, - instances: 165, - description: 'Lorem ipsum', - }, - { - name: 'People', - singularName: 'Person', - Icon: IconUser, - type: 'standard', - fields: 16, - instances: 462, - }, -]; +import { IconMouse2 } from '@/ui/display/icon'; export const standardObjects = [ { @@ -52,92 +14,3 @@ export const standardObjects = [ description: 'Individuals who interact with your website', }, ]; - -export const disabledObjectItems = [ - { - name: 'Travels', - Icon: IconLuggage, - type: 'custom', - fields: 23, - instances: 165, - }, - { - name: 'Flights', - Icon: IconPlane, - type: 'custom', - fields: 23, - instances: 165, - }, -]; - -export const activeFieldItems: ObjectFieldItem[] = [ - { - name: 'People', - Icon: IconUser, - type: 'standard', - dataType: 'relation', - }, - { - name: 'URL', - Icon: IconLink, - type: 'standard', - dataType: 'text', - }, - { - name: 'Linkedin', - Icon: IconBrandLinkedin, - type: 'standard', - dataType: 'social', - }, - { - name: 'Account Owner', - Icon: IconUserCircle, - type: 'standard', - dataType: 'teammate', - }, - { - name: 'Employees', - Icon: IconUsers, - type: 'custom', - dataType: 'number', - }, -]; - -export const disabledFieldItems: ObjectFieldItem[] = [ - { - name: 'ICP', - Icon: IconTarget, - type: 'standard', - dataType: 'boolean', - }, - { - name: 'Twitter', - Icon: IconBrandTwitter, - type: 'standard', - dataType: 'social', - }, - { - name: 'Annual revenue', - Icon: IconCurrencyDollar, - type: 'standard', - dataType: 'number', - }, - { - name: 'Is public', - Icon: IconGraph, - type: 'standard', - dataType: 'boolean', - }, - { - name: 'Free tier?', - Icon: IconFreeRights, - type: 'custom', - dataType: 'boolean', - }, - { - name: 'Priority support', - Icon: IconHeadphones, - type: 'custom', - dataType: 'boolean', - }, -]; diff --git a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts index a7ad21314..4a262cfdd 100644 --- a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts +++ b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts @@ -1,8 +1,6 @@ export type ObjectFieldDataType = | 'boolean' - | 'link' | 'number' | 'relation' - | 'social' - | 'teammate' - | 'text'; + | 'text' + | 'url'; diff --git a/front/src/modules/settings/data-model/types/ObjectFieldItem.ts b/front/src/modules/settings/data-model/types/ObjectFieldItem.ts deleted file mode 100644 index 3e3b3db3f..000000000 --- a/front/src/modules/settings/data-model/types/ObjectFieldItem.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IconComponent } from '@/ui/display/icon/types/IconComponent'; - -import { ObjectFieldDataType } from './ObjectFieldDataType'; - -export type ObjectFieldItem = { - name: string; - Icon: IconComponent; - type: 'standard' | 'custom'; - dataType: ObjectFieldDataType; -}; diff --git a/front/src/modules/ui/display/icon/index.ts b/front/src/modules/ui/display/icon/index.ts index 66a372139..903be78d7 100644 --- a/front/src/modules/ui/display/icon/index.ts +++ b/front/src/modules/ui/display/icon/index.ts @@ -5,7 +5,6 @@ export { IconAlertTriangle, IconArchive, IconArchiveOff, - IconArrowBack, IconArrowDown, IconArrowLeft, IconArrowRight, @@ -17,7 +16,6 @@ export { IconBrandGithub, IconBrandGoogle, IconBrandLinkedin, - IconBrandTwitter, IconBrandX, IconBriefcase, IconBuildingSkyscraper, @@ -31,11 +29,9 @@ export { IconChevronsRight, IconChevronUp, IconCircleDot, - IconCirclePlus, IconColorSwatch, IconMessageCircle as IconComment, IconCopy, - IconCross, IconCurrencyDollar, IconDatabase, IconDotsVertical, @@ -45,15 +41,11 @@ export { IconFileImport, IconFileUpload, IconForbid, - IconFreeRights, - IconGraph, IconGripVertical, - IconHeadphones, IconHeart, IconHeartOff, IconHelpCircle, IconHierarchy2, - IconInbox, IconLayoutKanban, IconLayoutSidebarLeftCollapse, IconLayoutSidebarRightCollapse, @@ -62,7 +54,6 @@ export { IconLinkOff, IconList, IconLogout, - IconLuggage, IconMail, IconMap, IconMinus, @@ -72,7 +63,6 @@ export { IconNumbers, IconPencil, IconPhone, - IconPlane, IconPlug, IconPlus, IconProgressCheck, @@ -80,7 +70,6 @@ export { IconRobot, IconSearch, IconSettings, - IconSocial, IconTag, IconTarget, IconTargetArrow, diff --git a/front/src/pages/settings/data-model/SettingsNewObject.tsx b/front/src/pages/settings/data-model/SettingsNewObject.tsx index 1c07ad252..6deeca825 100644 --- a/front/src/pages/settings/data-model/SettingsNewObject.tsx +++ b/front/src/pages/settings/data-model/SettingsNewObject.tsx @@ -26,10 +26,10 @@ export const SettingsNewObject = () => { const [customFormValues, setCustomFormValues] = useState<{ description?: string; - icon?: string; + icon: string; labelPlural: string; labelSingular: string; - }>({ labelPlural: '', labelSingular: '' }); + }>({ icon: 'IconPigMoney', labelPlural: '', labelSingular: '' }); const canSave = selectedObjectType === 'Custom' && diff --git a/front/src/pages/settings/data-model/SettingsObjectDetail.tsx b/front/src/pages/settings/data-model/SettingsObjectDetail.tsx index 3b39dd07d..99425db09 100644 --- a/front/src/pages/settings/data-model/SettingsObjectDetail.tsx +++ b/front/src/pages/settings/data-model/SettingsObjectDetail.tsx @@ -75,7 +75,7 @@ export const SettingsObjectDetail = () => {
diff --git a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx index c1db3e37d..d63d55313 100644 --- a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { useFieldMetadata } from '@/metadata/hooks/useFieldMetadata'; +import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection'; import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection'; -import { activeObjectItems } from '@/settings/data-model/constants/mockObjects'; import { ObjectFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType'; import { AppPath } from '@/types/AppPath'; import { IconSettings } from '@/ui/display/icon'; @@ -16,23 +17,30 @@ import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; export const SettingsObjectNewFieldStep2 = () => { const navigate = useNavigate(); const { objectSlug = '' } = useParams(); - const activeObject = activeObjectItems.find( - (activeObject) => activeObject.name.toLowerCase() === objectSlug, - ); + const { findActiveObjectBySlug } = useObjectMetadata(); + const activeObject = findActiveObjectBySlug(objectSlug); + const { createField } = useFieldMetadata(); useEffect(() => { if (!activeObject) navigate(AppPath.NotFound); }, [activeObject, navigate]); - const [formValues, setFormValues] = useState< - Partial<{ - iconKey: string; - name: string; - description: string; - }> & { type: ObjectFieldDataType } - >({ type: 'number' }); + const [formValues, setFormValues] = useState<{ + description?: string; + icon: string; + label: string; + type: ObjectFieldDataType; + }>({ icon: 'IconUsers', label: '', type: 'number' }); - const canSave = !!formValues.name; + const canSave = !!formValues.label; + + const handleSave = async () => { + if (!activeObject) return; + + await createField({ ...formValues, objectId: activeObject.id }); + + navigate(`/settings/objects/${objectSlug}`); + }; return ( @@ -42,7 +50,7 @@ export const SettingsObjectNewFieldStep2 = () => { links={[ { children: 'Objects', href: '/settings/objects' }, { - children: activeObject?.name ?? '', + children: activeObject?.labelPlural ?? '', href: `/settings/objects/${objectSlug}`, }, { children: 'New Field' }, @@ -51,14 +59,14 @@ export const SettingsObjectNewFieldStep2 = () => { { - navigate('/settings/objects'); + navigate(`/settings/objects/${objectSlug}`); }} - onSave={() => undefined} + onSave={handleSave} /> setFormValues((previousValues) => ({