Merge branch 'main' into feat/new-timezone-settings

This commit is contained in:
Lucas Bordeau 2024-07-24 17:57:59 +02:00
commit 7c9e22970a
300 changed files with 12140 additions and 10434 deletions

File diff suppressed because one or more lines are too long

894
.yarn/releases/yarn-4.3.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -2,4 +2,4 @@ enableInlineHunks: true
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.0.2.cjs
yarnPath: .yarn/releases/yarn-4.3.1.cjs

View File

@ -6,8 +6,9 @@
"@aws-sdk/client-lambda": "^3.614.0",
"@aws-sdk/client-s3": "^3.363.0",
"@aws-sdk/credential-providers": "^3.363.0",
"@blocknote/core": "^0.12.1",
"@blocknote/react": "^0.12.2",
"@blocknote/core": "^0.15.3",
"@blocknote/mantine": "^0.15.3",
"@blocknote/react": "^0.15.3",
"@chakra-ui/accordion": "^2.3.0",
"@chakra-ui/system": "^2.6.0",
"@codesandbox/sandpack-react": "^2.13.5",
@ -347,7 +348,7 @@
},
"license": "AGPL-3.0",
"name": "twenty",
"packageManager": "yarn@4.0.2",
"packageManager": "yarn@4.3.1",
"resolutions": {
"graphql": "16.8.0",
"type-fest": "4.10.1",

View File

@ -5,7 +5,7 @@ if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]
echo "Running database setup and migrations..."
# Run setup and migration scripts
npx ts-node ./scripts/setup-db.ts
NODE_OPTIONS="--max-old-space-size=1500" npx ts-node ./scripts/setup-db.ts
yarn database:migrate:prod
# Mark initialization as done

View File

@ -61,7 +61,7 @@
"test": {},
"storybook:build": {
"options": {
"env": { "NODE_OPTIONS": "--max_old_space_size=5000" }
"env": { "NODE_OPTIONS": "--max_old_space_size=6000" }
}
},
"storybook:serve:dev": {

View File

@ -165,7 +165,9 @@ export const PageChangeEffect = () => {
useEffect(() => {
if (
isCaptchaScriptLoaded &&
isMatchingLocation(AppPath.SignInUp || AppPath.Invite)
(isMatchingLocation(AppPath.SignInUp) ||
isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.ResetPassword))
) {
requestFreshCaptchaToken();
}

View File

@ -7,6 +7,7 @@ import { scrollPositionState } from '@/ui/utilities/scroll/states/scrollPosition
import { isDefined } from '~/utils/isDefined';
/**
* @deprecated We should now use useScrollToPosition instead
* Note that `location.key` is used in the cache key, not `location.pathname`,
* so the same path navigated to at different points in the history stack will
* not share the same scroll position.

View File

@ -1,7 +1,7 @@
import { ChangeEvent, useRef } from 'react';
import { createReactBlockSpec } from '@blocknote/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { ChangeEvent, useRef } from 'react';
import { Button } from '@/ui/input/button/components/Button';
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
@ -65,6 +65,10 @@ export const FileBlock = createReactBlockSpec(
}
const fileUrl = await editor.uploadFile?.(file);
if (!isNonEmptyString(fileUrl)) {
return '';
}
editor.updateBlock(block.id, {
props: {
...block.props,

View File

@ -5,11 +5,15 @@ import {
IconH1,
IconH2,
IconH3,
IconHeadphones,
IconList,
IconListCheck,
IconListNumbers,
IconMoodSmile,
IconPhoto,
IconPilcrow,
IconTable,
IconVideo,
} from 'twenty-ui';
import { SuggestionItem } from '@/ui/input/editor/components/CustomSlashMenu';
@ -22,9 +26,13 @@ const Icons: Record<string, IconComponent> = {
'Heading 3': IconH3,
'Numbered List': IconListNumbers,
'Bullet List': IconList,
'Check List': IconListCheck,
Paragraph: IconPilcrow,
Table: IconTable,
Image: IconPhoto,
Video: IconVideo,
Audio: IconHeadphones,
Emoji: IconMoodSmile,
};
export const getSlashMenu = (editor: typeof blockSchema.BlockNoteEditor) => {

View File

@ -1,7 +1,7 @@
import { ClipboardEvent, useCallback, useMemo } from 'react';
import { useApolloClient } from '@apollo/client';
import { useCreateBlockNote } from '@blocknote/react';
import { isArray, isNonEmptyString } from '@sniptt/guards';
import { ClipboardEvent, useCallback, useMemo } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce';
@ -30,7 +30,8 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { getFileType } from '../files/utils/getFileType';
import '@blocknote/react/style.css';
import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css';
type ActivityBodyEditorProps = {
activityId: string;

View File

@ -1,5 +1,5 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRef } from 'react';
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
import { ActivityBodyEffect } from '@/activities/components/ActivityBodyEffect';
@ -11,8 +11,6 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { ActivityTitle } from './ActivityTitle';
import '@blocknote/core/style.css';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;

View File

@ -25,12 +25,12 @@ describe('getDisplayNameFromParticipant', () => {
avatarUrl: '',
jobTitle: '',
linkedinLink: {
url: '',
label: '',
primaryLinkUrl: '',
primaryLinkLabel: '',
},
xLink: {
url: '',
label: '',
primaryLinkUrl: '',
primaryLinkLabel: '',
},
city: '',
email: '',

View File

@ -23,7 +23,9 @@ const StyledContainer = styled.div`
display: inline-flex;
height: ${({ theme }) => theme.spacing(12)};
min-width: calc(100% - ${({ theme }) => theme.spacing(8)});
max-width: calc(100% - ${({ theme }) => theme.spacing(8)});
padding: 0 ${({ theme }) => theme.spacing(4)};
overflow: hidden;
&:last-child {
border-bottom: 0;
@ -33,6 +35,9 @@ const StyledContainer = styled.div`
const StyledTaskBody = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
max-width: 100%;
flex: 1;
overflow: hidden;
`;
const StyledTaskTitle = styled.div<{
@ -42,6 +47,9 @@ const StyledTaskTitle = styled.div<{
font-weight: ${({ theme }) => theme.font.weight.medium};
padding: 0 ${({ theme }) => theme.spacing(2)};
text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const StyledCommentIcon = styled.div`
@ -73,6 +81,8 @@ const StyledPlaceholder = styled.div`
const StyledLeftSideContainer = styled.div`
display: flex;
flex: 1;
overflow: hidden;
`;
const StyledCheckboxContainer = styled.div`

View File

@ -5,6 +5,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useSetRecordValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isDefined } from 'twenty-ui';
export const EventFieldDiffValueEffect = ({
diffArtificialRecordStoreId,
@ -23,7 +24,7 @@ export const EventFieldDiffValueEffect = ({
const setRecordValue = useSetRecordValue();
useEffect(() => {
if (!diffRecord) return;
if (!isDefined(diffRecord)) return;
const forgedObjectRecord = {
__typename: mainObjectMetadataItem.nameSingular,

View File

@ -9,8 +9,16 @@ export type Company = {
address: string;
accountOwnerId?: string | null;
position?: number;
linkedinLink: { __typename?: 'Link'; url: string; label: string };
xLink?: { __typename?: 'Link'; url: string; label: string };
linkedinLink: {
__typename?: 'Links';
primaryLinkUrl: string;
primaryLinkLabel: string;
};
xLink?: {
__typename?: 'Links';
primaryLinkUrl: string;
primaryLinkLabel: string;
};
annualRecurringRevenue: {
__typename?: 'Currency';
amountMicros: number | null;

View File

@ -92,8 +92,9 @@ export const mocks = [
person {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -106,8 +107,9 @@ export const mocks = [
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl
@ -132,12 +134,14 @@ export const mocks = [
company {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
domainName
annualRecurringRevenue {
@ -218,8 +222,9 @@ export const mocks = [
person {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -232,8 +237,9 @@ export const mocks = [
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl
@ -258,12 +264,14 @@ export const mocks = [
company {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
domainName
annualRecurringRevenue {

View File

@ -0,0 +1,15 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const SORTABLE_FIELD_METADATA_TYPES = [
FieldMetadataType.DateTime,
FieldMetadataType.Date,
FieldMetadataType.Number,
FieldMetadataType.Text,
FieldMetadataType.Boolean,
FieldMetadataType.Select,
FieldMetadataType.Phone,
FieldMetadataType.Email,
FieldMetadataType.FullName,
FieldMetadataType.Rating,
FieldMetadataType.Currency,
];

View File

@ -0,0 +1,20 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useMemo } from 'react';
export const useActiveFieldMetadataItems = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const activeFieldMetadataItems = useMemo(
() =>
objectMetadataItem
? objectMetadataItem.fields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
)
: [],
[objectMetadataItem],
);
return { activeFieldMetadataItems };
};

View File

@ -50,13 +50,15 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
__typename
xLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
domainName
annualRecurringRevenue
@ -113,8 +115,9 @@ idealCustomerProfile
__typename
xLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
accountOwner
{
@ -128,8 +131,9 @@ id
}
linkedinLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
domainName
annualRecurringRevenue
@ -157,8 +161,9 @@ people
__typename
xLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -173,8 +178,9 @@ name
phone
linkedinLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -39,8 +39,9 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
__typename
xLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -49,13 +50,15 @@ company
__typename
xLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
domainName
annualRecurringRevenue
@ -93,8 +96,9 @@ name
phone
linkedinLink
{
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -1,6 +1,6 @@
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { SORTABLE_FIELD_METADATA_TYPES } from '@/object-metadata/constants/SortableFieldMetadataTypes';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const formatFieldMetadataItemsAsSortDefinitions = ({
@ -9,20 +9,7 @@ export const formatFieldMetadataItemsAsSortDefinitions = ({
fields: Array<ObjectMetadataItem['fields'][0]>;
}): SortDefinition[] =>
fields.reduce((acc, field) => {
if (
![
FieldMetadataType.DateTime,
FieldMetadataType.Date,
FieldMetadataType.Number,
FieldMetadataType.Text,
FieldMetadataType.Boolean,
FieldMetadataType.Select,
FieldMetadataType.Phone,
FieldMetadataType.Email,
FieldMetadataType.FullName,
FieldMetadataType.Rating,
].includes(field.type)
) {
if (!SORTABLE_FIELD_METADATA_TYPES.includes(field.type)) {
return acc;
}

View File

@ -703,7 +703,7 @@ export const getObjectMetadataItemsMock = () => {
{
__typename: 'field',
id: '20202020-a3a7-4f63-9303-10226f6055be',
type: 'LINK',
type: 'LINKS',
name: 'xLink',
label: 'X',
description: 'Contacts X/Twitter account',
@ -984,7 +984,7 @@ export const getObjectMetadataItemsMock = () => {
{
__typename: 'field',
id: '20202020-dcf6-445a-b543-37e55de43c25',
type: 'LINK',
type: 'LINKS',
name: 'linkedinLink',
label: 'Linkedin',
description: 'Contacts Linkedin account',
@ -2847,7 +2847,7 @@ export const getObjectMetadataItemsMock = () => {
{
__typename: 'field',
id: '20202020-46e3-479a-b8f4-77137c74daa6',
type: 'LINK',
type: 'LINKS',
name: 'xLink',
label: 'X',
description: 'The company Twitter/X account',
@ -2894,7 +2894,7 @@ export const getObjectMetadataItemsMock = () => {
{
__typename: 'field',
id: '20202020-a61d-4b78-b998-3fd88b4f73a1',
type: 'LINK',
type: 'LINKS',
name: 'linkedinLink',
label: 'Linkedin',
description: 'The company Linkedin account',

View File

@ -1,8 +1,8 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType';
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
export const getOrderByFieldForObjectMetadataItem = (
@ -13,23 +13,10 @@ export const getOrderByFieldForObjectMetadataItem = (
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
if (isDefined(labelIdentifierFieldMetadata)) {
switch (labelIdentifierFieldMetadata.type) {
case FieldMetadataType.FullName:
return [
{
[labelIdentifierFieldMetadata.name]: {
firstName: orderBy ?? 'AscNullsLast',
lastName: orderBy ?? 'AscNullsLast',
},
},
];
default:
return [
{
[labelIdentifierFieldMetadata.name]: orderBy ?? 'AscNullsLast',
},
];
}
return getOrderByForFieldMetadataType(
labelIdentifierFieldMetadata,
orderBy,
);
} else {
return [
{

View File

@ -0,0 +1,35 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getOrderByForFieldMetadataType = (
field: Pick<FieldMetadataItem, 'id' | 'name' | 'type'>,
direction: OrderBy | null | undefined,
): RecordGqlOperationOrderBy => {
switch (field.type) {
case FieldMetadataType.FullName:
return [
{
[field.name]: {
firstName: direction ?? 'AscNullsLast',
lastName: direction ?? 'AscNullsLast',
},
},
];
case FieldMetadataType.Currency:
return [
{
[field.name]: {
amountMicros: direction ?? 'AscNullsLast',
},
},
];
default:
return [
{
[field.name]: direction ?? 'AscNullsLast',
},
];
}
};

View File

@ -7,8 +7,9 @@ export const query = gql`
createPeople(data: $data, upsert: $upsert) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -21,8 +22,9 @@ export const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl
@ -44,8 +46,9 @@ export const variables = { data };
export const responseData = {
__typeName: '',
xLink: {
label: '',
url: '',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: null,
},
createdAt: '',
city: '',
@ -57,8 +60,9 @@ export const responseData = {
},
phone: '',
linkedinLink: {
label: '',
url: '',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: null,
},
updatedAt: '',
avatarUrl: '',

View File

@ -5,8 +5,9 @@ export const query = gql`
createPerson(data: $input) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -19,8 +20,9 @@ export const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl
@ -34,8 +36,9 @@ export const responseData = {
edges: [],
},
xLink: {
label: '',
url: '',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: null,
},
pointOfContactForOpportunities: {
edges: [],
@ -62,8 +65,9 @@ export const responseData = {
},
phone: '',
linkedinLink: {
label: '',
url: '',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: null,
},
updatedAt: '',
avatarUrl: '',

View File

@ -7,8 +7,9 @@ export const query = gql`
executeQuickActionOnPerson(id: $idToExecuteQuickActionOn) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -21,8 +22,9 @@ export const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -10,8 +10,9 @@ export const query = gql`
node {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -24,8 +25,9 @@ export const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -38,8 +38,8 @@ export const query = gql`
}
}
xLink {
label
url
primaryLinkLabel
primaryLinkUrl
}
id
pointOfContactForOpportunities {
@ -67,12 +67,12 @@ export const query = gql`
__typename
id
xLink {
label
url
primaryLinkLabel
primaryLinkUrl
}
linkedinLink {
label
url
primaryLinkLabel
primaryLinkUrl
}
domainName
annualRecurringRevenue {
@ -152,8 +152,8 @@ export const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkLabel
primaryLinkUrl
}
updatedAt
avatarUrl

View File

@ -7,8 +7,9 @@ export const query = gql`
person(filter: { id: { eq: $objectRecordId } }) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -21,8 +22,9 @@ export const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -5,8 +5,9 @@ export const query = gql`
updatePerson(id: $idToUpdate, data: $input) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -19,8 +20,9 @@ export const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl
@ -32,8 +34,9 @@ export const query = gql`
const basePerson = {
id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9',
xLink: {
label: '',
url: '',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: null,
},
createdAt: '',
city: '',
@ -45,8 +48,9 @@ const basePerson = {
},
phone: '',
linkedinLink: {
label: '',
url: '',
primaryLinkUrl: '',
primaryLinkLabel: '',
secondaryLinks: null,
},
updatedAt: '',
avatarUrl: '',

View File

@ -9,8 +9,9 @@ const expectedQueryTemplate = `
createPeople(data: $data, upsert: $upsert) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -23,8 +24,9 @@ const expectedQueryTemplate = `
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -9,8 +9,9 @@ const expectedQueryTemplate = `
createPerson(data: $input) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -23,8 +24,9 @@ const expectedQueryTemplate = `
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -9,8 +9,9 @@ const expectedQueryTemplate = `
executeQuickActionOnPerson(id: $idToExecuteQuickActionOn) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -23,8 +24,9 @@ const expectedQueryTemplate = `
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -11,8 +11,9 @@ const expectedQueryTemplate = `
node {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -25,8 +26,9 @@ const expectedQueryTemplate = `
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -11,8 +11,9 @@ const expectedQueryTemplate = `
node {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -25,8 +26,9 @@ const expectedQueryTemplate = `
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -9,8 +9,9 @@ query FindOnePerson($objectRecordId: ID!) {
person(filter: { id: { eq: $objectRecordId } }) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -23,8 +24,9 @@ query FindOnePerson($objectRecordId: ID!) {
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -9,8 +9,9 @@ mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
updatePerson(id: $idToUpdate, data: $input) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -23,8 +24,9 @@ mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -209,15 +209,16 @@ export const useFetchMoreRecordsWithPagination = <
const totalCount = data?.[objectMetadataItem.namePlural]?.totalCount;
const recordConnection = data?.[objectMetadataItem.namePlural];
const records = useMemo(
() =>
data?.[objectMetadataItem.namePlural]
isDefined(recordConnection)
? getRecordsFromRecordConnection<T>({
recordConnection: data?.[objectMetadataItem.namePlural],
recordConnection,
})
: ([] as T[]),
[data, objectMetadataItem.namePlural],
: [],
[recordConnection],
);
return {

View File

@ -7,7 +7,9 @@ import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetada
import { RatingInput } from '@/ui/field/input/components/RatingInput';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
const convertFieldRatingValueToNumber = (rating: FieldRatingValue): string => {
const convertFieldRatingValueToNumber = (
rating: Exclude<FieldRatingValue, null>,
): string => {
return rating.split('_')[1];
};
@ -51,6 +53,10 @@ export const ObjectFilterDropdownRatingInput = () => {
<RatingInput
value={selectedFilter?.value as FieldRatingValue}
onChange={(newValue: FieldRatingValue) => {
if (!newValue) {
return;
}
selectFilter?.({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,

View File

@ -2,20 +2,23 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { hasPositionField } from '@/object-metadata/utils/hasPositionField';
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { Field, FieldMetadataType } from '~/generated/graphql';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType';
import { Sort } from '../types/Sort';
export const turnSortsIntoOrderBy = (
objectMetadataItem: ObjectMetadataItem,
sorts: Sort[],
): RecordGqlOperationOrderBy => {
const fields: Pick<Field, 'id' | 'name' | 'type'>[] =
const fields: Pick<FieldMetadataItem, 'id' | 'name' | 'type'>[] =
objectMetadataItem?.fields ?? [];
const fieldsById = mapArrayToObject(fields, ({ id }) => id);
const sortsOrderBy = sorts
.map((sort) => {
const correspondingField = fieldsById[sort.fieldMetadataId];
@ -32,26 +35,14 @@ export const turnSortsIntoOrderBy = (
.filter(isDefined);
if (hasPositionField(objectMetadataItem)) {
return [...sortsOrderBy, { position: 'AscNullsFirst' }];
const positionOrderBy = [
{
position: 'AscNullsFirst',
},
] satisfies RecordGqlOperationOrderBy;
return [...sortsOrderBy, ...positionOrderBy].flat();
}
return sortsOrderBy;
};
const getOrderByForFieldMetadataType = (
field: Pick<Field, 'id' | 'name' | 'type'>,
direction: OrderBy,
) => {
switch (field.type) {
case FieldMetadataType.FullName:
return {
[field.name]: {
firstName: direction,
lastName: direction,
},
};
default:
return { [field.name]: direction };
}
return sortsOrderBy.flat();
};

View File

@ -136,7 +136,6 @@ export const useRecordActionBar = ({
accent: 'danger',
onClick: () => {
setIsDeleteRecordsModalOpen(true);
handleDeleteClick();
},
ConfirmationModal: (
<ConfirmationModal

View File

@ -25,9 +25,11 @@ export type RecordBoardProps = {
const StyledContainer = styled.div`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
overflow: auto;
display: flex;
flex: 1;
flex-direction: row;
min-height: calc(100% - 1px);
`;
const StyledWrapper = styled.div`

View File

@ -13,7 +13,7 @@ import {
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { RecordIndexRecordChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
@ -222,7 +222,7 @@ export const RecordBoardCard = () => {
}}
>
<StyledBoardCardHeader showCompactView={isCompactModeActive}>
<RecordIndexRecordChip
<RecordIdentifierChip
objectNameSingular={objectMetadataItem.nameSingular}
record={record}
variant={AvatarChipVariant.Transparent}

View File

@ -64,7 +64,7 @@ export const linkFieldDefinition: FieldDefinition<FieldLinkMetadata> = {
label: 'LinkedIn URL',
iconName: 'url',
type: FieldMetadataType.Link,
defaultValue: { label: '', url: '' },
defaultValue: { url: '', label: '' },
metadata: {
fieldName: 'linkedInURL',
placeHolder: 'https://linkedin.com/user',

View File

@ -4,7 +4,8 @@ import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/dis
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
import { isFieldChipDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldChipDisplay';
import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
@ -46,7 +47,10 @@ import { isFieldUuid } from '../types/guards/isFieldUuid';
export const FieldDisplay = () => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
const isChipDisplay = isFieldChipDisplay(fieldDefinition, isLabelIdentifier);
const isChipDisplay = isFieldIdentifierDisplay(
fieldDefinition,
isLabelIdentifier,
);
return isChipDisplay ? (
<ChipFieldDisplay />

View File

@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { gql } from '@apollo/client';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -25,8 +25,9 @@ const query = gql`
updatePerson(id: $idToUpdate, data: $input) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
id
createdAt
@ -39,8 +40,9 @@ const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
updatedAt
avatarUrl

View File

@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { gql } from '@apollo/client';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -27,12 +27,14 @@ const mocks: MockedResponse[] = [
updateCompany(id: $idToUpdate, data: $input) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
domainName
annualRecurringRevenue {

View File

@ -1,6 +1,6 @@
import { RecordChip } from '@/object-record/components/RecordChip';
import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay';
import { RecordIndexRecordChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
export const ChipFieldDisplay = () => {
const { recordValue, objectNameSingular, isLabelIdentifier } =
@ -11,7 +11,7 @@ export const ChipFieldDisplay = () => {
}
return isLabelIdentifier ? (
<RecordIndexRecordChip
<RecordIdentifierChip
objectNameSingular={objectNameSingular}
record={recordValue}
/>

View File

@ -1,7 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useFullNameFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useFullNameFieldDisplay';
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
import { OverflowingTextWithTooltip } from 'twenty-ui';
export const FullNameFieldDisplay = () => {
const { fieldValue } = useFullNameFieldDisplay();
@ -10,5 +10,5 @@ export const FullNameFieldDisplay = () => {
.filter(isNonEmptyString)
.join(' ');
return <TextDisplay text={content} />;
return <OverflowingTextWithTooltip text={content} />;
};

View File

@ -2,7 +2,7 @@ import { useTextFieldDisplay } from '@/object-record/record-field/meta-types/hoo
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
export const TextFieldDisplay = () => {
const { fieldValue, maxWidth } = useTextFieldDisplay();
const { fieldValue } = useTextFieldDisplay();
return <TextDisplay text={fieldValue} maxWidth={maxWidth} />;
return <TextDisplay text={fieldValue} />;
};

View File

@ -6,8 +6,8 @@ export const fieldValue = [
domainName: 'google.com',
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
name: 'Google',
annualRecurringRevenue: {
@ -25,8 +25,8 @@ export const fieldValue = [
updatedAt: '2024-05-01T13:16:29.046Z',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
},
{
@ -34,8 +34,8 @@ export const fieldValue = [
domainName: 'airbnb.com',
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
name: 'Airbnb',
annualRecurringRevenue: {
@ -53,8 +53,8 @@ export const fieldValue = [
updatedAt: '2024-05-01T13:16:29.046Z',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
},
];
@ -72,8 +72,8 @@ export const otherPersonMock = {
domainName: 'google.com',
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
name: 'Google',
annualRecurringRevenue: {
@ -91,8 +91,8 @@ export const otherPersonMock = {
updatedAt: '2024-05-01T13:16:29.046Z',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
},
id: 'd3e70589-c449-4e64-8268-065640fdaff0',
@ -100,13 +100,13 @@ export const otherPersonMock = {
phone: '+33744332211',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
tEst: '',
position: 14,
@ -128,8 +128,8 @@ export const relationFromManyFieldDisplayMock = {
domainName: 'google.com',
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
name: 'Google',
annualRecurringRevenue: {
@ -147,8 +147,8 @@ export const relationFromManyFieldDisplayMock = {
updatedAt: '2024-05-01T13:16:29.046Z',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
},
id: '20202020-2d40-4e49-8df4-9c6a049191df',
@ -156,13 +156,13 @@ export const relationFromManyFieldDisplayMock = {
phone: '+33788901235',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
tEst: '',
position: 15,
@ -172,8 +172,8 @@ export const relationFromManyFieldDisplayMock = {
domainName: 'microsoft.com',
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
name: 'Microsoft',
annualRecurringRevenue: {
@ -191,13 +191,13 @@ export const relationFromManyFieldDisplayMock = {
updatedAt: '2024-05-01T13:16:29.046Z',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
},
fieldDefinition: {
fieldMetadataId: '4e79f0b7-d100-4e89-a07b-315a710b8059',
label: 'Company',
primaryLinkLabel: 'Company',
metadata: {
fieldName: 'company',
placeHolder: 'Company',

View File

@ -3,7 +3,7 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
export const isFieldChipDisplay = (
export const isFieldIdentifierDisplay = (
field: Pick<FieldMetadataItem, 'type'>,
isLabelIdentifier: boolean,
) =>

View File

@ -4,7 +4,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { convertCurrencyToCurrencyMicros } from '~/utils/convert-currency-amount';
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
import { FieldContext } from '../../contexts/FieldContext';
import { usePersistField } from '../../hooks/usePersistField';
@ -45,7 +45,7 @@ export const useCurrencyField = () => {
const newCurrencyValue = {
amountMicros: isNaN(amount)
? null
: convertCurrencyToCurrencyMicros(amount),
: convertCurrencyAmountToCurrencyMicros(amount),
currencyCode,
};

View File

@ -16,14 +16,14 @@ export const useRatingField = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldRatingValue | null>(
const [fieldValue, setFieldValue] = useRecoilState<FieldRatingValue>(
recordStoreFamilySelector({
recordId: entityId,
fieldName: fieldName,
}),
);
const rating = fieldValue ?? 'RATING_1';
const rating = fieldValue ?? null;
return {
fieldDefinition,

View File

@ -5,8 +5,7 @@ import { useRecordFieldValue } from '@/object-record/record-store/contexts/Recor
import { FieldContext } from '../../contexts/FieldContext';
export const useTextFieldDisplay = () => {
const { entityId, fieldDefinition, hotkeyScope, maxWidth } =
useContext(FieldContext);
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
@ -14,7 +13,6 @@ export const useTextFieldDisplay = () => {
useRecordFieldValue<string | undefined>(entityId, fieldName) ?? '';
return {
maxWidth,
fieldDefinition,
fieldValue,
hotkeyScope,

View File

@ -185,7 +185,7 @@ export type FieldAddressValue = {
addressLat: number | null;
addressLng: number | null;
};
export type FieldRatingValue = (typeof RATING_VALUES)[number];
export type FieldRatingValue = (typeof RATING_VALUES)[number] | null;
export type FieldSelectValue = string | null;
export type FieldMultiSelectValue = string[] | null;

View File

@ -1,17 +1,10 @@
import styled from '@emotion/styled';
import {
useRecoilCallback,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
@ -26,22 +19,17 @@ import { recordIndexIsCompactModeActiveState } from '@/object-record/record-inde
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { useFindRecordCursorFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useFindRecordCursorFromFindManyCacheRootQuery';
import { findView } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { ViewBar } from '@/views/components/ViewBar';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { View } from '@/views/types/View';
import { ViewField } from '@/views/types/ViewField';
import { ViewType } from '@/views/types/ViewType';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useNavigate } from 'react-router-dom';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const StyledContainer = styled.div`
@ -52,7 +40,8 @@ const StyledContainer = styled.div`
overflow: auto;
`;
const StyledContainerWithPadding = styled.div`
const StyledContainerWithPadding = styled.div<{ fullHeight?: boolean }>`
min-height: ${({ fullHeight }) => (fullHeight ? '100%' : 'auto')};
padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
`;
@ -124,54 +113,11 @@ export const RecordIndexContainer = ({
[columnDefinitions, setTableColumns],
);
const navigate = useNavigate();
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const currentViewId = useRecoilValue(
currentViewIdComponentState({
scopeId: recordIndexId,
}),
);
const view = findView({
objectMetadataItemId: objectMetadataItem?.id ?? '',
viewId: currentViewId ?? null,
views,
});
const filter = turnObjectDropdownFilterIntoQueryFilter(
mapViewFiltersToFilters(view?.viewFilters ?? [], filterDefinitions),
objectMetadataItem?.fields ?? [],
);
const orderBy = turnSortsIntoOrderBy(
const { handleIndexIdentifierClick } = useHandleIndexIdentifierClick({
objectMetadataItem,
mapViewSortsToSorts(view?.viewSorts ?? [], sortDefinitions),
);
const { findCursorInCache } = useFindRecordCursorFromFindManyCacheRootQuery({
fieldVariables: {
filter,
orderBy,
},
objectNamePlural: objectNamePlural,
recordIndexId,
});
const handleIndexIdentifierClick = (recordId: string) => {
const cursor = findCursorInCache(recordId);
// TODO: use URL builder
navigate(
`/object/${objectNameSingular}/${recordId}?view=${currentViewId}`,
{
state: {
cursor,
},
},
);
};
const handleIndexRecordsLoaded = useRecoilCallback(
({ set }) =>
() => {
@ -248,7 +194,7 @@ export const RecordIndexContainer = ({
</>
)}
{recordIndexViewType === ViewType.Kanban && (
<StyledContainerWithPadding>
<StyledContainerWithPadding fullHeight>
<RecordIndexBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}

View File

@ -5,17 +5,17 @@ import { RecordIndexEventContext } from '@/object-record/record-index/contexts/R
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useContext } from 'react';
export type RecordIndexRecordChipProps = {
export type RecordIdentifierChipProps = {
objectNameSingular: string;
record: ObjectRecord;
variant?: AvatarChipVariant;
};
export const RecordIndexRecordChip = ({
export const RecordIdentifierChip = ({
objectNameSingular,
record,
variant,
}: RecordIndexRecordChipProps) => {
}: RecordIdentifierChipProps) => {
const { onIndexIdentifierClick } = useContext(RecordIndexEventContext);
const { recordChipData } = useRecordChipData({

View File

@ -0,0 +1,33 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
export const useHandleIndexIdentifierClick = ({
objectMetadataItem,
recordIndexId,
}: {
recordIndexId: string;
objectMetadataItem: ObjectMetadataItem;
}) => {
const navigate = useNavigate();
const currentViewId = useRecoilValue(
currentViewIdComponentState({
scopeId: recordIndexId,
}),
);
const handleIndexIdentifierClick = (recordId: string) => {
const showPageURL = buildShowPageURL(
objectMetadataItem.nameSingular,
recordId,
currentViewId,
);
navigate(showPageURL);
};
return { handleIndexIdentifierClick };
};

View File

@ -19,7 +19,7 @@ import {
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { useSpreadsheetRecordImport } from '@/object-record/spreadsheet-import/useSpreadsheetRecordImport';
import { useOpenObjectRecordsSpreasheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreasheetImportDialog';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
@ -114,8 +114,8 @@ export const RecordIndexOptionsDropdownContent = ({
? handleBoardFieldVisibilityChange
: handleColumnVisibilityChange;
const { openRecordSpreadsheetImport } =
useSpreadsheetRecordImport(objectNameSingular);
const { openObjectRecordsSpreasheetImportDialog } =
useOpenObjectRecordsSpreasheetImportDialog(objectNameSingular);
const { progress, download } = useExportTableData({
delayMs: 100,
@ -135,7 +135,7 @@ export const RecordIndexOptionsDropdownContent = ({
hasSubMenu
/>
<MenuItem
onClick={() => openRecordSpreadsheetImport()}
onClick={() => openObjectRecordsSpreasheetImportDialog()}
LeftIcon={IconFileImport}
text="Import"
/>

View File

@ -1,7 +1,8 @@
import { useMemo } from 'react';
import { json2csv } from 'json-2-csv';
import { useMemo } from 'react';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useProcessRecordsForCSVExport } from '@/object-record/record-index/options/hooks/useProcessRecordsForCSVExport';
import {
useTableData,
UseTableDataOptions,
@ -66,12 +67,15 @@ export const generateCsv: GenerateExport = ({
.filter(isDefined)
.join(' '),
};
const fieldsWithSubFields = rows.find((row) => {
const fieldValue = (row as any)[column.field];
const hasSubFields =
fieldValue &&
typeof fieldValue === 'object' &&
!Array.isArray(fieldValue);
return hasSubFields;
});
@ -84,8 +88,10 @@ export const generateCsv: GenerateExport = ({
field: `${column.field}.${key}`,
title: `${column.title} ${key[0].toUpperCase() + key.slice(1)}`,
}));
return nestedFieldsWithoutTypename;
}
return [column];
});
@ -138,12 +144,17 @@ export const useExportTableData = ({
pageSize = 30,
recordIndexId,
}: UseExportTableDataOptions) => {
const { processRecordsForCSVExport } =
useProcessRecordsForCSVExport(objectNameSingular);
const downloadCsv = useMemo(
() =>
(rows: ObjectRecord[], columns: ColumnDefinition<FieldMetadata>[]) => {
csvDownloader(filename, { rows, columns });
(records: ObjectRecord[], columns: ColumnDefinition<FieldMetadata>[]) => {
const recordsProcessedForExport = processRecordsForCSVExport(records);
csvDownloader(filename, { rows: recordsProcessedForExport, columns });
},
[filename],
[filename, processRecordsForCSVExport],
);
const { getTableData: download, progress } = useTableData({

View File

@ -0,0 +1,39 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { convertCurrencyMicrosToCurrencyAmount } from '~/utils/convertCurrencyToCurrencyMicros';
export const useProcessRecordsForCSVExport = (objectNameSingular: string) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const processRecordsForCSVExport = (records: ObjectRecord[]) => {
return records.map((record) => {
const currencyFields = objectMetadataItem.fields.filter(
(field) => field.type === FieldMetadataType.Currency,
);
const processedRecord = {
...record,
};
for (const currencyField of currencyFields) {
if (isDefined(record[currencyField.name])) {
processedRecord[currencyField.name] = {
amountMicros: convertCurrencyMicrosToCurrencyAmount(
record[currencyField.name].amountMicros,
),
currencyCode: record[currencyField.name].currencyCode,
} satisfies FieldCurrencyValue;
}
}
return processedRecord;
});
};
return { processRecordsForCSVExport };
};

View File

@ -18,11 +18,14 @@ import { RecordInlineCellContainer } from './RecordInlineCellContainer';
type RecordInlineCellProps = {
readonly?: boolean;
loading?: boolean;
isCentered?: boolean;
};
// TODO: refactor props drilling with a RecordInlineCellContext
export const RecordInlineCell = ({
readonly,
loading,
isCentered,
}: RecordInlineCellProps) => {
const { fieldDefinition, entityId } = useContext(FieldContext);
const buttonIcon = useGetButtonIcon();
@ -86,6 +89,7 @@ export const RecordInlineCell = ({
label={fieldDefinition.label}
labelWidth={fieldDefinition.labelWidth}
showLabel={fieldDefinition.showLabel}
isCentered={isCentered}
editModeContent={
<FieldInput
recordFieldInputdId={`${entityId}-${fieldDefinition?.metadata?.fieldName}`}

View File

@ -1,6 +1,6 @@
import React, { ReactElement, useContext } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ReactElement, useContext } from 'react';
import { AppTooltip, IconComponent, TooltipDelay } from 'twenty-ui';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -52,6 +52,8 @@ const StyledInlineCellBaseContainer = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
user-select: none;
justify-content: center;
`;
export const StyledSkeletonDiv = styled.div`
@ -72,8 +74,10 @@ export type RecordInlineCellContainerProps = {
isDisplayModeFixHeight?: boolean;
disableHoverEffect?: boolean;
loading?: boolean;
isCentered?: boolean;
};
// TODO: refactor props drilling with a RecordInlineCellContext
export const RecordInlineCellContainer = ({
readonly,
IconLabel,
@ -88,6 +92,7 @@ export const RecordInlineCellContainer = ({
isDisplayModeFixHeight,
disableHoverEffect,
loading = false,
isCentered,
}: RecordInlineCellContainerProps) => {
const { entityId, fieldDefinition } = useContext(FieldContext);
@ -153,6 +158,7 @@ export const RecordInlineCellContainer = ({
loading,
readonly,
showLabel,
isCentered,
}}
/>
</StyledValueContainer>

View File

@ -1,7 +1,7 @@
import { useContext } from 'react';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
import { useContext } from 'react';
import { createPortal } from 'react-dom';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -19,7 +19,8 @@ const StyledInlineCellInput = styled.div`
display: flex;
min-height: 32px;
min-width: 200px;
min-width: 320px;
width: inherit;
z-index: 1000;
@ -29,6 +30,7 @@ type RecordInlineCellEditModeProps = {
children: React.ReactNode;
};
// TODO: Refactor this to avoid setting absolute px values.
export const RecordInlineCellEditMode = ({
children,
}: RecordInlineCellEditModeProps) => {
@ -41,7 +43,8 @@ export const RecordInlineCellEditMode = ({
offset(
isCentered
? {
mainAxis: -30,
mainAxis: -32,
crossAxis: 160,
}
: {
crossAxis: -4,

View File

@ -8,11 +8,20 @@ import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/com
import { RecordInlineCellSkeletonLoader } from '@/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
const StyledClickableContainer = styled.div<{ readonly?: boolean }>`
const StyledClickableContainer = styled.div<{
readonly?: boolean;
isCentered?: boolean;
}>`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
width: 100%;
${({ isCentered }) =>
isCentered === true &&
`
justify-content: center;
`};
${({ readonly }) =>
!readonly &&
css`
@ -33,6 +42,7 @@ type RecordInlineCellValueProps = Pick<
| 'loading'
| 'showLabel'
| 'label'
| 'isCentered'
>;
export const RecordInlineCellValue = ({
@ -47,6 +57,7 @@ export const RecordInlineCellValue = ({
loading,
showLabel,
label,
isCentered,
}: RecordInlineCellValueProps) => {
const { isFocused } = useFieldFocus();
@ -68,7 +79,7 @@ export const RecordInlineCellValue = ({
<RecordInlineCellEditMode>{editModeContent}</RecordInlineCellEditMode>
)}
{editModeContentOnly ? (
<StyledClickableContainer readonly={readonly}>
<StyledClickableContainer readonly={readonly} isCentered={isCentered}>
<RecordInlineCellDisplayMode
disableHoverEffect={disableHoverEffect}
isDisplayModeFixHeight={isDisplayModeFixHeight}
@ -82,6 +93,7 @@ export const RecordInlineCellValue = ({
<StyledClickableContainer
readonly={readonly}
onClick={handleDisplayModeClick}
isCentered={isCentered}
>
<RecordInlineCellDisplayMode
disableHoverEffect={disableHoverEffect}

View File

@ -171,7 +171,7 @@ export const RecordShowContainer = ({
isCentered: true,
}}
>
<RecordInlineCell readonly={isReadOnly} />
<RecordInlineCell readonly={isReadOnly} isCentered={true} />
</FieldContext.Provider>
}
avatarType={recordIdentifier?.avatarType ?? 'rounded'}

View File

@ -1,11 +1,10 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
export const findOneRecordForShowPageOperationSignatureFactory: RecordGqlOperationSignatureFactory =
export const buildFindOneRecordForShowPageOperationSignature: RecordGqlOperationSignatureFactory =
({ objectMetadataItem }: { objectMetadataItem: ObjectMetadataItem }) => ({
objectNameSingular: CoreObjectNameSingular.Activity,
objectNameSingular: objectMetadataItem.nameSingular,
variables: {},
fields: generateDepthOneRecordGqlFields({ objectMetadataItem }),
});

View File

@ -1,34 +0,0 @@
import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
import { useApolloClient } from '@apollo/client';
import { createApolloStoreFieldName } from '~/utils/createApolloStoreFieldName';
export const useFindRecordCursorFromFindManyCacheRootQuery = ({
objectNamePlural,
fieldVariables,
}: {
objectNamePlural: string;
fieldVariables: {
filter: any;
orderBy: any;
};
}) => {
const apollo = useApolloClient();
const testsFieldNameOnRootQuery = createApolloStoreFieldName({
fieldName: objectNamePlural,
fieldVariables: fieldVariables,
});
const findCursorInCache = (recordId: string) => {
const extractedCache = apollo.cache.extract() as any;
const edgesInCache =
extractedCache?.['ROOT_QUERY']?.[testsFieldNameOnRootQuery]?.edges ?? [];
return edgesInCache.find(
(edge: RecordGqlRefEdge) => edge.node?.__ref.split(':')[1] === recordId,
)?.cursor;
};
return { findCursorInCache };
};

View File

@ -7,7 +7,7 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { findOneRecordForShowPageOperationSignatureFactory } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory';
import { buildFindOneRecordForShowPageOperationSignature } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
@ -39,7 +39,8 @@ export const useRecordShowPage = (
const { getIcon } = useIcons();
const headerIcon = getIcon(objectMetadataItem?.icon);
const FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE =
findOneRecordForShowPageOperationSignatureFactory({ objectMetadataItem });
buildFindOneRecordForShowPageOperationSignature({ objectMetadataItem });
const { record, loading } = useFindOneRecord({
objectRecordId,
objectNameSingular,

View File

@ -1,52 +1,18 @@
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
import { useMemo, useState } from 'react';
import {
useLocation,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL';
import { buildIndexTablePageURL } from '@/object-record/record-table/utils/buildIndexTableURL';
import { useQueryVariablesFromActiveFieldsOfViewOrDefaultView } from '@/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView';
import { isNonEmptyString } from '@sniptt/guards';
import { capitalize } from '~/utils/string/capitalize';
export const findView = ({
viewId,
objectMetadataItemId,
views,
}: {
viewId: string | null;
objectMetadataItemId: string;
views: View[];
}) => {
if (!viewId) {
return views.find(
(view: any) =>
view.key === 'INDEX' && view?.objectMetadataId === objectMetadataItemId,
) as View;
} else {
return views.find(
(view: any) =>
view?.id === viewId && view?.objectMetadataId === objectMetadataItemId,
) as View;
}
};
export const useRecordShowPagePagination = (
propsObjectNameSingular: string,
propsObjectRecordId: string,
@ -61,8 +27,6 @@ export const useRecordShowPagePagination = (
const setLastShowPageRecordId = useSetRecoilState(lastShowPageRecordIdState);
const [isLoadedRecords] = useState(false);
const objectNameSingular = propsObjectNameSingular || paramObjectNameSingular;
const objectRecordId = propsObjectRecordId || paramObjectRecordId;
@ -71,81 +35,43 @@ export const useRecordShowPagePagination = (
}
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const view = useMemo(() => {
return findView({
objectMetadataItemId: objectMetadataItem?.id ?? '',
viewId: viewIdQueryParam,
views,
});
}, [viewIdQueryParam, objectMetadataItem, views]);
const activeFieldMetadataItems = useMemo(
() =>
objectMetadataItem
? objectMetadataItem.fields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
)
: [],
[objectMetadataItem],
);
const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({
fields: activeFieldMetadataItems,
});
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
fields: activeFieldMetadataItems,
});
const filter = turnObjectDropdownFilterIntoQueryFilter(
mapViewFiltersToFilters(view?.viewFilters ?? [], filterDefinitions),
objectMetadataItem?.fields ?? [],
);
const orderBy = turnSortsIntoOrderBy(
objectMetadataItem,
mapViewSortsToSorts(view?.viewSorts ?? [], sortDefinitions),
);
const recordGqlFields = generateDepthOneRecordGqlFields({
objectMetadataItem,
});
const { state } = useLocation();
const { filter, orderBy } =
useQueryVariablesFromActiveFieldsOfViewOrDefaultView({
objectMetadataItem,
viewId: viewIdQueryParam,
});
const cursorFromIndexPage = state?.cursor;
const { loading: loadingCurrentRecord, pageInfo: currentRecordsPageInfo } =
const { loading: loadingCursor, pageInfo: currentRecordsPageInfo } =
useFindManyRecords({
filter: {
id: { eq: objectRecordId },
},
orderBy,
skip: isLoadedRecords,
limit: 1,
objectNameSingular,
recordGqlFields,
});
const currentRecordCursor = currentRecordsPageInfo?.endCursor;
const cursor = cursorFromIndexPage ?? currentRecordCursor;
const cursorFromRequest = currentRecordsPageInfo?.endCursor;
const {
loading: loadingRecordBefore,
records: recordsBefore,
pageInfo: pageInfoBefore,
totalCount: totalCountBefore,
} = useFindManyRecords({
skip: loadingCursor,
fetchPolicy: 'network-only',
filter,
orderBy,
skip: isLoadedRecords,
cursorFilter: isNonEmptyString(cursor)
cursorFilter: isNonEmptyString(cursorFromRequest)
? {
cursorDirection: 'before',
cursor: cursor,
cursor: cursorFromRequest,
limit: 1,
}
: undefined,
@ -156,16 +82,16 @@ export const useRecordShowPagePagination = (
const {
loading: loadingRecordAfter,
records: recordsAfter,
pageInfo: pageInfoAfter,
totalCount: totalCountAfter,
} = useFindManyRecords({
skip: loadingCursor,
filter,
fetchPolicy: 'network-only',
orderBy,
skip: isLoadedRecords,
cursorFilter: cursor
cursorFilter: cursorFromRequest
? {
cursorDirection: 'after',
cursor: cursor,
cursor: cursorFromRequest,
limit: 1,
}
: undefined,
@ -175,8 +101,7 @@ export const useRecordShowPagePagination = (
const totalCount = Math.max(totalCountBefore ?? 0, totalCountAfter ?? 0);
const loading =
loadingRecordAfter || loadingRecordBefore || loadingCurrentRecord;
const loading = loadingRecordAfter || loadingRecordBefore || loadingCursor;
const isThereARecordBefore = recordsBefore.length > 0;
const isThereARecordAfter = recordsAfter.length > 0;
@ -184,43 +109,27 @@ export const useRecordShowPagePagination = (
const recordBefore = recordsBefore[0];
const recordAfter = recordsAfter[0];
const recordBeforeCursor = pageInfoBefore?.endCursor;
const recordAfterCursor = pageInfoAfter?.endCursor;
const navigateToPreviousRecord = () => {
navigate(
`/object/${objectNameSingular}/${recordBefore.id}${
viewIdQueryParam ? `?view=${viewIdQueryParam}` : ''
}`,
{
state: {
cursor: recordBeforeCursor,
},
},
buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam),
);
};
const navigateToNextRecord = () => {
navigate(
`/object/${objectNameSingular}/${recordAfter.id}${
viewIdQueryParam ? `?view=${viewIdQueryParam}` : ''
}`,
{
state: {
cursor: recordAfterCursor,
},
},
buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam),
);
};
const navigateToIndexView = () => {
const indexPath = `/objects/${objectMetadataItem.namePlural}${
viewIdQueryParam ? `?view=${viewIdQueryParam}` : ''
}`;
const indexTableURL = buildIndexTablePageURL(
objectMetadataItem.namePlural,
viewIdQueryParam,
);
setLastShowPageRecordId(objectRecordId);
navigate(indexPath);
navigate(indexTableURL);
};
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({

View File

@ -0,0 +1,9 @@
export const buildShowPageURL = (
objectNameSingular: string,
recordId: string,
viewId?: string | null | undefined,
) => {
return `/object/${objectNameSingular}/${recordId}${
viewId ? `?view=${viewId}` : ''
}`;
};

View File

@ -7,8 +7,8 @@ export const mockPerformance = {
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
primaryLinkLabelSingular: 'Person',
primaryLinkLabelPlural: 'People',
description: 'A person',
icon: 'IconUser',
isCustom: false,
@ -17,7 +17,7 @@ export const mockPerformance = {
isSystem: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
labelIdentifierFieldMetadataId: null,
primaryLinkLabelIdentifierFieldMetadataId: null,
imageIdentifierFieldMetadataId: null,
fields: [
{
@ -25,7 +25,7 @@ export const mockPerformance = {
id: '9058056e-36b3-4a3f-9037-f0bca9744296',
type: 'RELATION',
name: 'company',
label: 'Company',
primaryLinkLabel: 'Company',
description: 'Contacts company',
icon: 'IconBuildingSkyscraper',
isCustom: false,
@ -85,7 +85,7 @@ export const mockPerformance = {
id: 'bd504d22-ecae-4228-8729-5c770a174336',
type: 'TEXT',
name: 'avatarUrl',
label: 'Avatar',
primaryLinkLabel: 'Avatar',
description: 'Contacts avatar',
icon: 'IconFileUpload',
isCustom: false,
@ -105,7 +105,7 @@ export const mockPerformance = {
id: '21238919-5d92-402e-8124-367948ef86e6',
type: 'TEXT',
name: 'city',
label: 'City',
primaryLinkLabel: 'City',
description: 'Contacts city',
icon: 'IconMap',
isCustom: false,
@ -125,7 +125,7 @@ export const mockPerformance = {
id: '78edf4bb-c6a6-449e-b9db-20a575b97d5e',
type: 'RELATION',
name: 'activityTargets',
label: 'Activities',
primaryLinkLabel: 'Activities',
description: 'Activities tied to the contact',
icon: 'IconCheckbox',
isCustom: false,
@ -185,7 +185,7 @@ export const mockPerformance = {
id: '4128b168-1439-441e-bb6a-223fa1276642',
type: 'RELATION',
name: 'pointOfContactForOpportunities',
label: 'POC for Opportunities',
primaryLinkLabel: 'POC for Opportunities',
description: 'Point of Contact for Opportunities',
icon: 'IconTargetArrow',
isCustom: false,
@ -245,7 +245,7 @@ export const mockPerformance = {
id: '3db3a6ac-a960-42bd-8375-59ab6c4837d6',
type: 'RELATION',
name: 'calendarEventParticipants',
label: 'Calendar Event Participants',
primaryLinkLabel: 'Calendar Event Participants',
description: 'Calendar Event Participants',
icon: 'IconCalendar',
isCustom: false,
@ -305,7 +305,7 @@ export const mockPerformance = {
id: 'f0a290ac-fa74-48da-a77f-db221cb0206a',
type: 'DATE_TIME',
name: 'createdAt',
label: 'Creation date',
primaryLinkLabel: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isCustom: false,
@ -325,7 +325,7 @@ export const mockPerformance = {
id: 'b96e0e45-278c-44b6-a601-30ba24592dd6',
type: 'RELATION',
name: 'favorites',
label: 'Favorites',
primaryLinkLabel: 'Favorites',
description: 'Favorites linked to the contact',
icon: 'IconHeart',
isCustom: false,
@ -385,7 +385,7 @@ export const mockPerformance = {
id: '430af81e-2a8c-4ce2-9969-c0f0e91818bb',
type: 'LINK',
name: 'linkedinLink',
label: 'Linkedin',
primaryLinkLabel: 'Linkedin',
description: 'Contacts Linkedin account',
icon: 'IconBrandLinkedin',
isCustom: false,
@ -397,8 +397,8 @@ export const mockPerformance = {
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
url: "''",
label: "''",
primaryLinkUrl: "''",
primaryLinkLabel: "''",
},
options: null,
relationDefinition: null,
@ -408,7 +408,7 @@ export const mockPerformance = {
id: 'c885c3d9-63e2-4c0d-b7d6-ee9e867eb1f6',
type: 'RELATION',
name: 'attachments',
label: 'Attachments',
primaryLinkLabel: 'Attachments',
description: 'Attachments linked to the contact.',
icon: 'IconFileImport',
isCustom: false,
@ -468,7 +468,7 @@ export const mockPerformance = {
id: 'cc63e38f-56d6-495e-a545-edf101e400cf',
type: 'TEXT',
name: 'phone',
label: 'Phone',
primaryLinkLabel: 'Phone',
description: 'Contacts phone number',
icon: 'IconPhone',
isCustom: false,
@ -488,7 +488,7 @@ export const mockPerformance = {
id: '0084a5f7-cb57-4cd5-8b14-93ab51c21f45',
type: 'POSITION',
name: 'position',
label: 'Position',
primaryLinkLabel: 'Position',
description: 'Person record Position',
icon: 'IconHierarchy2',
isCustom: false,
@ -508,7 +508,7 @@ export const mockPerformance = {
id: 'ca54aa1d-1ecb-486c-99ea-b8240871a0da',
type: 'EMAIL',
name: 'email',
label: 'Email',
primaryLinkLabel: 'Email',
description: 'Contacts Email',
icon: 'IconMail',
isCustom: false,
@ -528,7 +528,7 @@ export const mockPerformance = {
id: '54561a8e-b918-471b-a363-5a77f49cd348',
type: 'TEXT',
name: 'jobTitle',
label: 'Job Title',
primaryLinkLabel: 'Job Title',
description: 'Contacts job title',
icon: 'IconBriefcase',
isCustom: false,
@ -548,7 +548,7 @@ export const mockPerformance = {
id: '4e844d31-f117-443c-8754-8cb63e963ecc',
type: 'DATE_TIME',
name: 'updatedAt',
label: 'Update date',
primaryLinkLabel: 'Update date',
description: 'Update date',
icon: 'IconCalendar',
isCustom: false,
@ -568,7 +568,7 @@ export const mockPerformance = {
id: '4ddd38df-d9a3-4889-a39f-1e336cd8113c',
type: 'UUID',
name: 'companyId',
label: 'Company id (foreign key)',
primaryLinkLabel: 'Company id (foreign key)',
description: 'Contacts company id foreign key',
icon: 'IconBuildingSkyscraper',
isCustom: false,
@ -588,7 +588,7 @@ export const mockPerformance = {
id: 'e6922ecb-7a3a-4520-b001-bbf95fc33197',
type: 'RELATION',
name: 'timelineActivities',
label: 'Events',
primaryLinkLabel: 'Events',
description: 'Events linked to the company',
icon: 'IconTimelineEvent',
isCustom: false,
@ -648,7 +648,7 @@ export const mockPerformance = {
id: '07a8a574-ed28-4015-b456-c01ff3050e2b',
type: 'FULL_NAME',
name: 'name',
label: 'Name',
primaryLinkLabel: 'Name',
description: 'Contacts name',
icon: 'IconUser',
isCustom: false,
@ -671,7 +671,7 @@ export const mockPerformance = {
id: 'c470144b-6692-47cb-a28f-04610d9d641c',
type: 'LINK',
name: 'xLink',
label: 'X',
primaryLinkLabel: 'X',
description: 'Contacts X/Twitter account',
icon: 'IconBrandX',
isCustom: false,
@ -683,8 +683,8 @@ export const mockPerformance = {
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
url: "''",
label: "''",
primaryLinkUrl: "''",
primaryLinkLabel: "''",
},
options: null,
relationDefinition: null,
@ -694,7 +694,7 @@ export const mockPerformance = {
id: 'c692aa2c-e88e-4aff-b77e-b9ebf26509e3',
type: 'RELATION',
name: 'messageParticipants',
label: 'Message Participants',
primaryLinkLabel: 'Message Participants',
description: 'Message Participants',
icon: 'IconUserCircle',
isCustom: false,
@ -754,7 +754,7 @@ export const mockPerformance = {
id: '66d33eae-71be-49fa-ad7a-3e10ac53dfba',
type: 'UUID',
name: 'id',
label: 'Id',
primaryLinkLabel: 'Id',
description: 'Id',
icon: 'Icon123',
isCustom: false,
@ -789,8 +789,8 @@ export const mockPerformance = {
domainName: 'google.com',
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
name: 'Google',
annualRecurringRevenue: {
@ -810,8 +810,8 @@ export const mockPerformance = {
updatedAt: '2024-05-01T13:16:29.046Z',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
},
id: '20202020-2d40-4e49-8df4-9c6a049191df',
@ -819,13 +819,13 @@ export const mockPerformance = {
phone: '+33788901235',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
tEst: '',
position: 15,
@ -835,8 +835,8 @@ export const mockPerformance = {
domainName: 'microsoft.com',
xLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
name: 'Microsoft',
annualRecurringRevenue: {
@ -854,13 +854,13 @@ export const mockPerformance = {
updatedAt: '2024-05-01T13:16:29.046Z',
linkedinLink: {
__typename: 'Link',
label: '',
url: '',
primaryLinkLabel: '',
primaryLinkUrl: '',
},
},
fieldDefinition: {
fieldMetadataId: '4e79f0b7-d100-4e89-a07b-315a710b8059',
label: 'Company',
primaryLinkLabel: 'Company',
metadata: {
fieldName: 'company',
placeHolder: 'Company',
@ -885,7 +885,7 @@ export const mockPerformance = {
visibleTableColumns: [
{
fieldMetadataId: '07a8a574-ed28-4015-b456-c01ff3050e2b',
label: 'Name',
primaryLinkLabel: 'Name',
metadata: {
fieldName: 'name',
placeHolder: 'Name',
@ -910,7 +910,7 @@ export const mockPerformance = {
},
{
fieldMetadataId: 'ca54aa1d-1ecb-486c-99ea-b8240871a0da',
label: 'Email',
primaryLinkLabel: 'Email',
metadata: {
fieldName: 'email',
placeHolder: 'Email',
@ -932,7 +932,7 @@ export const mockPerformance = {
},
{
fieldMetadataId: '9058056e-36b3-4a3f-9037-f0bca9744296',
label: 'Company',
primaryLinkLabel: 'Company',
metadata: {
fieldName: 'company',
placeHolder: 'Company',
@ -956,7 +956,7 @@ export const mockPerformance = {
},
{
fieldMetadataId: 'cc63e38f-56d6-495e-a545-edf101e400cf',
label: 'Phone',
primaryLinkLabel: 'Phone',
metadata: {
fieldName: 'phone',
placeHolder: 'Phone',
@ -978,7 +978,7 @@ export const mockPerformance = {
},
{
fieldMetadataId: 'f0a290ac-fa74-48da-a77f-db221cb0206a',
label: 'Creation date',
primaryLinkLabel: 'Creation date',
metadata: {
fieldName: 'createdAt',
placeHolder: 'Creation date',
@ -1000,7 +1000,7 @@ export const mockPerformance = {
},
{
fieldMetadataId: '21238919-5d92-402e-8124-367948ef86e6',
label: 'City',
primaryLinkLabel: 'City',
metadata: {
fieldName: 'city',
placeHolder: 'City',
@ -1022,7 +1022,7 @@ export const mockPerformance = {
},
{
fieldMetadataId: '54561a8e-b918-471b-a363-5a77f49cd348',
label: 'Job Title',
primaryLinkLabel: 'Job Title',
metadata: {
fieldName: 'jobTitle',
placeHolder: 'Job Title',
@ -1044,7 +1044,7 @@ export const mockPerformance = {
},
{
fieldMetadataId: '430af81e-2a8c-4ce2-9969-c0f0e91818bb',
label: 'Linkedin',
primaryLinkLabel: 'Linkedin',
metadata: {
fieldName: 'linkedinLink',
placeHolder: 'Linkedin',
@ -1063,13 +1063,13 @@ export const mockPerformance = {
isSortable: false,
isFilterable: true,
defaultValue: {
url: "''",
label: "''",
primaryLinkUrl: "''",
primaryLinkLabel: "''",
},
},
{
fieldMetadataId: 'c470144b-6692-47cb-a28f-04610d9d641c',
label: 'X',
primaryLinkLabel: 'X',
metadata: {
fieldName: 'xLink',
placeHolder: 'X',
@ -1088,8 +1088,8 @@ export const mockPerformance = {
isSortable: false,
isFilterable: true,
defaultValue: {
url: "''",
label: "''",
primaryLinkUrl: "''",
primaryLinkLabel: "''",
},
},
],

View File

@ -0,0 +1 @@
export const ROW_HEIGHT = 32;

View File

@ -24,17 +24,18 @@ export const useSetRecordTableData = ({
return useRecoilCallback(
({ set, snapshot }) =>
<T extends ObjectRecord>(newEntityArray: T[], totalCount?: number) => {
for (const entity of newEntityArray) {
<T extends ObjectRecord>(newRecords: T[], totalCount?: number) => {
for (const record of newRecords) {
// TODO: refactor with scoped state later
const currentEntity = snapshot
.getLoadable(recordStoreFamilyState(entity.id))
const currentRecord = snapshot
.getLoadable(recordStoreFamilyState(record.id))
.getValue();
if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) {
set(recordStoreFamilyState(entity.id), entity);
if (JSON.stringify(currentRecord) !== JSON.stringify(record)) {
set(recordStoreFamilyState(record.id), record);
}
}
const currentRowIds = getSnapshotValue(snapshot, tableRowIdsState);
const hasUserSelectedAllRows = getSnapshotValue(
@ -42,7 +43,7 @@ export const useSetRecordTableData = ({
hasUserSelectedAllRowsState,
);
const entityIds = newEntityArray.map((entity) => entity.id);
const entityIds = newRecords.map((entity) => entity.id);
if (!isDeeplyEqual(currentRowIds, entityIds)) {
set(tableRowIdsState, entityIds);

View File

@ -4,6 +4,7 @@ import { useDebouncedCallback } from 'use-debounce';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
@ -13,11 +14,8 @@ import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { isNonEmptyString } from '@sniptt/guards';
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
export const ROW_HEIGHT = 32;
export const RecordTableBodyEffect = () => {
const { objectNameSingular } = useContext(RecordTableContext);
@ -45,6 +43,7 @@ export const RecordTableBodyEffect = () => {
isRecordTableScrolledTopComponentState,
);
// TODO: move this outside because it might cause way too many re-renders for other hooks
useEffect(() => {
setIsRecordTableScrolledTop(scrollTop === 0);
if (scrollTop > 0) {
@ -83,9 +82,6 @@ export const RecordTableBodyEffect = () => {
}
}, [scrollLeft, setIsRecordTableScrolledLeft]);
const rowHeight = ROW_HEIGHT;
const viewportHeight = records.length * rowHeight;
const [lastShowPageRecordId, setLastShowPageRecordId] = useRecoilState(
lastShowPageRecordIdState,
);
@ -121,8 +117,6 @@ export const RecordTableBodyEffect = () => {
setLastShowPageRecordId,
]);
useScrollRestoration(viewportHeight);
useEffect(() => {
if (!loading) {
setRecordTableData(records, totalCount);

View File

@ -68,9 +68,6 @@ export const RecordTableCellSoftFocusMode = ({
},
TableHotkeyScope.TableSoftFocus,
[clearField, isFieldClearable, isFieldInputOnly],
{
enabled: !isFieldInputOnly,
},
);
useScopedHotkeys(

View File

@ -4,18 +4,25 @@ import { useRecoilValue } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { Checkbox } from '@/ui/input/components/Checkbox';
import { useTheme } from '@emotion/react';
const StyledContainer = styled.div`
align-items: center;
display: flex;
height: 32px;
justify-content: center;
background-color: ${({ theme }) => theme.background.primary};
`;
const StyledColumnHeaderCell = styled.th`
background-color: ${({ theme }) => theme.background.primary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-right: transparent;
border-top: 1px solid ${({ theme }) => theme.border.color.light};
max-width: 30px;
min-width: 30px;
width: 30px;
`;
export const RecordTableHeaderCheckboxColumn = () => {
const { allRowsSelectedStatusSelector } = useRecordTableStates();
@ -36,19 +43,8 @@ export const RecordTableHeaderCheckboxColumn = () => {
}
};
const theme = useTheme();
return (
<th
style={{
borderBottom: `1px solid ${theme.border.color.light}`,
borderTop: `1px solid ${theme.border.color.light}`,
width: 30,
minWidth: 30,
maxWidth: 30,
borderRight: 'transparent',
}}
>
<StyledColumnHeaderCell>
<StyledContainer>
<Checkbox
checked={checked}
@ -56,6 +52,6 @@ export const RecordTableHeaderCheckboxColumn = () => {
indeterminate={indeterminate}
/>
</StyledContainer>
</th>
</StyledColumnHeaderCell>
);
};

View File

@ -1,10 +1,15 @@
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { ThemeContext } from 'twenty-ui';
const StyledTh = styled.th`
const StyledTh = styled.th<{ backgroundColor: string }>`
background: ${({ backgroundColor }) => backgroundColor};
border-bottom: none;
border-top: none;
`;
export const RecordTableHeaderDragDropColumn = () => {
return <StyledTh></StyledTh>;
const { theme } = useContext(ThemeContext);
return <StyledTh backgroundColor={theme.background.primary}></StyledTh>;
};

View File

@ -4,6 +4,7 @@ import { ComponentStateKey } from '@/ui/utilities/state/component-state/types/Co
import { ColumnDefinition } from '../../types/ColumnDefinition';
// TODO: separate scope contexts from event contexts
type RecordTableScopeInternalContextProps = ComponentStateKey & {
onColumnsChange: (columns: ColumnDefinition<FieldMetadata>[]) => void;
};

View File

@ -0,0 +1,6 @@
export const buildIndexTablePageURL = (
objectNamePlural: string,
viewId?: string | null | undefined,
) => {
return `/objects/${objectNamePlural}${viewId ? `?view=${viewId}` : ''}`;
};

View File

@ -1,14 +1,14 @@
import { ReactNode } from 'react';
import { gql } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
import { useSpreadsheetRecordImport } from '../useSpreadsheetRecordImport';
import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext';
import { useOpenObjectRecordsSpreasheetImportDialog } from '../hooks/useOpenObjectRecordsSpreasheetImportDialog';
const companyId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a';
@ -27,12 +27,14 @@ const companyMocks = [
createCompanies(data: $data, upsert: $upsert) {
__typename
xLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
label
url
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
domainName
annualRecurringRevenue {
@ -62,7 +64,6 @@ const companyMocks = [
variables: {
data: [
{
address: 'test',
domainName: 'example.com',
employees: 0,
idealCustomerProfile: true,
@ -94,67 +95,81 @@ const fakeCsv = () => {
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider mocks={companyMocks} addTypename={false}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<SnackBarManagerScopeInternalContext.Provider
value={{ scopeId: 'snack-bar-manager' }}
>
{children}
</SnackBarProviderScope>
</SnackBarManagerScopeInternalContext.Provider>
</MockedProvider>
</RecoilRoot>
);
// TODO: improve object metadata item seeds to have more field types to add tests on composite fields here
describe('useSpreadsheetCompanyImport', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
const spreadsheetImport = useRecoilValue(spreadsheetImportState);
const { openRecordSpreadsheetImport } = useSpreadsheetRecordImport(
const spreadsheetImportDialog = useRecoilValue(
spreadsheetImportDialogState,
);
const {
openObjectRecordsSpreasheetImportDialog: openRecordSpreadsheetImport,
} = useOpenObjectRecordsSpreasheetImportDialog(
CoreObjectNameSingular.Company,
);
return { openRecordSpreadsheetImport, spreadsheetImport };
return {
openRecordSpreadsheetImport,
spreadsheetImportDialog,
};
},
{
wrapper: Wrapper,
},
);
const { spreadsheetImport, openRecordSpreadsheetImport } = result.current;
const { spreadsheetImportDialog, openRecordSpreadsheetImport } =
result.current;
expect(spreadsheetImport.isOpen).toBe(false);
expect(spreadsheetImport.options).toBeNull();
expect(spreadsheetImportDialog.isOpen).toBe(false);
expect(spreadsheetImportDialog.options).toBeNull();
await act(async () => {
openRecordSpreadsheetImport();
});
const { spreadsheetImport: updatedImport } = result.current;
const { spreadsheetImportDialog: spreadsheetImportDialogAfterOpen } =
result.current;
expect(updatedImport.isOpen).toBe(true);
expect(updatedImport.options).toHaveProperty('onSubmit');
expect(updatedImport.options?.onSubmit).toBeInstanceOf(Function);
expect(updatedImport.options).toHaveProperty('fields');
expect(Array.isArray(updatedImport.options?.fields)).toBe(true);
expect(spreadsheetImportDialogAfterOpen.isOpen).toBe(true);
expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty('onSubmit');
expect(spreadsheetImportDialogAfterOpen.options?.onSubmit).toBeInstanceOf(
Function,
);
expect(spreadsheetImportDialogAfterOpen.options).toHaveProperty('fields');
expect(
Array.isArray(spreadsheetImportDialogAfterOpen.options?.fields),
).toBe(true);
act(() => {
updatedImport.options?.onSubmit(
spreadsheetImportDialogAfterOpen.options?.onSubmit(
{
validData: [
validStructuredRows: [
{
id: companyId,
name: 'Example Company',
domainName: 'example.com',
idealCustomerProfile: true,
address: 'test',
employees: '0',
},
],
invalidData: [],
all: [
invalidStructuredRows: [],
allStructuredRows: [
{
id: companyId,
name: 'Example Company',
domainName: 'example.com',
__index: 'cbc3985f-dde9-46d1-bae2-c124141700ac',
idealCustomerProfile: true,
address: 'test',
employees: '0',
},
],

View File

@ -0,0 +1,28 @@
import {
FieldAddressValue,
FieldCurrencyValue,
FieldFullNameValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const COMPOSITE_FIELD_IMPORT_LABELS = {
[FieldMetadataType.FullName]: {
firstNameLabel: 'First Name',
lastNameLabel: 'Last Name',
} satisfies CompositeFieldLabels<FieldFullNameValue>,
[FieldMetadataType.Currency]: {
currencyCodeLabel: 'Currency Code',
amountMicrosLabel: 'Amount',
} satisfies CompositeFieldLabels<FieldCurrencyValue>,
[FieldMetadataType.Address]: {
addressStreet1Label: 'Address 1',
addressStreet2Label: 'Address 2',
addressCityLabel: 'City',
addressPostcodeLabel: 'Post Code',
addressStateLabel: 'State',
addressCountryLabel: 'Country',
addressLatLabel: 'Latitude',
addressLngLabel: 'Longitude',
} satisfies CompositeFieldLabels<FieldAddressValue>,
};

View File

@ -0,0 +1,128 @@
import { useIcons } from 'twenty-ui';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport';
import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/util/getSpreadSheetFieldValidationDefinitions';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useBuildAvailableFieldsForImport = () => {
const { getIcon } = useIcons();
const buildAvailableFieldsForImport = (
fieldMetadataItems: FieldMetadataItem[],
) => {
const availableFieldsForImport: AvailableFieldForImport[] = [];
for (const fieldMetadataItem of fieldMetadataItems) {
if (fieldMetadataItem.type === FieldMetadataType.FullName) {
const { firstNameLabel, lastNameLabel } =
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.FullName];
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${firstNameLabel} (${fieldMetadataItem.label})`,
key: `${firstNameLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${firstNameLabel} (${fieldMetadataItem.label})`,
),
});
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${lastNameLabel} (${fieldMetadataItem.label})`,
key: `${lastNameLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${lastNameLabel} (${fieldMetadataItem.label})`,
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.Relation) {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label + ' (ID)',
key: fieldMetadataItem.name,
fieldType: {
type: 'input',
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label + ' (ID)',
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.Currency) {
const { currencyCodeLabel, amountMicrosLabel } =
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.Currency];
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${currencyCodeLabel} (${fieldMetadataItem.label})`,
key: `${currencyCodeLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${currencyCodeLabel} (${fieldMetadataItem.label})`,
),
});
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${amountMicrosLabel} (${fieldMetadataItem.label})`,
key: `${amountMicrosLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
FieldMetadataType.Number,
`${amountMicrosLabel} (${fieldMetadataItem.label})`,
),
});
} else if (fieldMetadataItem.type === FieldMetadataType.Address) {
Object.entries(
COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.Address],
).forEach(([_, fieldLabel]) => {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: `${fieldLabel} (${fieldMetadataItem.label})`,
key: `${fieldLabel} (${fieldMetadataItem.name})`,
fieldType: {
type: 'input',
},
fieldValidationDefinitions:
getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
`${fieldLabel} (${fieldMetadataItem.label})`,
),
});
});
} else {
availableFieldsForImport.push({
icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
key: fieldMetadataItem.name,
fieldType: {
type: 'input',
},
fieldValidationDefinitions: getSpreadSheetFieldValidationDefinitions(
fieldMetadataItem.type,
fieldMetadataItem.label,
),
});
}
}
return availableFieldsForImport;
};
return { buildAvailableFieldsForImport };
};

View File

@ -0,0 +1,78 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport';
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow';
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useOpenObjectRecordsSpreasheetImportDialog = (
objectNameSingular: string,
) => {
const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog<any>();
const { enqueueSnackBar } = useSnackBar();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { createManyRecords } = useCreateManyRecords({
objectNameSingular,
});
const { buildAvailableFieldsForImport } = useBuildAvailableFieldsForImport();
const openObjectRecordsSpreasheetImportDialog = (
options?: Omit<
SpreadsheetImportDialogOptions<any>,
'fields' | 'isOpen' | 'onClose'
>,
) => {
const availableFieldMetadataItems = objectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
fieldMetadataItem.isActive &&
(!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
fieldMetadataItem.name !== 'createdAt' &&
(fieldMetadataItem.type !== FieldMetadataType.Relation ||
fieldMetadataItem.toRelationMetadata),
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
);
const availableFields = buildAvailableFieldsForImport(
availableFieldMetadataItems,
);
openSpreadsheetImportDialog({
...options,
onSubmit: async (data) => {
const createInputs = data.validStructuredRows.map((record) => {
const fieldMapping: Record<string, any> =
buildRecordFromImportedStructuredRow(
record,
availableFieldMetadataItems,
);
return fieldMapping;
});
try {
await createManyRecords(createInputs, true);
} catch (error: any) {
enqueueSnackBar(error?.message || 'Something went wrong', {
variant: SnackBarVariant.Error,
});
}
},
fields: availableFields,
});
};
return {
openObjectRecordsSpreasheetImportDialog,
};
};

View File

@ -0,0 +1,12 @@
import { FieldValidationDefinition } from '@/spreadsheet-import/types';
import { IconComponent } from 'twenty-ui';
export type AvailableFieldForImport = {
icon: IconComponent;
label: string;
key: string;
fieldType: {
type: 'input' | 'checkbox';
};
fieldValidationDefinitions?: FieldValidationDefinition[];
};

View File

@ -0,0 +1,5 @@
import { KeyOfCompositeField } from '@/object-record/spreadsheet-import/types/KeyOfCompositeField';
export type CompositeFieldLabels<T> = {
[key in `${KeyOfCompositeField<T>}Label`]: string;
};

View File

@ -0,0 +1,3 @@
export type KeyOfCompositeField<T> = keyof Omit<T, '__typename'> extends string
? keyof Omit<T, '__typename'>
: never;

View File

@ -1,202 +0,0 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useIcons } from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { getSpreadSheetValidation } from '@/object-record/spreadsheet-import/util/getSpreadSheetValidation';
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
import { Field, SpreadsheetOptions } from '@/spreadsheet-import/types';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
const firstName = 'Firstname';
const lastName = 'Lastname';
export const useSpreadsheetRecordImport = (objectNameSingular: string) => {
const { openSpreadsheetImport } = useSpreadsheetImport<any>();
const { enqueueSnackBar } = useSnackBar();
const { getIcon } = useIcons();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const fields = objectMetadataItem.fields
.filter(
(x) =>
x.isActive &&
(!x.isSystem || x.name === 'id') &&
x.name !== 'createdAt' &&
(x.type !== FieldMetadataType.Relation || x.toRelationMetadata),
)
.sort((a, b) => a.name.localeCompare(b.name));
const templateFields: Field<string>[] = [];
for (const field of fields) {
if (field.type === FieldMetadataType.FullName) {
templateFields.push({
icon: getIcon(field.icon),
label: `${firstName} (${field.label})`,
key: `${firstName} (${field.name})`,
fieldType: {
type: 'input',
},
validations: getSpreadSheetValidation(
field.type,
`${firstName} (${field.label})`,
),
});
templateFields.push({
icon: getIcon(field.icon),
label: `${lastName} (${field.label})`,
key: `${lastName} (${field.name})`,
fieldType: {
type: 'input',
},
validations: getSpreadSheetValidation(
field.type,
`${lastName} (${field.label})`,
),
});
} else if (field.type === FieldMetadataType.Relation) {
templateFields.push({
icon: getIcon(field.icon),
label: field.label + ' (ID)',
key: field.name,
fieldType: {
type: 'input',
},
validations: getSpreadSheetValidation(
field.type,
field.label + ' (ID)',
),
});
} else if (field.type === FieldMetadataType.Select) {
templateFields.push({
icon: getIcon(field.icon),
label: field.label,
key: field.name,
fieldType: {
type: 'select',
options:
field.options?.map((option) => ({
label: option.label,
value: option.value,
})) || [],
},
validations: getSpreadSheetValidation(
field.type,
field.label + ' (ID)',
),
});
} else if (field.type === FieldMetadataType.Boolean) {
templateFields.push({
icon: getIcon(field.icon),
label: field.label,
key: field.name,
fieldType: {
type: 'checkbox',
},
validations: getSpreadSheetValidation(field.type, field.label),
});
} else {
templateFields.push({
icon: getIcon(field.icon),
label: field.label,
key: field.name,
fieldType: {
type: 'input',
},
validations: getSpreadSheetValidation(field.type, field.label),
});
}
}
const { createManyRecords } = useCreateManyRecords({
objectNameSingular,
});
const openRecordSpreadsheetImport = (
options?: Omit<SpreadsheetOptions<any>, 'fields' | 'isOpen' | 'onClose'>,
) => {
openSpreadsheetImport({
...options,
onSubmit: async (data) => {
const createInputs = data.validData.map((record) => {
const fieldMapping: Record<string, any> = {};
for (const field of fields) {
const value = record[field.name];
switch (field.type) {
case FieldMetadataType.Boolean:
if (value !== undefined) {
fieldMapping[field.name] = value === 'true' || value === true;
}
break;
case FieldMetadataType.Number:
case FieldMetadataType.Numeric:
if (value !== undefined) {
fieldMapping[field.name] = Number(value);
}
break;
case FieldMetadataType.Currency:
if (value !== undefined) {
fieldMapping[field.name] = {
amountMicros: Number(value),
currencyCode: 'USD',
};
}
break;
case FieldMetadataType.Link:
if (value !== undefined) {
fieldMapping[field.name] = {
label: field.name,
url: value || null,
};
}
break;
case FieldMetadataType.Relation:
if (
isDefined(value) &&
(isNonEmptyString(value) || value !== false)
) {
fieldMapping[field.name + 'Id'] = value;
}
break;
case FieldMetadataType.FullName:
if (
isDefined(
record[`${firstName} (${field.name})`] ||
record[`${lastName} (${field.name})`],
)
) {
fieldMapping[field.name] = {
firstName: record[`${firstName} (${field.name})`] || '',
lastName: record[`${lastName} (${field.name})`] || '',
};
}
break;
default:
if (value !== undefined) {
fieldMapping[field.name] = value;
}
break;
}
}
return fieldMapping;
});
try {
await createManyRecords(createInputs, true);
} catch (error: any) {
enqueueSnackBar(error?.message || 'Something went wrong', {
variant: SnackBarVariant.Error,
});
}
},
fields: templateFields,
});
};
return { openRecordSpreadsheetImport };
};

View File

@ -0,0 +1,147 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels';
import { ImportedStructuredRow } from '@/spreadsheet-import/types';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { castToString } from '~/utils/castToString';
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
export const buildRecordFromImportedStructuredRow = (
importedStructuredRow: ImportedStructuredRow<any>,
fields: FieldMetadataItem[],
) => {
const recordToBuild: Record<string, any> = {};
const {
ADDRESS: {
addressCityLabel,
addressCountryLabel,
addressLatLabel,
addressLngLabel,
addressPostcodeLabel,
addressStateLabel,
addressStreet1Label,
addressStreet2Label,
},
CURRENCY: { amountMicrosLabel, currencyCodeLabel },
FULL_NAME: { firstNameLabel, lastNameLabel },
} = COMPOSITE_FIELD_IMPORT_LABELS;
for (const field of fields) {
const importedFieldValue = importedStructuredRow[field.name];
switch (field.type) {
case FieldMetadataType.Boolean:
recordToBuild[field.name] =
importedFieldValue === 'true' || importedFieldValue === true;
break;
case FieldMetadataType.Number:
case FieldMetadataType.Numeric:
recordToBuild[field.name] = Number(importedFieldValue);
break;
case FieldMetadataType.Currency:
if (
isDefined(
importedStructuredRow[`${amountMicrosLabel} (${field.name})`],
) ||
isDefined(
importedStructuredRow[`${currencyCodeLabel} (${field.name})`],
)
) {
recordToBuild[field.name] = {
amountMicros: convertCurrencyAmountToCurrencyMicros(
Number(
importedStructuredRow[`${amountMicrosLabel} (${field.name})`],
),
),
currencyCode:
importedStructuredRow[`${currencyCodeLabel} (${field.name})`] ||
'USD',
};
}
break;
case FieldMetadataType.Address: {
if (
isDefined(
importedStructuredRow[`${addressStreet1Label} (${field.name})`] ||
importedStructuredRow[`${addressStreet2Label} (${field.name})`] ||
importedStructuredRow[`${addressCityLabel} (${field.name})`] ||
importedStructuredRow[
`${addressPostcodeLabel} (${field.name})`
] ||
importedStructuredRow[`${addressStateLabel} (${field.name})`] ||
importedStructuredRow[`${addressCountryLabel} (${field.name})`] ||
importedStructuredRow[`${addressLatLabel} (${field.name})`] ||
importedStructuredRow[`${addressLngLabel} (${field.name})`],
)
) {
recordToBuild[field.name] = {
addressStreet1: castToString(
importedStructuredRow[`${addressStreet1Label} (${field.name})`],
),
addressStreet2: castToString(
importedStructuredRow[`${addressStreet2Label} (${field.name})`],
),
addressCity: castToString(
importedStructuredRow[`${addressCityLabel} (${field.name})`],
),
addressPostcode: castToString(
importedStructuredRow[`${addressPostcodeLabel} (${field.name})`],
),
addressState: castToString(
importedStructuredRow[`${addressStateLabel} (${field.name})`],
),
addressCountry: castToString(
importedStructuredRow[`${addressCountryLabel} (${field.name})`],
),
addressLat: Number(
importedStructuredRow[`${addressLatLabel} (${field.name})`],
),
addressLng: Number(
importedStructuredRow[`${addressLngLabel} (${field.name})`],
),
} satisfies FieldAddressValue;
}
break;
}
case FieldMetadataType.Link:
if (importedFieldValue !== undefined) {
recordToBuild[field.name] = {
label: field.name,
url: importedFieldValue || null,
};
}
break;
case FieldMetadataType.Relation:
if (
isDefined(importedFieldValue) &&
(isNonEmptyString(importedFieldValue) || importedFieldValue !== false)
) {
recordToBuild[field.name + 'Id'] = importedFieldValue;
}
break;
case FieldMetadataType.FullName:
if (
isDefined(
importedStructuredRow[`${firstNameLabel} (${field.name})`] ??
importedStructuredRow[`${lastNameLabel} (${field.name})`],
)
) {
recordToBuild[field.name] = {
firstName:
importedStructuredRow[`${firstNameLabel} (${field.name})`] ?? '',
lastName:
importedStructuredRow[`${lastNameLabel} (${field.name})`] ?? '',
};
}
break;
default:
recordToBuild[field.name] = importedFieldValue;
break;
}
}
return recordToBuild;
};

View File

@ -1,14 +1,37 @@
import { isValidPhoneNumber } from 'libphonenumber-js';
import { isValidUuid } from '@/object-record/spreadsheet-import/util/isValidUuid';
import { Validation } from '@/spreadsheet-import/types';
import { FieldValidationDefinition } from '@/spreadsheet-import/types';
import { isDefined } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isValidUuid } from '~/utils/isValidUuid';
export const getSpreadSheetValidation = (
export const getSpreadSheetFieldValidationDefinitions = (
type: FieldMetadataType,
fieldName: string,
): Validation[] => {
): FieldValidationDefinition[] => {
switch (type) {
case FieldMetadataType.FullName:
return [
{
rule: 'object',
isValid: ({
firstName,
lastName,
}: {
firstName: string;
lastName: string;
}) => {
return (
isDefined(firstName) &&
isDefined(lastName) &&
typeof firstName === 'string' &&
typeof lastName === 'string'
);
},
errorMessage: fieldName + ' must be a full name',
level: 'error',
},
];
case FieldMetadataType.Number:
return [
{

View File

@ -8,7 +8,7 @@ import { getAvatarUrl } from '@/object-metadata/utils/getAvatarUrl';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { getLabelIdentifierFieldValue } from '@/object-metadata/utils/getLabelIdentifierFieldValue';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { isFieldChipDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldChipDisplay';
import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay';
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -34,7 +34,7 @@ export const getRecordChipGenerators = (
(fieldMetadataItem) =>
labelIdentifierFieldMetadataItem?.id === fieldMetadataItem.id ||
fieldMetadataItem.type === FieldMetadataType.Relation ||
isFieldChipDisplay(
isFieldIdentifierDisplay(
fieldMetadataItem,
isLabelIdentifierField({
fieldMetadataItem: fieldMetadataItem,

View File

@ -12,14 +12,14 @@ export type Person = {
avatarUrl?: string;
jobTitle: string;
linkedinLink: {
__typename?: 'Link';
url: string;
label: string;
__typename?: 'Links';
primaryLinkUrl: string;
primaryLinkLabel: string;
};
xLink: {
__typename?: 'Link';
url: string;
label: string;
__typename?: 'Links';
primaryLinkUrl: string;
primaryLinkLabel: string;
};
city: string;
email: string;
@ -30,11 +30,5 @@ export type Person = {
__typename: 'Links';
primaryLinkUrl: string;
primaryLinkLabel: '';
secondaryLinks?:
| {
url: string;
label: string;
}[]
| null;
};
};

View File

@ -36,8 +36,8 @@ export const query = gql`
}
}
xLink {
label
url
primaryLinkLabel
primaryLinkUrl
}
id
pointOfContactForOpportunities {
@ -63,12 +63,12 @@ export const query = gql`
company {
id
xLink {
label
url
primaryLinkLabel
primaryLinkUrl
}
linkedinLink {
label
url
primaryLinkLabel
primaryLinkUrl
}
domainName
annualRecurringRevenue {
@ -139,8 +139,8 @@ export const query = gql`
}
phone
linkedinLink {
label
url
primaryLinkLabel
primaryLinkUrl
}
updatedAt
avatarUrl

View File

@ -1,8 +1,7 @@
import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { DropResult } from '@hello-pangea/dnd';
import { Key } from 'ts-key-enum';
import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { IconPlus } from 'twenty-ui';
import { z } from 'zod';
@ -21,8 +20,6 @@ import { CardContent } from '@/ui/layout/card/components/CardContent';
import { CardFooter } from '@/ui/layout/card/components/CardFooter';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { toSpliced } from '~/utils/array/toSpliced';
@ -189,19 +186,15 @@ export const SettingsDataModelFieldSelectForm = ({
setFormValue('options', newOptions);
};
useScopedHotkeys(
Key.Enter,
() => {
const newOptions = getOptionsWithNewOption();
const handleInputEnter = () => {
const newOptions = getOptionsWithNewOption();
setFormValue('options', newOptions);
setFormValue('options', newOptions);
const lastOptionId = newOptions[newOptions.length - 1].id;
const lastOptionId = newOptions[newOptions.length - 1].id;
setFocusedOptionId(lastOptionId);
},
AppHotkeyScope.App,
);
setFocusedOptionId(lastOptionId);
};
return (
<>
@ -225,6 +218,7 @@ export const SettingsDataModelFieldSelectForm = ({
<>
{options.map((option, index) => (
<DraggableItem
isInsideScrollableContainer
key={option.id}
draggableId={option.id}
index={index}
@ -270,6 +264,7 @@ export const SettingsDataModelFieldSelectForm = ({
onRemoveAsDefault={() =>
handleRemoveOptionAsDefault(option.value)
}
onInputEnter={handleInputEnter}
/>
}
/>

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useMemo } from 'react';
import {
ColorSample,
IconCheck,
@ -31,6 +31,7 @@ type SettingsDataModelFieldSelectFormOptionRowProps = {
onRemove?: () => void;
onSetAsDefault?: () => void;
onRemoveAsDefault?: () => void;
onInputEnter?: () => void;
option: FieldMetadataItemOption;
focused?: boolean;
};
@ -64,11 +65,10 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
onRemove,
onSetAsDefault,
onRemoveAsDefault,
onInputEnter,
option,
focused,
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const theme = useTheme();
const dropdownIds = useMemo(() => {
@ -84,11 +84,9 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
dropdownIds.actions,
);
useEffect(() => {
if (focused === true) {
inputRef.current?.focus();
}
}, [focused]);
const handleInputEnter = () => {
onInputEnter?.();
};
return (
<StyledRow className={className}>
@ -123,8 +121,6 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
}
/>
<StyledOptionInput
ref={inputRef}
disableHotkeys
value={option.label}
onChange={(label) =>
onChange({
@ -133,8 +129,10 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
value: getOptionValueFromLabel(label),
})
}
focused={focused}
RightIcon={isDefault ? IconCheck : undefined}
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
onInputEnter={handleInputEnter}
/>
<Dropdown
dropdownId={dropdownIds.actions}

View File

@ -1,12 +1,11 @@
import { Handle, Position } from 'reactflow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Handle, Position } from 'reactflow';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
type ObjectFieldRowProps = {
field: FieldMetadataItem;
@ -41,9 +40,7 @@ export const ObjectFieldRow = ({ field }: ObjectFieldRowProps) => {
return (
<StyledRow>
{Icon && <Icon size={theme.icon.size.md} />}
<StyledFieldName>
{capitalize(relatedObject?.namePlural ?? '')}
</StyledFieldName>
<StyledFieldName>{relatedObject?.labelPlural ?? ''}</StyledFieldName>
<Handle
type={field.toRelationMetadata ? 'source' : 'target'}
position={Position.Right}

View File

@ -0,0 +1,38 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useIcons } from 'twenty-ui';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
type ObjectFieldRowWithoutRelationProps = {
field: FieldMetadataItem;
};
const StyledRow = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
position: relative;
width: 100%;
padding: 0 ${({ theme }) => theme.spacing(2)};
`;
const StyledFieldName = styled.div`
color: ${({ theme }) => theme.font.color.primary};
`;
export const ObjectFieldRowWithoutRelation = ({
field,
}: ObjectFieldRowWithoutRelationProps) => {
const { getIcon } = useIcons();
const theme = useTheme();
const Icon = getIcon(field?.icon);
return (
<StyledRow>
{Icon && <Icon size={theme.icon.size.md} />}
<StyledFieldName>{field.label}</StyledFieldName>
</StyledRow>
);
};

Some files were not shown because too many files have changed in this diff Show More