feat: make record avatar/icon resolution data-driven via a configurable image identifier field (#22644)

## Summary

Today the avatar/icon shown for a record is hardcoded per object —
Company pulls a favicon from its domain link, Person uses `avatarUrl`,
etc. This PR replaces that hardcoding with a generic, data-driven
abstraction based on a configurable **image identifier field** on each
object's metadata (mirroring the existing **label identifier** concept).

An object's image identifier can point to:
- a **`FILES`** field → the uploaded image is used directly (rounded
avatar), or
- a **`LINKS`** field → a favicon is derived from the primary URL via
the Twenty icons service (squared avatar), gated by
`ALLOW_REQUESTS_TO_TWENTY_ICONS`.

This lets any object type (Opportunity, a custom "Listing", etc.) define
its own avatar/icon without code changes, and makes the field
configurable/overridable for standard objects.


##  Open question: also allow `TEXT` → direct image URL?
Right now the image identifier is restricted to `FILES` (uploaded file)
and `LINKS` (favicon). We deliberately left out `TEXT` → **direct image
URL** (e.g. an imported/synced photo URL stored in a text field).
There's precedent for it — Person's avatar was originally a `TEXT`
`avatarUrl`, and WorkspaceMember still is — and it's unambiguous (a
`TEXT` field has no favicon-vs-image ambiguity, and selecting it as the
image identifier is itself the declaration of intent). It's a small,
clean extension:
- add `TEXT` to the allowed image-identifier types,
- add an explicit `TEXT → raw URL` case
- `getAvatarType`: `TEXT → rounded`.
Caveats: it relies on admin assertion that the text values are image
URLs (no data-level guarantee), and external image URLs load third-party
content in the browser (IP-leak/hotlinking, same as favicons — a
proxy/cache would be the more robust long-term answer).

###  Resolution
Decision: **we will not support `TEXT` as an image identifier.** Image
identifiers stay restricted to `FILES` and `LINKS`, and any other type
fails closed (returns no avatar) on both the frontend and backend.
Instead, the legacy items that still rely on a `TEXT` avatar — Person's
deprecated `avatarUrl` and WorkspaceMember's `avatarUrl` — will be
migrated to `FILE` fields in a follow-up PR. Until then, WorkspaceMember
remains an exception (its `avatarUrl` still resolves through the
existing CorePicture path), and legacy Person `avatarUrl` values that
haven't been migrated will show initials placeholders.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22644?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Abdul Rahman
2026-07-15 19:15:47 +05:30
committed by GitHub
parent e31e6c7794
commit f4ff234db8
58 changed files with 1986 additions and 430 deletions
@@ -1,9 +1,10 @@
import { type MouseEvent } from 'react';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
import { Avatar } from 'twenty-ui/data-display';
import { MenuItemSuggestion } from 'twenty-ui/navigation';
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
type MentionMenuListItemProps = {
recordId: string;
@@ -24,6 +25,12 @@ export const MentionMenuListItem = ({
isSelected,
onClick,
}: MentionMenuListItemProps) => {
const { objectMetadataItems } = useObjectMetadataItems();
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === objectNameSingular,
);
const handleClick = (event?: MouseEvent) => {
event?.preventDefault();
event?.stopPropagation();
@@ -42,7 +49,7 @@ export const MentionMenuListItem = ({
placeholder={label}
placeholderColorSeed={recordId}
avatarUrl={getAbsoluteImageUrl(imageUrl)}
type={getAvatarType(objectNameSingular) ?? 'rounded'}
type={getAvatarType(objectMetadataItem)}
size="sm"
/>
)}
@@ -18,7 +18,7 @@ export const recordIdentifierToObjectRecordIdentifier = ({
recordIdentifier: RecordIdentifierDTO;
objectMetadataItem: EnrichedObjectMetadataItem;
}): ObjectRecordIdentifier => {
const avatarType = getAvatarType(objectMetadataItem.nameSingular);
const avatarType = getAvatarType(objectMetadataItem);
const basePathToShowPage = getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular,
@@ -1,12 +1,11 @@
import { styled } from '@linaria/react';
import { useContext, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { getLinkFaviconUrl, isDefined } from 'twenty-shared/utils';
import { getIconTileColorShades } from 'twenty-ui/data-display';
import { type IconComponent } from 'twenty-ui/icon';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink';
import { getLinkFaviconUrl } from '@/navigation-menu-item/display/link/utils/getLinkFaviconUrl';
const failedFaviconUrls = new Set<string>();
@@ -1,20 +0,0 @@
import { getLogoUrlFromDomainName } from 'twenty-shared/utils';
export const getLinkFaviconUrl = (
link: string | null | undefined,
): string | undefined => {
const trimmed = (link ?? '').trim();
if (!trimmed) {
return undefined;
}
const normalized =
trimmed.startsWith('http://') || trimmed.startsWith('https://')
? trimmed
: `https://${trimmed}`;
try {
const hostname = new URL(normalized).hostname;
return getLogoUrlFromDomainName(hostname);
} catch {
return undefined;
}
};
@@ -65,7 +65,7 @@ describe('useGetObjectRecordIdentifierByNameSingular', () => {
rerender({
record: {
id: 'recordId',
domainName: 'https://cool-company.com',
domainName: { primaryLinkUrl: 'https://cool-company.com' },
},
objectNameSingular: 'company',
});
@@ -0,0 +1,90 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getImageIdentifierFieldValue } from '@/object-metadata/utils/getImageIdentifierFieldValue';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FieldMetadataType } from 'twenty-shared/types';
const buildFieldMetadataItem = (
type: FieldMetadataType,
name = 'imageField',
): FieldMetadataItem => ({ name, type }) as FieldMetadataItem;
const buildRecord = (value: unknown, name = 'imageField'): ObjectRecord =>
({ id: 'record-id', [name]: value }) as unknown as ObjectRecord;
describe('getImageIdentifierFieldValue', () => {
it('returns null when the image identifier field metadata item is undefined', () => {
expect(getImageIdentifierFieldValue(buildRecord('value'), undefined)).toBe(
null,
);
});
it('returns null when the field value is not defined on the record', () => {
expect(
getImageIdentifierFieldValue(
buildRecord(undefined),
buildFieldMetadataItem(FieldMetadataType.FILES),
),
).toBe(null);
});
describe('FILES', () => {
it('returns the url of the first file', () => {
expect(
getImageIdentifierFieldValue(
buildRecord([{ url: 'https://example.com/a.png' }]),
buildFieldMetadataItem(FieldMetadataType.FILES),
),
).toBe('https://example.com/a.png');
});
it('returns null for an empty files array', () => {
expect(
getImageIdentifierFieldValue(
buildRecord([]),
buildFieldMetadataItem(FieldMetadataType.FILES),
),
).toBe(null);
});
});
describe('LINKS', () => {
it('returns the favicon url when requests to twenty-icons are allowed', () => {
expect(
getImageIdentifierFieldValue(
buildRecord({ primaryLinkUrl: 'twenty.com' }),
buildFieldMetadataItem(FieldMetadataType.LINKS),
true,
),
).toBe('https://twenty-icons.com/twenty.com');
});
it('returns null when requests to twenty-icons are not allowed', () => {
expect(
getImageIdentifierFieldValue(
buildRecord({ primaryLinkUrl: 'twenty.com' }),
buildFieldMetadataItem(FieldMetadataType.LINKS),
false,
),
).toBe(null);
});
it('returns null when the primary link url is not defined', () => {
expect(
getImageIdentifierFieldValue(
buildRecord({ primaryLinkUrl: null }),
buildFieldMetadataItem(FieldMetadataType.LINKS),
true,
),
).toBe(null);
});
});
it('returns null for unsupported field types (image identifiers are constrained to FILES/LINKS)', () => {
expect(
getImageIdentifierFieldValue(
buildRecord('https://example.com/avatar.png'),
buildFieldMetadataItem(FieldMetadataType.TEXT),
),
).toBe(null);
});
});
@@ -1,17 +1,39 @@
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { getImageIdentifierFieldMetadataItem } from '@/object-metadata/utils/getImageIdentifierFieldMetadataItem';
import { CoreObjectNameSingular, FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const getAvatarType = (objectNameSingular: string) => {
if (objectNameSingular === CoreObjectNameSingular.WorkspaceMember) {
export const getAvatarType = (
objectMetadataItem?: Pick<
EnrichedObjectMetadataItem,
'fields' | 'imageIdentifierFieldMetadataId' | 'nameSingular'
>,
) => {
if (!isDefined(objectMetadataItem)) {
return 'rounded';
}
if (objectNameSingular === CoreObjectNameSingular.Company) {
return 'squared';
if (
objectMetadataItem.nameSingular === CoreObjectNameSingular.WorkspaceMember
) {
return 'rounded';
}
const imageIdentifierFieldMetadataItem =
getImageIdentifierFieldMetadataItem(objectMetadataItem);
if (isDefined(imageIdentifierFieldMetadataItem)) {
switch (imageIdentifierFieldMetadataItem.type) {
case FieldMetadataType.LINKS:
return 'squared';
case FieldMetadataType.FILES:
return 'rounded';
}
}
if (
objectNameSingular === CoreObjectNameSingular.Task ||
objectNameSingular === CoreObjectNameSingular.Note
objectMetadataItem.nameSingular === CoreObjectNameSingular.Task ||
objectMetadataItem.nameSingular === CoreObjectNameSingular.Note
) {
return 'icon';
}
@@ -1,9 +1,9 @@
import { type Company } from '@/companies/types/Company';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils';
import { getImageIdentifierFieldValue } from './getImageIdentifierFieldValue';
export const getAvatarUrl = (
@@ -16,22 +16,10 @@ export const getAvatarUrl = (
return record.avatarUrl ?? undefined;
}
if (
objectNameSingular === CoreObjectNameSingular.Company &&
allowRequestsToTwentyIcons === true
) {
return getLogoUrlFromDomainName(
getCompanyDomainName(record as Company) ?? '',
);
}
if (objectNameSingular === CoreObjectNameSingular.Person) {
return record.avatarFile?.[0]?.url ?? '';
}
const imageIdentifierFieldValue = getImageIdentifierFieldValue(
record,
imageIdentifierFieldMetadataItem,
allowRequestsToTwentyIcons,
);
if (isDefined(imageIdentifierFieldValue)) {
@@ -1,16 +1,14 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { isImageIdentifierField } from '@/object-metadata/utils/isImageIdentifierField';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
export const getImageIdentifierFieldMetadataItem = (
objectMetadataItem: Pick<
EnrichedObjectMetadataItem,
'fields' | 'imageIdentifierFieldMetadataId' | 'nameSingular'
'fields' | 'imageIdentifierFieldMetadataId'
>,
): FieldMetadataItem | undefined =>
objectMetadataItem.fields.find((fieldMetadataItem) =>
isImageIdentifierField({
fieldMetadataItem,
objectMetadataItem,
}),
objectMetadataItem.fields.find(
(fieldMetadataItem) =>
fieldMetadataItem.id ===
objectMetadataItem.imageIdentifierFieldMetadataId,
);
@@ -1,14 +1,46 @@
import { isNonEmptyString } from '@sniptt/guards';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from 'twenty-shared/types';
import { getLinkFaviconUrl, isDefined } from 'twenty-shared/utils';
export const getImageIdentifierFieldValue = (
record: ObjectRecord,
imageIdentifierFieldMetadataItem: FieldMetadataItem | undefined,
) => {
if (isDefined(imageIdentifierFieldMetadataItem?.name)) {
return record[imageIdentifierFieldMetadataItem.name] as string;
allowRequestsToTwentyIcons?: boolean,
): string | null => {
if (!isDefined(imageIdentifierFieldMetadataItem?.name)) {
return null;
}
return null;
const fieldValue = record[imageIdentifierFieldMetadataItem.name];
if (!isDefined(fieldValue)) {
return null;
}
switch (imageIdentifierFieldMetadataItem.type) {
case FieldMetadataType.FILES: {
const url = Array.isArray(fieldValue) ? fieldValue[0]?.url : undefined;
return isNonEmptyString(url) ? url : null;
}
case FieldMetadataType.LINKS: {
if (allowRequestsToTwentyIcons !== true) {
return null;
}
const primaryLinkUrl =
typeof fieldValue === 'object' && 'primaryLinkUrl' in fieldValue
? fieldValue.primaryLinkUrl
: undefined;
return isNonEmptyString(primaryLinkUrl)
? (getLinkFaviconUrl(primaryLinkUrl) ?? null)
: null;
}
default:
return null;
}
};
@@ -1,4 +1,5 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { getImageIdentifierFieldMetadataItem } from '@/object-metadata/utils/getImageIdentifierFieldMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
@@ -31,13 +32,11 @@ export const getObjectRecordIdentifier = ({
labelIdentifierFieldMetadataItem,
);
const imageIdentifierFieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === objectMetadataItem.imageIdentifierFieldMetadataId,
);
const imageIdentifierFieldMetadata =
getImageIdentifierFieldMetadataItem(objectMetadataItem);
const avatarType = getAvatarType(objectMetadataItem.nameSingular);
const avatarType = getAvatarType(objectMetadataItem);
// TODO: This is a temporary solution before we seed imageIdentifierFieldMetadataId in the database
const avatarUrl = getAvatarUrl(
objectMetadataItem.nameSingular,
record,
@@ -1,29 +0,0 @@
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
export const isImageIdentifierField = ({
fieldMetadataItem,
objectMetadataItem,
}: {
fieldMetadataItem: Pick<FieldMetadataItem, 'id' | 'name'>;
objectMetadataItem: Pick<
EnrichedObjectMetadataItem,
'imageIdentifierFieldMetadataId' | 'nameSingular'
>;
}) => {
if (
objectMetadataItem.nameSingular === CoreObjectNameSingular.Company &&
fieldMetadataItem.name === 'domainName'
) {
return true;
}
if (objectMetadataItem.nameSingular === CoreObjectNameSingular.Person) {
return fieldMetadataItem.name === 'avatarFile';
}
return (
fieldMetadataItem.id === objectMetadataItem.imageIdentifierFieldMetadataId
);
};
@@ -3,6 +3,7 @@
exports[`generateDepthRecordGqlFieldsFromObject should generate depth one record gql fields from object 1`] = `
{
"accountOwner": {
"avatarUrl": true,
"id": true,
"name": true,
},
@@ -88,7 +88,7 @@ export const MultipleRecordPickerMenuItemContent = ({
placeholderColorSeed={searchRecord.recordId}
placeholder={displayText}
size="md"
type={getAvatarType(objectMetadataItem.nameSingular) ?? 'rounded'}
type={getAvatarType(objectMetadataItem)}
/>
}
text={displayText}
@@ -1,3 +1,6 @@
import { useMemo } from 'react';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
@@ -7,13 +10,13 @@ import { type RecordPickerPickableMorphItem } from '@/object-record/record-picke
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { Avatar } from 'twenty-ui/data-display';
import { MenuItemSelectAvatar } from 'twenty-ui/navigation';
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
type SingleRecordPickerMenuItemProps = {
morphItem: RecordPickerPickableMorphItem;
@@ -51,6 +54,18 @@ export const SingleRecordPickerMenuItem = ({
recordPickerComponentInstanceId,
);
const objectMetadataItem = useMemo(
() =>
singleRecordPickerSearchableObjectMetadataItems.find(
(searchableObjectMetadataItem: EnrichedObjectMetadataItem) =>
searchableObjectMetadataItem.id === morphItem.objectMetadataId,
),
[
singleRecordPickerSearchableObjectMetadataItems,
morphItem.objectMetadataId,
],
);
if (!isDefined(searchRecordStore)) {
return null;
}
@@ -78,9 +93,7 @@ export const SingleRecordPickerMenuItem = ({
placeholderColorSeed={morphItem.recordId}
placeholder={searchRecordStore.label}
size="md"
type={
getAvatarType(searchRecordStore.objectNameSingular) ?? 'rounded'
}
type={getAvatarType(objectMetadataItem)}
/>
}
contextualText={
@@ -1,10 +1,9 @@
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { usePersonAvatarUpload } from '@/object-record/record-show/hooks/usePersonAvatarUpload';
import { useRecordImageIdentifierUpload } from '@/object-record/record-show/hooks/useRecordImageIdentifierUpload';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
@@ -49,7 +48,10 @@ export const SummaryCard = ({
objectNameSingular,
});
const { onUploadPicture } = usePersonAvatarUpload(objectRecordId);
const { onUploadPicture } = useRecordImageIdentifierUpload({
objectNameSingular,
recordId: objectRecordId,
});
const isMobile = useIsMobile() || isInSidePanel;
@@ -115,11 +117,7 @@ export const SummaryCard = ({
</FieldContext.Provider>
}
avatarType={recordIdentifier?.avatarType ?? 'rounded'}
onUploadPicture={
objectNameSingular === CoreObjectNameSingular.Person
? onUploadPicture
: undefined
}
onUploadPicture={onUploadPicture}
/>
);
};
@@ -1,59 +0,0 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useApolloClient, useMutation } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import {
FieldMetadataType,
UploadFilesFieldFileDocument,
} from '~/generated-metadata/graphql';
export const usePersonAvatarUpload = (personRecordId: string) => {
const apolloClient = useApolloClient();
const [uploadFilesFieldFile] = useMutation(UploadFilesFieldFileDocument, {
client: apolloClient,
});
const { updateOneRecord } = useUpdateOneRecord();
const { objectMetadataItem: personMetadata } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Person,
});
const avatarFileFieldMetadataId = personMetadata.fields.find(
(field) =>
field.type === FieldMetadataType.FILES && field.name === 'avatarFile',
)?.id;
const onUploadPicture = async (file: File) => {
assertIsDefinedOrThrow(
avatarFileFieldMetadataId,
new Error(t`Avatar file field not found for person object`),
);
const result = await uploadFilesFieldFile({
variables: { file, fieldMetadataId: avatarFileFieldMetadataId },
});
const uploadedFile = result?.data?.uploadFilesFieldFile;
if (!isDefined(uploadedFile)) {
return;
}
await updateOneRecord({
objectNameSingular: CoreObjectNameSingular.Person,
idToUpdate: personRecordId,
updateOneRecordInput: {
avatarFile: [
{
fileId: uploadedFile.id,
label: file.name,
},
],
},
});
};
return { onUploadPicture };
};
@@ -0,0 +1,82 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getImageIdentifierFieldMetadataItem } from '@/object-metadata/utils/getImageIdentifierFieldMetadataItem';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import { useApolloClient, useMutation } from '@apollo/client/react';
import { isDefined } from 'twenty-shared/utils';
import {
FieldMetadataType,
UploadFilesFieldFileDocument,
} from '~/generated-metadata/graphql';
type UseRecordImageIdentifierUploadParams = {
objectNameSingular: string;
recordId: string;
};
export const useRecordImageIdentifierUpload = ({
objectNameSingular,
recordId,
}: UseRecordImageIdentifierUploadParams) => {
const apolloClient = useApolloClient();
const [uploadFilesFieldFile] = useMutation(UploadFilesFieldFileDocument, {
client: apolloClient,
});
const { updateOneRecord } = useUpdateOneRecord();
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const imageIdentifierFieldMetadataItem =
getImageIdentifierFieldMetadataItem(objectMetadataItem);
const filesImageIdentifierFieldMetadataItem =
imageIdentifierFieldMetadataItem?.type === FieldMetadataType.FILES
? imageIdentifierFieldMetadataItem
: undefined;
const isImageIdentifierFieldReadOnly = useIsRecordFieldReadOnly({
recordId,
objectMetadataId: objectMetadataItem.id,
fieldMetadataId: filesImageIdentifierFieldMetadataItem?.id ?? '',
});
const canUploadImageIdentifier =
isDefined(filesImageIdentifierFieldMetadataItem) &&
!isImageIdentifierFieldReadOnly;
const onUploadPicture = async (file: File) => {
if (!isDefined(filesImageIdentifierFieldMetadataItem)) {
return;
}
const result = await uploadFilesFieldFile({
variables: {
file,
fieldMetadataId: filesImageIdentifierFieldMetadataItem.id,
},
});
const uploadedFile = result?.data?.uploadFilesFieldFile;
if (!isDefined(uploadedFile)) {
return;
}
await updateOneRecord({
objectNameSingular,
idToUpdate: recordId,
updateOneRecordInput: {
[filesImageIdentifierFieldMetadataItem.name]: [
{
fileId: uploadedFile.id,
label: file.name,
},
],
},
});
};
return {
onUploadPicture: canUploadImageIdentifier ? onUploadPicture : undefined,
};
};
@@ -5,6 +5,7 @@ import {
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
import { getAvatarUrl } from '@/object-metadata/utils/getAvatarUrl';
import { getImageIdentifierFieldMetadataItem } from '@/object-metadata/utils/getImageIdentifierFieldMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { getLabelIdentifierFieldValue } from '@/object-metadata/utils/getLabelIdentifierFieldValue';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
@@ -72,13 +73,9 @@ export const getRecordChipGenerators = (
getLabelIdentifierFieldMetadataItem(objectMetadataItemToUse);
const imageIdentifierFieldMetadataToUse =
objectMetadataItemToUse.fields.find(
(field) =>
field.id ===
objectMetadataItemToUse.imageIdentifierFieldMetadataId,
);
getImageIdentifierFieldMetadataItem(objectMetadataItemToUse);
const avatarType = getAvatarType(objectNameSingularToFind);
const avatarType = getAvatarType(objectMetadataItemToUse);
return [
fieldMetadataItem.name,
@@ -2,6 +2,7 @@ import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
import { useGetIsMetadataItemCustom } from '@/object-metadata/hooks/useGetIsMetadataItemCustom';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
@@ -10,10 +11,10 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
import { zodResolver } from '@hookform/resolvers/zod';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import {
isImageIdentifierFieldMetadataType,
isLabelIdentifierFieldMetadataTypes,
isSearchableFieldType,
} from 'twenty-shared/utils';
@@ -67,9 +68,18 @@ export const SettingsDataModelObjectIdentifiersForm = ({
const handleSave = async (
formValues: SettingsDataModelObjectIdentifiersFormValues,
) => {
const {
labelIdentifierFieldMetadataId: _labelIdentifierFieldMetadataId,
...payloadWithoutLabelIdentifier
} = formValues;
const updatePayload = isCustomObject
? formValues
: payloadWithoutLabelIdentifier;
const result = await updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: formValues,
updatePayload,
});
if (result.status === 'successful') {
@@ -78,23 +88,25 @@ export const SettingsDataModelObjectIdentifiersForm = ({
};
const { getIcon } = useIcons();
const labelIdentifierFieldOptions = useMemo(
() =>
getActiveFieldMetadataItems(objectMetadataItem)
.filter(
({ id, type }) =>
(isLabelIdentifierFieldMetadataTypes(type) &&
isSearchableFieldType(type)) ||
objectMetadataItem.labelIdentifierFieldMetadataId === id,
)
.map<SelectOption<string | null>>((fieldMetadataItem) => ({
Icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
value: fieldMetadataItem.id,
})),
[getIcon, objectMetadataItem],
);
const imageIdentifierFieldOptions: SelectOption<string | null>[] = [];
const mapFieldToSelectOption = (
fieldMetadataItem: FieldMetadataItem,
): SelectOption<string | null> => ({
Icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
value: fieldMetadataItem.id,
});
const labelIdentifierFieldOptions = getActiveFieldMetadataItems(
objectMetadataItem,
)
.filter(
({ id, type }) =>
(isLabelIdentifierFieldMetadataTypes(type) &&
isSearchableFieldType(type)) ||
objectMetadataItem.labelIdentifierFieldMetadataId === id,
)
.map(mapFieldToSelectOption);
const emptyOption: SelectOption<string | null> = {
Icon: IconCircleOff,
@@ -102,6 +114,17 @@ export const SettingsDataModelObjectIdentifiersForm = ({
value: null,
};
const imageIdentifierFieldOptions = [
emptyOption,
...getActiveFieldMetadataItems(objectMetadataItem)
.filter(
({ id, type }) =>
isImageIdentifierFieldMetadataType(type) ||
objectMetadataItem.imageIdentifierFieldMetadataId === id,
)
.map(mapFieldToSelectOption),
];
const navigate = useNavigate();
return (
@@ -112,14 +135,16 @@ export const SettingsDataModelObjectIdentifiersForm = ({
fieldName: LABEL_IDENTIFIER_FIELD_METADATA_ID,
options: labelIdentifierFieldOptions,
defaultValue: objectMetadataItem.labelIdentifierFieldMetadataId,
disabled: !isCustomObject || readonly,
},
{
label: t`Record image`,
fieldName: IMAGE_IDENTIFIER_FIELD_METADATA_ID,
options: imageIdentifierFieldOptions,
defaultValue: null,
defaultValue: objectMetadataItem.imageIdentifierFieldMetadataId,
disabled: readonly,
},
].map(({ fieldName, label, options, defaultValue }) => (
].map(({ fieldName, label, options, defaultValue, disabled }) => (
<Controller
key={fieldName}
name={fieldName}
@@ -134,7 +159,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({
options={options}
value={value}
withSearchInput={label === t`Record label`}
disabled={!isCustomObject || readonly}
disabled={disabled}
callToActionButton={
label === t`Record label`
? {
@@ -146,8 +171,8 @@ export const SettingsDataModelObjectIdentifiersForm = ({
}
: undefined
}
onChange={(value) => {
onChange(value);
onChange={(newValue) => {
onChange(newValue);
formConfig.handleSubmit(handleSave)();
}}
/>
@@ -11,6 +11,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -102,7 +103,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-22.png",
"avatarFile": null,
"id": "20202020-b030-4743-9edd-d1a1776d653d",
"name": {
"__typename": "FullName",
@@ -115,7 +116,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-33.png",
"avatarFile": null,
"id": "20202020-bcc1-434e-995a-f80dfa92b596",
"name": {
"__typename": "FullName",
@@ -215,6 +216,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -306,7 +308,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "",
"avatarFile": null,
"id": "20202020-bf28-4a93-bba3-b02aa55543a3",
"name": {
"__typename": "FullName",
@@ -319,7 +321,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-49.png",
"avatarFile": null,
"id": "20202020-b0ae-46da-9697-ed949ee75b67",
"name": {
"__typename": "FullName",
@@ -411,6 +413,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-77d5-4cb6-b60a-f4a835a85d61",
"name": {
"__typename": "FullName",
@@ -502,7 +505,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-86.png",
"avatarFile": null,
"id": "20202020-b165-49bf-b2c1-60fd1ee3d368",
"name": {
"__typename": "FullName",
@@ -515,7 +518,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-76.png",
"avatarFile": null,
"id": "20202020-b664-4460-83e8-5c5f1c64c836",
"name": {
"__typename": "FullName",
@@ -599,6 +602,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -707,7 +711,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-84.png",
"avatarFile": null,
"id": "20202020-bfea-4a51-81be-d46de6a93db7",
"name": {
"__typename": "FullName",
@@ -720,7 +724,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-33.png",
"avatarFile": null,
"id": "20202020-bfa7-4546-a865-18a9cd06de4c",
"name": {
"__typename": "FullName",
@@ -812,6 +816,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -912,7 +917,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-71.png",
"avatarFile": null,
"id": "20202020-b397-497b-90bc-f62c1c34b2a3",
"name": {
"__typename": "FullName",
@@ -925,7 +930,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-58.png",
"avatarFile": null,
"id": "20202020-beb3-40b1-8f18-de25a7fd1146",
"name": {
"__typename": "FullName",
@@ -1033,6 +1038,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-77d5-4cb6-b60a-f4a835a85d61",
"name": {
"__typename": "FullName",
@@ -1124,7 +1130,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-1.png",
"avatarFile": null,
"id": "20202020-b171-46bc-a285-ec8ee3e3b702",
"name": {
"__typename": "FullName",
@@ -1137,7 +1143,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-7.png",
"avatarFile": null,
"id": "20202020-b307-4b8e-9281-b3ddb0ef420c",
"name": {
"__typename": "FullName",
@@ -1229,6 +1235,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -1320,7 +1327,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "",
"avatarFile": null,
"id": "20202020-b29f-45a3-9e75-6a1d49f1299f",
"name": {
"__typename": "FullName",
@@ -1333,7 +1340,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-1.png",
"avatarFile": null,
"id": "20202020-bcbc-4e67-8f84-df60aad8cd7f",
"name": {
"__typename": "FullName",
@@ -1441,6 +1448,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -1532,7 +1540,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-19.png",
"avatarFile": null,
"id": "20202020-bab2-4892-8834-6ca25212fd35",
"name": {
"__typename": "FullName",
@@ -1545,7 +1553,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "",
"avatarFile": null,
"id": "20202020-b48a-4ca5-9596-abdee52b69a6",
"name": {
"__typename": "FullName",
@@ -1653,6 +1661,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -1744,7 +1753,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-87.png",
"avatarFile": null,
"id": "20202020-b597-4d76-b0d5-cd2b7d7e0255",
"name": {
"__typename": "FullName",
@@ -1757,7 +1766,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "",
"avatarFile": null,
"id": "20202020-bbaf-436a-b635-79e7c349f388",
"name": {
"__typename": "FullName",
@@ -1889,6 +1898,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "Company",
"accountOwner": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-77d5-4cb6-b60a-f4a835a85d61",
"name": {
"__typename": "FullName",
@@ -1997,7 +2007,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "",
"avatarFile": null,
"id": "20202020-ba0b-48a9-85ba-e223975696ea",
"name": {
"__typename": "FullName",
@@ -2010,7 +2020,7 @@ export const mockedCompanyRecords: ObjectRecord[] =
"__typename": "PersonEdge",
"node": {
"__typename": "Person",
"avatarUrl": "https://twentyhq.github.io/placeholder-images/people/image-65.png",
"avatarFile": null,
"id": "20202020-b3e6-4514-88e8-7394fa3017cc",
"name": {
"__typename": "FullName",
@@ -11,6 +11,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -102,6 +103,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -193,6 +195,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -284,6 +287,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -375,6 +379,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -466,6 +471,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -557,6 +563,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -648,6 +655,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -739,6 +747,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-1553-45c6-a028-5a9064cce07f",
"name": {
"__typename": "FullName",
@@ -830,6 +839,7 @@ export const mockedTaskRecords: ObjectRecord[] =
"__typename": "Task",
"assignee": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-77d5-4cb6-b60a-f4a835a85d61",
"name": {
"__typename": "FullName",
@@ -36,6 +36,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "goo.gle",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a305-41e7-8c72-ba44072a4c58",
"name": "Google"
},
@@ -76,6 +82,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -114,6 +121,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "microsoft.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a225-4b3d-a89c-7f6c30df998a",
"name": "Microsoft"
},
@@ -154,6 +167,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -192,6 +206,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "metacareers.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a8b0-422c-8fcf-5b7496f94975",
"name": "Meta"
},
@@ -232,6 +252,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -270,6 +291,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "slb.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-aaf7-41d6-87a9-7add07bebfd8",
"name": "SLB"
},
@@ -310,6 +337,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -348,6 +376,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "cisco.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a19d-422b-9cb2-5f8382a56877",
"name": "Cisco"
},
@@ -388,6 +422,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -426,6 +461,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "uber.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a39c-4644-867d-e8e1851b3ee8",
"name": "Uber"
},
@@ -466,6 +507,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -504,6 +546,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "salesforce.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a0eb-4c51-aa03-c4cd2423d7cb",
"name": "Salesforce"
},
@@ -544,6 +592,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -582,6 +631,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "amdocs.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a9b5-48ec-97c0-dbbfcbe8df1b",
"name": "Amdocs"
},
@@ -622,6 +677,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -660,6 +716,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "vmware.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a89d-44f9-ac9c-25e462460cb0",
"name": "VMware"
},
@@ -700,6 +762,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -738,6 +801,12 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"targetCompany": {
"__typename": "Company",
"domainName": {
"__typename": "Links",
"primaryLinkUrl": "globallogic.com",
"primaryLinkLabel": "",
"secondaryLinks": []
},
"id": "20202020-a377-4693-a2d9-89dc9188a1dc",
"name": "GlobalLogic"
},
@@ -778,6 +847,7 @@ export const mockedTimelineActivityRecords: ObjectRecord[] =
},
"workspaceMember": {
"__typename": "WorkspaceMember",
"avatarUrl": "",
"id": "20202020-0687-4c41-b707-ed1bfca972a7",
"name": {
"__typename": "FullName",
@@ -10243,7 +10243,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"createdAt": "2026-04-10T08:55:56.200Z",
"updatedAt": "2026-04-10T08:55:56.200Z",
"labelIdentifierFieldMetadataId": "d8ff93be-5b55-4e4b-af50-20eb2f38320c",
"imageIdentifierFieldMetadataId": null,
"imageIdentifierFieldMetadataId": "07ce14cd-f2e2-4f21-ad62-52f0a1329a6b",
"applicationId": "dd6a5463-023d-4a10-855f-a4abaf32c1ec",
"shortcut": null,
"isLabelSyncedWithName": false,
@@ -14806,7 +14806,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"createdAt": "2026-04-10T08:55:56.200Z",
"updatedAt": "2026-04-10T08:55:56.200Z",
"labelIdentifierFieldMetadataId": "1b86fb74-25cf-4228-af97-b6a479643407",
"imageIdentifierFieldMetadataId": null,
"imageIdentifierFieldMetadataId": "a1bbfc74-315b-475c-b13b-4268476c2894",
"applicationId": "dd6a5463-023d-4a10-855f-a4abaf32c1ec",
"shortcut": "P",
"isLabelSyncedWithName": false,
@@ -16669,7 +16669,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"createdAt": "2026-04-10T08:55:56.200Z",
"updatedAt": "2026-04-10T08:55:56.200Z",
"labelIdentifierFieldMetadataId": "50c85704-9c8e-4e30-8a86-dc6adb756999",
"imageIdentifierFieldMetadataId": null,
"imageIdentifierFieldMetadataId": "be9fd5e1-ca36-4b91-ba14-22a5ab409e92",
"applicationId": "dd6a5463-023d-4a10-855f-a4abaf32c1ec",
"shortcut": "C",
"isLabelSyncedWithName": false,