Skip to content

Commit

Permalink
Merge pull request #10079 from marmelab/feat/crm-improve-contact-form
Browse files Browse the repository at this point in the history
Feat(crm): Display Contact inputs by category in columns and improve UX form
  • Loading branch information
arnault-dev authored Jul 26, 2024
2 parents 8c4a161 + 5b890f9 commit f52c583
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 109 deletions.
34 changes: 16 additions & 18 deletions examples/crm/src/contacts/ContactAside.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,33 +66,31 @@ export const ContactAside = ({ link = 'edit' }: { link?: 'edit' | 'show' }) => {
/>
</Stack>
)}
{record.phone_number1 && (
{record.phone_number1.number && (
<Stack direction="row" alignItems="center" gap={1}>
<PhoneIcon color="disabled" fontSize="small" />
<Box>
<TextField source="phone_number1" />{' '}
<Typography
variant="body2"
color="textSecondary"
component="span"
>
Work
</Typography>
<TextField source="phone_number1.number" />{' '}
{record.phone_number1.type !== 'Other' && (
<TextField
source="phone_number1.type"
color="textSecondary"
/>
)}
</Box>
</Stack>
)}
{record.phone_number2 && (
{record.phone_number2.number && (
<Stack direction="row" alignItems="center" gap={1}>
<PhoneIcon color="disabled" fontSize="small" />
<Box>
<TextField source="phone_number2" />{' '}
<Typography
variant="body2"
color="textSecondary"
component="span"
>
Home
</Typography>
<TextField source="phone_number2.number" />{' '}
{record.phone_number2.type !== 'Other' && (
<TextField
source="phone_number2.type"
color="textSecondary"
/>
)}
</Box>
</Stack>
)}
Expand Down
11 changes: 2 additions & 9 deletions examples/crm/src/contacts/ContactCreate.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { CreateBase, Form, Toolbar, useGetIdentity } from 'react-admin';
import { Card, CardContent, Box, Avatar } from '@mui/material';
import { Card, CardContent, Box } from '@mui/material';

import { ContactInputs } from './ContactInputs';
import { Contact } from '../types';
Expand All @@ -22,14 +22,7 @@ export const ContactCreate = () => {
<Form defaultValues={{ sales_id: identity?.id }}>
<Card>
<CardContent>
<Box>
<Box display="flex">
<Box mr={2}>
<Avatar />
</Box>
<ContactInputs />
</Box>
</Box>
<ContactInputs />
</CardContent>
<Toolbar />
</Card>
Expand Down
10 changes: 1 addition & 9 deletions examples/crm/src/contacts/ContactEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react';
import { EditBase, Form, Toolbar, useEditContext } from 'react-admin';
import { Card, CardContent, Box } from '@mui/material';

import { Avatar } from './Avatar';
import { ContactInputs } from './ContactInputs';
import { ContactAside } from './ContactAside';
import { Contact } from '../types';
Expand All @@ -22,14 +21,7 @@ const ContactEditContent = () => {
<Form>
<Card>
<CardContent>
<Box>
<Box display="flex">
<Box mr={2}>
<Avatar />
</Box>
<ContactInputs />
</Box>
</Box>
<ContactInputs />
</CardContent>
<Toolbar />
</Card>
Expand Down
210 changes: 139 additions & 71 deletions examples/crm/src/contacts/ContactInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ import {
useCreate,
useGetIdentity,
useNotify,
RadioButtonGroupInput,
} from 'react-admin';
import { Divider, Box, Stack } from '@mui/material';
import {
Divider,
Stack,
Typography,
useMediaQuery,
useTheme,
} from '@mui/material';
import { useConfigurationContext } from '../root/ConfigurationContext';
import { Avatar } from './Avatar';
import { Sale } from '../types';

const isLinkedinUrl = (url: string) => {
if (!url) return;
try {
// Parse the URL to ensure it is valid
const parsedUrl = new URL(url);
if (!parsedUrl.hostname.includes('linkedin.com')) {
if (!parsedUrl.hostname.startsWith('https://linkedin.com/')) {
return 'URL must be from linkedin.com';
}
} catch (e) {
Expand All @@ -29,7 +38,64 @@ const isLinkedinUrl = (url: string) => {
};

export const ContactInputs = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));

return (
<Stack gap={4} p={1}>
<Stack gap={2}>
<Avatar />
<Stack gap={4} flexDirection={isMobile ? 'column' : 'row'}>
<ContactIdentityInputs />
<Divider
orientation={isMobile ? 'horizontal' : 'vertical'}
flexItem
/>
<ContactPositionInputs />
</Stack>
</Stack>

<Stack gap={4} flexDirection={isMobile ? 'column' : 'row'}>
<ContactPersonalInformationInputs />
<Divider
orientation={isMobile ? 'horizontal' : 'vertical'}
flexItem
/>
<ContactMiscInputs />
</Stack>
</Stack>
);
};

const ContactIdentityInputs = () => {
const { contactGender } = useConfigurationContext();
return (
<Stack gap={1} flex={1}>
<Typography variant="h6">Identity</Typography>
<RadioButtonGroupInput
label={false}
source="gender"
choices={contactGender}
helperText={false}
optionText="label"
optionValue="value"
sx={{ '& .MuiRadio-root': { paddingY: 0 } }}
/>
<TextInput
source="first_name"
validate={required()}
helperText={false}
/>
<TextInput
source="last_name"
validate={required()}
helperText={false}
/>
</Stack>
);
};

const ContactPositionInputs = () => {
const [create] = useCreate();
const { identity } = useGetIdentity();
const notify = useNotify();
Expand All @@ -55,87 +121,89 @@ export const ContactInputs = () => {
}
};
return (
<Box flex="1" mt={-1}>
<Stack direction="row" width={430} gap={1}>
<TextInput
source="first_name"
<Stack gap={1} flex={1}>
<Typography variant="h6">Position</Typography>
<TextInput source="title" helperText={false} />
<ReferenceInput source="company_id" reference="companies">
<AutocompleteInput
optionText="name"
validate={required()}
onCreate={handleCreateCompany}
helperText={false}
/>
</ReferenceInput>
</Stack>
);
};

const ContactPersonalInformationInputs = () => {
return (
<Stack gap={1} flex={1}>
<Typography variant="h6">Personal info</Typography>
<TextInput source="email" helperText={false} validate={email()} />
<Stack gap={1} flexDirection="row">
<TextInput
source="last_name"
validate={required()}
source="phone_number1.number"
label="Phone number 1"
helperText={false}
/>
<SelectInput
source="phone_number1.type"
label="Type"
helperText={false}
optionText={choice => choice.id}
choices={[{ id: 'Work' }, { id: 'Home' }, { id: 'Other' }]}
/>
</Stack>
<Stack direction="row" width={430} gap={1}>
<TextInput source="title" helperText={false} />
<ReferenceInput source="company_id" reference="companies">
<AutocompleteInput
optionText="name"
validate={required()}
onCreate={handleCreateCompany}
helperText={false}
/>
</ReferenceInput>
</Stack>
<Divider sx={{ my: 2 }} />
<Box width={430}>
<Stack gap={1} flexDirection="row">
<TextInput
source="email"
source="phone_number2.number"
label="Phone number 2"
helperText={false}
validate={email()}
/>
<Stack direction="row" gap={1}>
<TextInput source="phone_number1" helperText={false} />
<TextInput source="phone_number2" helperText={false} />
</Stack>
</Box>
<Divider sx={{ my: 2 }} />
<Box width={430}>
<TextInput
source="background"
label="Background info (bio, how you met, etc)"
multiline
<SelectInput
source="phone_number2.type"
label="Type"
helperText={false}
optionText={choice => choice.id}
choices={[{ id: 'Work' }, { id: 'Home' }, { id: 'Other' }]}
/>
<TextInput
source="linkedin_url"
label="Linkedin URL"
</Stack>
<TextInput
source="linkedin_url"
label="Linkedin URL"
helperText={false}
validate={isLinkedinUrl}
/>
</Stack>
);
};

const ContactMiscInputs = () => {
return (
<Stack gap={1} flex={1}>
<Typography variant="h6">Misc</Typography>
<TextInput
source="background"
label="Background info (bio, how you met, etc)"
multiline
helperText={false}
/>
<BooleanInput source="has_newsletter" helperText={false} />
<ReferenceInput
reference="sales"
source="sales_id"
sort={{ field: 'last_name', order: 'ASC' }}
>
<SelectInput
helperText={false}
validate={isLinkedinUrl}
label="Account manager"
optionText={saleOptionRenderer}
/>
<Stack direction="row" gap={1} alignItems="center">
<SelectInput
source="gender"
choices={contactGender}
helperText={false}
optionText="label"
optionValue="value"
/>
<BooleanInput
source="has_newsletter"
sx={{
width: '100%',
label: { justifyContent: 'center' },
}}
helperText={false}
/>
</Stack>
</Box>
<Divider sx={{ my: 2 }} />
<Box width={430}>
<ReferenceInput
reference="sales"
source="sales_id"
sort={{ field: 'last_name', order: 'ASC' }}
>
<SelectInput
helperText={false}
label="Account manager"
sx={{ width: 210 }}
/>
</ReferenceInput>
</Box>
</Box>
</ReferenceInput>
</Stack>
);
};

const saleOptionRenderer = (choice: Sale) =>
`${choice.first_name} ${choice.last_name}`;
10 changes: 8 additions & 2 deletions examples/crm/src/dataGenerator/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,14 @@ export const generateContacts = (db: Db): Contact[] => {
company_id: company.id,
company_name: company.name,
email,
phone_number1: phone.phoneNumber(),
phone_number2: phone.phoneNumber(),
phone_number1: {
number: phone.phoneNumber(),
type: random.arrayElement(['Work', 'Home', 'Other']),
},
phone_number2: {
number: phone.phoneNumber(),
type: random.arrayElement(['Work', 'Home', 'Other']),
},
background: lorem.sentence(),
acquisition: random.arrayElement(['inbound', 'outbound']),
avatar,
Expand Down
2 changes: 2 additions & 0 deletions examples/crm/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export interface Contact extends RaRecord {
nb_notes: number;
status: string;
background: string;
phone_number1: { number: string; type: 'Work' | 'Home' | 'Other' };
phone_number2: { number: string; type: 'Work' | 'Home' | 'Other' };
}

export interface ContactNote extends RaRecord {
Expand Down

0 comments on commit f52c583

Please sign in to comment.