mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 03:17:40 +03:00
Merge branch 'main' into feat/new-timezone-settings
This commit is contained in:
commit
7c9e22970a
893
.yarn/releases/yarn-4.0.2.cjs
vendored
893
.yarn/releases/yarn-4.0.2.cjs
vendored
File diff suppressed because one or more lines are too long
894
.yarn/releases/yarn-4.3.1.cjs
vendored
Executable file
894
.yarn/releases/yarn-4.3.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -25,12 +25,12 @@ describe('getDisplayNameFromParticipant', () => {
|
||||
avatarUrl: '',
|
||||
jobTitle: '',
|
||||
linkedinLink: {
|
||||
url: '',
|
||||
label: '',
|
||||
primaryLinkUrl: '',
|
||||
primaryLinkLabel: '',
|
||||
},
|
||||
xLink: {
|
||||
url: '',
|
||||
label: '',
|
||||
primaryLinkUrl: '',
|
||||
primaryLinkLabel: '',
|
||||
},
|
||||
city: '',
|
||||
email: '',
|
||||
|
@ -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`
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
];
|
@ -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 };
|
||||
};
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -703,7 +703,7 @@ export const getObjectMetadataItemsMock = () => {
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '20202020-a3a7-4f63-9303-10226f6055be',
|
||||
type: 'LINK',
|
||||
type: 'LINKS',
|
||||
name: 'xLink',
|
||||
label: 'X',
|
||||
description: 'Contact’s 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: 'Contact’s 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',
|
||||
|
@ -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 [
|
||||
{
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
@ -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: '',
|
||||
|
@ -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: '',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: '',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -136,7 +136,6 @@ export const useRecordActionBar = ({
|
||||
accent: 'danger',
|
||||
onClick: () => {
|
||||
setIsDeleteRecordsModalOpen(true);
|
||||
handleDeleteClick();
|
||||
},
|
||||
ConfirmationModal: (
|
||||
<ConfirmationModal
|
||||
|
@ -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`
|
||||
|
@ -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}
|
||||
|
@ -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',
|
||||
|
@ -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 />
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
) =>
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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({
|
||||
|
@ -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 };
|
||||
};
|
@ -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"
|
||||
/>
|
||||
|
@ -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({
|
||||
|
@ -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 };
|
||||
};
|
@ -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}`}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -171,7 +171,7 @@ export const RecordShowContainer = ({
|
||||
isCentered: true,
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell readonly={isReadOnly} />
|
||||
<RecordInlineCell readonly={isReadOnly} isCentered={true} />
|
||||
</FieldContext.Provider>
|
||||
}
|
||||
avatarType={recordIdentifier?.avatarType ?? 'rounded'}
|
||||
|
@ -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 }),
|
||||
});
|
||||
|
@ -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 };
|
||||
};
|
@ -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,
|
||||
|
@ -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({
|
||||
|
@ -0,0 +1,9 @@
|
||||
export const buildShowPageURL = (
|
||||
objectNameSingular: string,
|
||||
recordId: string,
|
||||
viewId?: string | null | undefined,
|
||||
) => {
|
||||
return `/object/${objectNameSingular}/${recordId}${
|
||||
viewId ? `?view=${viewId}` : ''
|
||||
}`;
|
||||
};
|
@ -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: 'Contact’s 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: 'Contact’s 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: 'Contact’s 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: 'Contact’s 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: 'Contact’s 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: 'Contact’s 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: 'Contact’s 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: 'Contact’s 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: 'Contact’s 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: 'Contact’s 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: "''",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -0,0 +1 @@
|
||||
export const ROW_HEIGHT = 32;
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -68,9 +68,6 @@ export const RecordTableCellSoftFocusMode = ({
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[clearField, isFieldClearable, isFieldInputOnly],
|
||||
{
|
||||
enabled: !isFieldInputOnly,
|
||||
},
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -0,0 +1,6 @@
|
||||
export const buildIndexTablePageURL = (
|
||||
objectNamePlural: string,
|
||||
viewId?: string | null | undefined,
|
||||
) => {
|
||||
return `/objects/${objectNamePlural}${viewId ? `?view=${viewId}` : ''}`;
|
||||
};
|
@ -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',
|
||||
},
|
||||
],
|
@ -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>,
|
||||
};
|
@ -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 };
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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[];
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
import { KeyOfCompositeField } from '@/object-record/spreadsheet-import/types/KeyOfCompositeField';
|
||||
|
||||
export type CompositeFieldLabels<T> = {
|
||||
[key in `${KeyOfCompositeField<T>}Label`]: string;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export type KeyOfCompositeField<T> = keyof Omit<T, '__typename'> extends string
|
||||
? keyof Omit<T, '__typename'>
|
||||
: never;
|
@ -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 };
|
||||
};
|
@ -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;
|
||||
};
|
@ -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 [
|
||||
{
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user