chore: New standard fields on People (#1104)

* Add New standard fields on People

Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>

* Add requested changes

Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus <matheus_benini@hotmail.com>

---------

Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Matheus <matheus_benini@hotmail.com>
This commit is contained in:
gitstart-twenty
2023-08-10 02:36:03 +08:00
committed by GitHub
parent b557766eb0
commit fc17a0639a
15 changed files with 196 additions and 22 deletions

View File

@ -13,7 +13,7 @@
"@floating-ui/react": "^0.24.3",
"@hello-pangea/dnd": "^16.2.0",
"@hookform/resolvers": "^3.1.1",
"@tabler/icons-react": "^2.20.0",
"@tabler/icons-react": "^2.30.0",
"@types/node": "^16.18.4",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",

View File

@ -1250,6 +1250,7 @@ export type Person = {
linkedinUrl?: Maybe<Scalars['String']>;
phone?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
xUrl?: Maybe<Scalars['String']>;
};
export type PersonCreateInput = {
@ -1268,6 +1269,7 @@ export type PersonCreateInput = {
linkedinUrl?: InputMaybe<Scalars['String']>;
phone?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
xUrl?: InputMaybe<Scalars['String']>;
};
export type PersonCreateNestedManyWithoutCompanyInput = {
@ -1313,6 +1315,7 @@ export type PersonOrderByWithRelationInput = {
linkedinUrl?: InputMaybe<SortOrder>;
phone?: InputMaybe<SortOrder>;
updatedAt?: InputMaybe<SortOrder>;
xUrl?: InputMaybe<SortOrder>;
};
export type PersonRelationFilter = {
@ -1334,7 +1337,8 @@ export enum PersonScalarFieldEnum {
LinkedinUrl = 'linkedinUrl',
Phone = 'phone',
UpdatedAt = 'updatedAt',
WorkspaceId = 'workspaceId'
WorkspaceId = 'workspaceId',
XUrl = 'xUrl'
}
export type PersonUpdateInput = {
@ -1353,6 +1357,7 @@ export type PersonUpdateInput = {
linkedinUrl?: InputMaybe<Scalars['String']>;
phone?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
xUrl?: InputMaybe<Scalars['String']>;
};
export type PersonUpdateManyWithoutCompanyNestedInput = {
@ -1397,6 +1402,7 @@ export type PersonWhereInput = {
linkedinUrl?: InputMaybe<StringNullableFilter>;
phone?: InputMaybe<StringNullableFilter>;
updatedAt?: InputMaybe<DateTimeFilter>;
xUrl?: InputMaybe<StringNullableFilter>;
};
export type PersonWhereUniqueInput = {
@ -2510,7 +2516,7 @@ export type GetPeopleQueryVariables = Exact<{
}>;
export type GetPeopleQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', id: string, phone?: string | null, email?: string | null, city?: string | null, firstName?: string | null, lastName?: string | null, displayName: string, jobTitle?: string | null, linkedinUrl?: string | null, avatarUrl?: string | null, createdAt: string, _activityCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null }> };
export type GetPeopleQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', id: string, phone?: string | null, email?: string | null, city?: string | null, firstName?: string | null, lastName?: string | null, displayName: string, jobTitle?: string | null, linkedinUrl?: string | null, xUrl?: string | null, avatarUrl?: string | null, createdAt: string, _activityCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null }> };
export type GetPersonPhoneByIdQueryVariables = Exact<{
id: Scalars['String'];
@ -2566,7 +2572,7 @@ export type GetPersonQueryVariables = Exact<{
}>;
export type GetPersonQuery = { __typename?: 'Query', findUniquePerson: { __typename?: 'Person', id: string, firstName?: string | null, lastName?: string | null, displayName: string, email?: string | null, createdAt: string, city?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, avatarUrl?: string | null, phone?: string | null, _activityCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null } };
export type GetPersonQuery = { __typename?: 'Query', findUniquePerson: { __typename?: 'Person', id: string, firstName?: string | null, lastName?: string | null, displayName: string, email?: string | null, createdAt: string, city?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, xUrl?: string | null, avatarUrl?: string | null, phone?: string | null, _activityCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null } };
export type UpdateOnePersonMutationVariables = Exact<{
where: PersonWhereUniqueInput;
@ -2574,14 +2580,14 @@ export type UpdateOnePersonMutationVariables = Exact<{
}>;
export type UpdateOnePersonMutation = { __typename?: 'Mutation', updateOnePerson?: { __typename?: 'Person', id: string, city?: string | null, email?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, firstName?: string | null, lastName?: string | null, displayName: string, phone?: string | null, createdAt: string, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } | null };
export type UpdateOnePersonMutation = { __typename?: 'Mutation', updateOnePerson?: { __typename?: 'Person', id: string, city?: string | null, email?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, xUrl?: string | null, firstName?: string | null, lastName?: string | null, displayName: string, phone?: string | null, createdAt: string, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } | null };
export type InsertOnePersonMutationVariables = Exact<{
data: PersonCreateInput;
}>;
export type InsertOnePersonMutation = { __typename?: 'Mutation', createOnePerson: { __typename?: 'Person', id: string, city?: string | null, email?: string | null, firstName?: string | null, lastName?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, displayName: string, phone?: string | null, createdAt: string, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } };
export type InsertOnePersonMutation = { __typename?: 'Mutation', createOnePerson: { __typename?: 'Person', id: string, city?: string | null, email?: string | null, firstName?: string | null, lastName?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, xUrl?: string | null, displayName: string, phone?: string | null, createdAt: string, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } };
export type DeleteManyPersonMutationVariables = Exact<{
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
@ -3942,6 +3948,7 @@ export const GetPeopleDocument = gql`
displayName
jobTitle
linkedinUrl
xUrl
avatarUrl
createdAt
_activityCount
@ -4254,6 +4261,7 @@ export const GetPersonDocument = gql`
city
jobTitle
linkedinUrl
xUrl
avatarUrl
phone
_activityCount
@ -4306,6 +4314,7 @@ export const UpdateOnePersonDocument = gql`
email
jobTitle
linkedinUrl
xUrl
firstName
lastName
displayName
@ -4356,6 +4365,7 @@ export const InsertOnePersonDocument = gql`
lastName
jobTitle
linkedinUrl
xUrl
displayName
phone
createdAt

View File

@ -10,6 +10,7 @@ import {
} from '@/ui/editable-field/types/ViewField';
import {
IconBrandLinkedin,
IconBrandX,
IconBriefcase,
IconBuildingSkyscraper,
IconCalendarEvent,
@ -120,4 +121,16 @@ export const peopleViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
placeHolder: 'LinkedIn',
},
} satisfies ViewFieldDefinition<ViewFieldURLMetadata>,
{
id: 'x',
columnLabel: 'X',
columnIcon: <IconBrandX />,
columnSize: 150,
columnOrder: 9,
metadata: {
type: 'url',
fieldName: 'xUrl',
placeHolder: 'X',
},
} satisfies ViewFieldDefinition<ViewFieldURLMetadata>,
];

View File

@ -30,6 +30,7 @@ export const GET_PEOPLE = gql`
displayName
jobTitle
linkedinUrl
xUrl
avatarUrl
createdAt
_activityCount

View File

@ -14,6 +14,7 @@ export const GET_PERSON = gql`
city
jobTitle
linkedinUrl
xUrl
avatarUrl
phone
_activityCount

View File

@ -16,6 +16,7 @@ export const UPDATE_ONE_PERSON = gql`
email
jobTitle
linkedinUrl
xUrl
firstName
lastName
displayName
@ -40,6 +41,7 @@ export const INSERT_ONE_PERSON = gql`
lastName
jobTitle
linkedinUrl
xUrl
displayName
phone
createdAt

View File

@ -51,5 +51,6 @@ export { IconUserCircle } from '@tabler/icons-react';
export { IconCalendar } from '@tabler/icons-react';
export { IconPencil } from '@tabler/icons-react';
export { IconCircleDot } from '@tabler/icons-react';
export { IconBrandX } from '@tabler/icons-react';
export { IconTag } from '@tabler/icons-react';
export { IconHelpCircle } from '@tabler/icons-react';

View File

@ -2,6 +2,7 @@ import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { RoundedLink } from '@/ui/link/components/RoundedLink';
import { LinkType, SocialLink } from '@/ui/link/components/SocialLink';
const StyledRawLink = styled(RoundedLink)`
overflow: hidden;
@ -17,17 +18,40 @@ type OwnProps = {
value: string;
};
const checkUrlType = (url: string) => {
if (
/^(http|https):\/\/(?:www\.)?linkedin.com(\w+:{0,1}\w*@)?(\S+)(:([0-9])+)?(\/|\/([\w#!:.?+=&%@!\-/]))?$/.test(
url,
)
) {
return LinkType.LinkedIn;
}
if (url.match(/^((http|https):\/\/)?(?:www\.)?twitter\.com\/(\w+)?$/i)) {
return LinkType.Twitter;
}
return LinkType.Url;
};
export function InplaceInputURLDisplayMode({ value }: OwnProps) {
function handleClick(event: MouseEvent<HTMLElement>) {
event.stopPropagation();
}
const absoluteUrl = value
? value.startsWith('http')
? value
: 'https://' + value
: '';
const type = checkUrlType(absoluteUrl);
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return (
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{value}
</SocialLink>
);
}
return (
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
{value}

View File

@ -0,0 +1,50 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { RoundedLink } from './RoundedLink';
export enum LinkType {
Url = 'url',
LinkedIn = 'linkedin',
Twitter = 'twitter',
}
type OwnProps = {
href: string;
children?: React.ReactNode;
type?: LinkType;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
};
const StyledRawLink = styled(RoundedLink)`
overflow: hidden;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
export function SocialLink({ children, href, onClick, type }: OwnProps) {
let displayValue = children;
if (type === 'linkedin') {
const splitUrl = href.split('/');
const splitName = splitUrl[4].split('-');
displayValue = splitName[2]
? `${splitName[0]}-${splitName[1]}`
: splitName[0];
}
if (type === 'twitter') {
const splitUrl = href.split('/');
displayValue = `@${splitUrl[3]}`;
}
return (
<StyledRawLink href={href} onClick={onClick}>
{displayValue}
</StyledRawLink>
);
}

View File

@ -1,6 +1,9 @@
import { ReactElement } from 'react';
import { ReactElement, useState } from 'react';
import styled from '@emotion/styled';
import { IconPencil } from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { IconButton } from '@/ui/button/components/IconButton';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { CellHotkeyScopeContext } from '../../states/CellHotkeyScopeContext';
@ -12,6 +15,11 @@ import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode';
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
const StyledEditButtonContainer = styled(motion.div)`
position: absolute;
right: 5px;
`;
export const CellBaseContainer = styled.div`
align-items: center;
box-sizing: border-box;
@ -48,7 +56,38 @@ export function EditableCell({
transparent = false,
maxContentWidth,
}: OwnProps) {
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
const { isCurrentCellInEditMode, setCurrentCellInEditMode } =
useCurrentCellEditMode();
const [isHovered, setIsHovered] = useState(false);
function isValidUrl(value: string) {
let testUrl = value;
if (testUrl && !testUrl.startsWith('http')) {
testUrl = 'http://' + testUrl;
}
try {
new URL(testUrl);
return true;
} catch (err) {
return false;
}
}
const handleClick = () => {
setCurrentCellInEditMode();
};
function handleContainerMouseEnter() {
setIsHovered(true);
}
function handleContainerMouseLeave() {
setIsHovered(false);
}
const value = nonEditModeContent.props.value;
const showEditButton =
!isCurrentCellInEditMode && isValidUrl(value) && isHovered;
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
@ -56,7 +95,10 @@ export function EditableCell({
<CellHotkeyScopeContext.Provider
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
>
<CellBaseContainer>
<CellBaseContainer
onMouseEnter={handleContainerMouseEnter}
onMouseLeave={handleContainerMouseLeave}
>
{isCurrentCellInEditMode ? (
<EditableCellEditMode
maxContentWidth={maxContentWidth}
@ -71,9 +113,27 @@ export function EditableCell({
{nonEditModeContent}
</EditableCellSoftFocusMode>
) : (
<EditableCellDisplayMode>
{nonEditModeContent}
</EditableCellDisplayMode>
<>
{showEditButton && (
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<IconButton
variant="shadow"
size="small"
onClick={handleClick}
icon={<IconPencil size={14} />}
/>
</StyledEditButtonContainer>
)}
<EditableCellDisplayMode>
{nonEditModeContent}
</EditableCellDisplayMode>
</>
)}
</CellBaseContainer>
</CellHotkeyScopeContext.Provider>

View File

@ -13,6 +13,7 @@ type MockedPerson = RequiredAndNotNull<
| 'lastName'
| 'displayName'
| 'linkedinUrl'
| 'xUrl'
| 'jobTitle'
| 'email'
| '__typename'
@ -35,6 +36,7 @@ export const mockedPeopleData: MockedPerson[] = [
displayName: 'Alexandre Prot',
email: 'alexandre@qonto.com',
linkedinUrl: 'https://www.linkedin.com/in/alexandreprot/',
xUrl: 'https://twitter.com/alexandreprot',
avatarUrl:
'data:image/jpeg;base64,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',
jobTitle: 'CEO',
@ -57,6 +59,7 @@ export const mockedPeopleData: MockedPerson[] = [
lastName: 'Doe',
displayName: 'John Doe',
linkedinUrl: 'https://www.linkedin.com/in/johndoe/',
xUrl: 'https://twitter.com/johndoe',
avatarUrl: '',
jobTitle: 'CTO',
email: 'john@linkedin.com',
@ -79,6 +82,7 @@ export const mockedPeopleData: MockedPerson[] = [
lastName: 'Doe',
displayName: 'Jane Doe',
linkedinUrl: 'https://www.linkedin.com/in/janedoe/',
xUrl: 'https://twitter.com/janedoe',
avatarUrl: '',
jobTitle: 'Investor',
email: 'jane@sequoiacap.com',
@ -102,6 +106,7 @@ export const mockedPeopleData: MockedPerson[] = [
displayName: 'Janice Dane',
email: 'janice@facebook.com',
linkedinUrl: 'https://www.linkedin.com/in/janicedane/',
xUrl: 'https://twitter.com/janicedane',
avatarUrl: '',
jobTitle: 'CEO',
company: {

View File

@ -4716,18 +4716,18 @@
"@svgr/plugin-jsx" "8.0.1"
"@svgr/plugin-svgo" "8.0.1"
"@tabler/icons-react@^2.20.0":
version "2.24.0"
resolved "https://registry.yarnpkg.com/@tabler/icons-react/-/icons-react-2.24.0.tgz#86b6c97fdabcfde38c40a9226c9ad95754ee99f0"
integrity sha512-0pNc+ffp4HZCsozv9aN/hSDiC/RTGozTmf0MCL4U9NIo8yMQh8q3zEfXRNr18IM2InyIBJL95/1J2kzgU2lYeA==
"@tabler/icons-react@^2.30.0":
version "2.30.0"
resolved "https://registry.yarnpkg.com/@tabler/icons-react/-/icons-react-2.30.0.tgz#e7ece4b83d6cd66b3a4ec043b89bf18e5341c03e"
integrity sha512-aYggXusHW133L4KujJkVf4GIIrjg7tIRHgNf/n37mnoHqMjwNP+PjmVdrBM1Z8Ywx9PKFRlrwM0eUMDcG+I4HA==
dependencies:
"@tabler/icons" "2.24.0"
"@tabler/icons" "2.30.0"
prop-types "^15.7.2"
"@tabler/icons@2.24.0":
version "2.24.0"
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.24.0.tgz#d24f7076dab1ad6c6889258ddee3d25f29080539"
integrity sha512-Otv6zrVF3HU54G6FK7OPODcQmKR9KgM6Ppi+ib3gHHB1LZEs2HIdQJYTHP5dGE+yOQWtXS9ZnGmSZDkSFLbkkg==
"@tabler/icons@2.30.0":
version "2.30.0"
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.30.0.tgz#4ea3c4da56fd5653bb9d0be0dc7feaa33602555c"
integrity sha512-tvtmkI4ALjKThVVORh++sB9JnkFY7eGInKxNy+Df7WVQiF7T85tlvGADzlgX4Ic+CK5MIUzZ0jhOlQ/RRlgXpg==
"@testing-library/dom@^8.3.0", "@testing-library/dom@^8.5.0":
version "8.20.1"

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "people" ADD COLUMN "twitterUrl" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "people" RENAME COLUMN "twitterUrl" TO "xUrl";

View File

@ -266,6 +266,9 @@ model Person {
linkedinUrl String?
/// @Validator.IsString()
/// @Validator.IsOptional()
xUrl String?
/// @Validator.IsString()
/// @Validator.IsOptional()
jobTitle String?
/// @Validator.IsString()
/// @Validator.IsOptional()