feat: Revamp navigation bar (#6031)
closes: #4428 Testing for fetchMoreRecords is pending, along with component tests --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
20
packages/twenty-front/src/hooks/useScrollToPosition.ts
Normal file
20
packages/twenty-front/src/hooks/useScrollToPosition.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScrollbarsState';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
export const useScrollToPosition = () => {
|
||||||
|
const scrollToPosition = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
(scrollPositionInPx: number) => {
|
||||||
|
const overlayScrollbars = snapshot
|
||||||
|
.getLoadable(overlayScrollbarsState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const scrollWrapper = overlayScrollbars?.elements().viewport;
|
||||||
|
|
||||||
|
scrollWrapper?.scrollTo({ top: scrollPositionInPx });
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { scrollToPosition };
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { css, useTheme } from '@emotion/react';
|
import { css, useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Avatar, AvatarGroup, IconArrowRight, IconLock } from 'twenty-ui';
|
import { Avatar, AvatarGroup, IconArrowRight, IconLock } from 'twenty-ui';
|
||||||
|
|
||||||
@ -14,8 +14,8 @@ import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEv
|
|||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { Card } from '@/ui/layout/card/components/Card';
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||||
import { CalendarChannelVisibility } from '~/generated/graphql';
|
|
||||||
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
|
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
|
||||||
|
import { CalendarChannelVisibility } from '~/generated/graphql';
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
@ -169,7 +169,9 @@ export const CalendarEventRow = ({
|
|||||||
? `${participant.firstName} ${participant.lastName}`
|
? `${participant.firstName} ${participant.lastName}`
|
||||||
: participant.displayName
|
: participant.displayName
|
||||||
}
|
}
|
||||||
entityId={participant.workspaceMemberId ?? participant.personId}
|
placeholderColorSeed={
|
||||||
|
participant.workspaceMemberId ?? participant.personId
|
||||||
|
}
|
||||||
type="rounded"
|
type="rounded"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export const CommentHeader = ({ comment, actionBar }: CommentHeaderProps) => {
|
|||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)}
|
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)}
|
||||||
size="md"
|
size="md"
|
||||||
entityId={author?.id}
|
placeholderColorSeed={author?.id}
|
||||||
placeholder={authorName}
|
placeholder={authorName}
|
||||||
/>
|
/>
|
||||||
<StyledName>{authorName}</StyledName>
|
<StyledName>{authorName}</StyledName>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useRef } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { Avatar, IconNotes, IconSparkles } from 'twenty-ui';
|
import { Avatar, IconNotes, IconSparkles } from 'twenty-ui';
|
||||||
@ -377,7 +377,7 @@ export const CommandMenu = () => {
|
|||||||
<Avatar
|
<Avatar
|
||||||
type="rounded"
|
type="rounded"
|
||||||
avatarUrl={null}
|
avatarUrl={null}
|
||||||
entityId={person.id}
|
placeholderColorSeed={person.id}
|
||||||
placeholder={
|
placeholder={
|
||||||
person.name.firstName +
|
person.name.firstName +
|
||||||
' ' +
|
' ' +
|
||||||
@ -399,7 +399,7 @@ export const CommandMenu = () => {
|
|||||||
to={`object/company/${company.id}`}
|
to={`object/company/${company.id}`}
|
||||||
Icon={() => (
|
Icon={() => (
|
||||||
<Avatar
|
<Avatar
|
||||||
entityId={company.id}
|
placeholderColorSeed={company.id}
|
||||||
placeholder={company.name}
|
placeholder={company.name}
|
||||||
avatarUrl={getLogoUrlFromDomainName(
|
avatarUrl={getLogoUrlFromDomainName(
|
||||||
company.domainName,
|
company.domainName,
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export const Favorites = () => {
|
|||||||
label={labelIdentifier}
|
label={labelIdentifier}
|
||||||
Icon={() => (
|
Icon={() => (
|
||||||
<StyledAvatar
|
<StyledAvatar
|
||||||
entityId={recordId}
|
placeholderColorSeed={recordId}
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)}
|
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl)}
|
||||||
type={avatarType}
|
type={avatarType}
|
||||||
placeholder={labelIdentifier}
|
placeholder={labelIdentifier}
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
||||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||||
import { getRecordChipGeneratorPerObjectPerField } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
|
||||||
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
||||||
|
|
||||||
export const ObjectMetadataItemsProvider = ({
|
export const ObjectMetadataItemsProvider = ({
|
||||||
@ -15,23 +14,15 @@ export const ObjectMetadataItemsProvider = ({
|
|||||||
|
|
||||||
const shouldDisplayChildren = objectMetadataItems.length > 0;
|
const shouldDisplayChildren = objectMetadataItems.length > 0;
|
||||||
|
|
||||||
const chipGeneratorPerObjectPerField = useMemo(() => {
|
|
||||||
return getRecordChipGeneratorPerObjectPerField(objectMetadataItems);
|
|
||||||
}, [objectMetadataItems]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ObjectMetadataItemsLoadEffect />
|
<ObjectMetadataItemsLoadEffect />
|
||||||
{shouldDisplayChildren ? (
|
{shouldDisplayChildren ? (
|
||||||
<PreComputedChipGeneratorsContext.Provider
|
<PreComputedChipGeneratorsProvider>
|
||||||
value={{
|
|
||||||
chipGeneratorPerObjectPerField,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RelationPickerScope relationPickerScopeId="relation-picker">
|
<RelationPickerScope relationPickerScopeId="relation-picker">
|
||||||
{children}
|
{children}
|
||||||
</RelationPickerScope>
|
</RelationPickerScope>
|
||||||
</PreComputedChipGeneratorsContext.Provider>
|
</PreComputedChipGeneratorsProvider>
|
||||||
) : (
|
) : (
|
||||||
<UserOrMetadataLoader />
|
<UserOrMetadataLoader />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { getRecordChipGenerators } from '@/object-record/utils/getRecordChipGenerators';
|
||||||
|
|
||||||
|
export const PreComputedChipGeneratorsProvider = ({
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren) => {
|
||||||
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
|
const { chipGeneratorPerObjectPerField, identifierChipGeneratorPerObject } =
|
||||||
|
useMemo(() => {
|
||||||
|
return getRecordChipGenerators(objectMetadataItems);
|
||||||
|
}, [objectMetadataItems]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PreComputedChipGeneratorsContext.Provider
|
||||||
|
value={{
|
||||||
|
chipGeneratorPerObjectPerField,
|
||||||
|
identifierChipGeneratorPerObject,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PreComputedChipGeneratorsContext.Provider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -3,13 +3,19 @@ import { createContext } from 'react';
|
|||||||
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
export type ChipGeneratorPerObjectPerField = Record<
|
export type ChipGeneratorPerObjectNameSingularPerFieldName = Record<
|
||||||
string,
|
string,
|
||||||
Record<string, (record: ObjectRecord) => RecordChipData>
|
Record<string, (record: ObjectRecord) => RecordChipData>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type IdentifierChipGeneratorPerObject = Record<
|
||||||
|
string,
|
||||||
|
(record: ObjectRecord) => RecordChipData
|
||||||
|
>;
|
||||||
|
|
||||||
export type PreComputedChipGeneratorsContextProps = {
|
export type PreComputedChipGeneratorsContextProps = {
|
||||||
chipGeneratorPerObjectPerField: ChipGeneratorPerObjectPerField;
|
chipGeneratorPerObjectPerField: ChipGeneratorPerObjectNameSingularPerFieldName;
|
||||||
|
identifierChipGeneratorPerObject: IdentifierChipGeneratorPerObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PreComputedChipGeneratorsContext =
|
export const PreComputedChipGeneratorsContext =
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export const getLabelIdentifierFieldValue = (
|
|||||||
record: ObjectRecord,
|
record: ObjectRecord,
|
||||||
labelIdentifierFieldMetadataItem: FieldMetadataItem | undefined,
|
labelIdentifierFieldMetadataItem: FieldMetadataItem | undefined,
|
||||||
objectNameSingular: string,
|
objectNameSingular: string,
|
||||||
) => {
|
): string => {
|
||||||
if (
|
if (
|
||||||
objectNameSingular === CoreObjectNameSingular.WorkspaceMember ||
|
objectNameSingular === CoreObjectNameSingular.WorkspaceMember ||
|
||||||
labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FullName
|
labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FullName
|
||||||
@ -17,7 +17,7 @@ export const getLabelIdentifierFieldValue = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(labelIdentifierFieldMetadataItem?.name)) {
|
if (isDefined(labelIdentifierFieldMetadataItem?.name)) {
|
||||||
return record[labelIdentifierFieldMetadataItem.name] as string | number;
|
return String(record[labelIdentifierFieldMetadataItem.name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|||||||
|
|
||||||
export const getLinkToShowPage = (
|
export const getLinkToShowPage = (
|
||||||
objectNameSingular: string,
|
objectNameSingular: string,
|
||||||
record: ObjectRecord,
|
record: Pick<ObjectRecord, 'id'>,
|
||||||
) => {
|
) => {
|
||||||
const basePathToShowPage = getBasePathToShowPage({
|
const basePathToShowPage = getBasePathToShowPage({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import { EntityChip, EntityChipVariant } from 'twenty-ui';
|
import { AvatarChip, AvatarChipVariant } from 'twenty-ui';
|
||||||
|
|
||||||
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
||||||
|
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export type RecordChipProps = {
|
export type RecordChipProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
record: ObjectRecord;
|
record: ObjectRecord;
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: EntityChipVariant;
|
variant?: AvatarChipVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordChip = ({
|
export const RecordChip = ({
|
||||||
@ -16,19 +20,29 @@ export const RecordChip = ({
|
|||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
}: RecordChipProps) => {
|
}: RecordChipProps) => {
|
||||||
const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { recordChipData } = useRecordChipData({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
record,
|
||||||
});
|
});
|
||||||
|
|
||||||
const objectRecordIdentifier = mapToObjectRecordIdentifier(record);
|
const handleAvatarChipClick = (event: MouseEvent) => {
|
||||||
|
const linkToShowPage = getLinkToShowPage(objectNameSingular, record);
|
||||||
|
|
||||||
|
if (isNonEmptyString(linkToShowPage)) {
|
||||||
|
event.stopPropagation();
|
||||||
|
navigate(linkToShowPage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityChip
|
<AvatarChip
|
||||||
entityId={record.id}
|
placeholderColorSeed={record.id}
|
||||||
name={objectRecordIdentifier.name}
|
name={recordChipData.name}
|
||||||
avatarType={objectRecordIdentifier.avatarType}
|
avatarType={recordChipData.avatarType}
|
||||||
avatarUrl={objectRecordIdentifier.avatarUrl ?? ''}
|
avatarUrl={recordChipData.avatarUrl ?? ''}
|
||||||
linkToEntity={objectRecordIdentifier.linkToShowPage}
|
onClick={handleAvatarChipClick}
|
||||||
className={className}
|
className={className}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||||
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
|
import { QueryCursorDirection } from '@/object-record/utils/generateFindManyRecordsQuery';
|
||||||
|
|
||||||
export type RecordGqlOperationVariables = {
|
export type RecordGqlOperationVariables = {
|
||||||
filter?: RecordGqlOperationFilter;
|
filter?: RecordGqlOperationFilter;
|
||||||
orderBy?: RecordGqlOperationOrderBy;
|
orderBy?: RecordGqlOperationOrderBy;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
cursorFilter?: {
|
||||||
|
cursor: string;
|
||||||
|
cursorDirection: QueryCursorDirection;
|
||||||
|
limit: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const useFetchAllRecordIds = <T>({
|
|||||||
const firstQueryResult =
|
const firstQueryResult =
|
||||||
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
|
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
|
||||||
|
|
||||||
const totalCount = firstQueryResult?.totalCount ?? 1;
|
const totalCount = firstQueryResult?.totalCount ?? 0;
|
||||||
|
|
||||||
const recordsCount = firstQueryResult?.edges.length ?? 0;
|
const recordsCount = firstQueryResult?.edges.length ?? 0;
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
fetchPolicy,
|
fetchPolicy,
|
||||||
onError,
|
onError,
|
||||||
onCompleted,
|
onCompleted,
|
||||||
|
cursorFilter,
|
||||||
}: UseFindManyRecordsParams<T>) => {
|
}: UseFindManyRecordsParams<T>) => {
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
@ -42,6 +43,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
const { findManyRecordsQuery } = useFindManyRecordsQuery({
|
const { findManyRecordsQuery } = useFindManyRecordsQuery({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
|
cursorDirection: cursorFilter?.cursorDirection,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
|
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
|
||||||
@ -67,15 +69,16 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
skip: skip || !objectMetadataItem || !currentWorkspaceMember,
|
skip: skip || !objectMetadataItem || !currentWorkspaceMember,
|
||||||
variables: {
|
variables: {
|
||||||
filter,
|
filter,
|
||||||
limit,
|
|
||||||
orderBy,
|
orderBy,
|
||||||
|
lastCursor: cursorFilter?.cursor ?? undefined,
|
||||||
|
limit: cursorFilter?.limit ?? limit,
|
||||||
},
|
},
|
||||||
fetchPolicy: fetchPolicy,
|
fetchPolicy: fetchPolicy,
|
||||||
onCompleted: handleFindManyRecordsCompleted,
|
onCompleted: handleFindManyRecordsCompleted,
|
||||||
onError: handleFindManyRecordsError,
|
onError: handleFindManyRecordsError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { fetchMoreRecords, totalCount, records, hasNextPage } =
|
const { fetchMoreRecords, records, hasNextPage } =
|
||||||
useFetchMoreRecordsWithPagination<T>({
|
useFetchMoreRecordsWithPagination<T>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter,
|
filter,
|
||||||
@ -87,6 +90,9 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pageInfo = data?.[objectMetadataItem.namePlural].pageInfo;
|
||||||
|
const totalCount = data?.[objectMetadataItem.namePlural].totalCount;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
records,
|
records,
|
||||||
@ -96,5 +102,6 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
fetchMoreRecords,
|
fetchMoreRecords,
|
||||||
queryStateIdentifier: queryIdentifier,
|
queryStateIdentifier: queryIdentifier,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
pageInfo,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,16 +3,21 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
|
import {
|
||||||
|
generateFindManyRecordsQuery,
|
||||||
|
QueryCursorDirection,
|
||||||
|
} from '@/object-record/utils/generateFindManyRecordsQuery';
|
||||||
|
|
||||||
export const useFindManyRecordsQuery = ({
|
export const useFindManyRecordsQuery = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
computeReferences,
|
computeReferences,
|
||||||
|
cursorDirection = 'after',
|
||||||
}: {
|
}: {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
computeReferences?: boolean;
|
computeReferences?: boolean;
|
||||||
|
cursorDirection?: QueryCursorDirection;
|
||||||
}) => {
|
}) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
@ -25,6 +30,7 @@ export const useFindManyRecordsQuery = ({
|
|||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
computeReferences,
|
computeReferences,
|
||||||
|
cursorDirection,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||||
|
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
export const useRecordChipData = ({
|
||||||
|
objectNameSingular,
|
||||||
|
record,
|
||||||
|
}: {
|
||||||
|
objectNameSingular: string;
|
||||||
|
record: ObjectRecord;
|
||||||
|
}) => {
|
||||||
|
const { identifierChipGeneratorPerObject } = useContext(
|
||||||
|
PreComputedChipGeneratorsContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateRecordChipData =
|
||||||
|
identifierChipGeneratorPerObject[objectNameSingular] ??
|
||||||
|
generateDefaultRecordChipData;
|
||||||
|
|
||||||
|
const recordChipData = generateRecordChipData(record);
|
||||||
|
|
||||||
|
return { recordChipData };
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { EntityChip, IconComponent } from 'twenty-ui';
|
import { AvatarChip, IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
|
|
||||||
@ -13,8 +13,8 @@ export const GenericEntityFilterChip = ({
|
|||||||
filter,
|
filter,
|
||||||
Icon,
|
Icon,
|
||||||
}: GenericEntityFilterChipProps) => (
|
}: GenericEntityFilterChipProps) => (
|
||||||
<EntityChip
|
<AvatarChip
|
||||||
entityId={filter.value}
|
placeholderColorSeed={filter.value}
|
||||||
name={filter.displayValue}
|
name={filter.displayValue}
|
||||||
avatarType="rounded"
|
avatarType="rounded"
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(filter.displayAvatarUrl) || ''}
|
avatarUrl={getImageAbsoluteURIOrBase64(filter.displayAvatarUrl) || ''}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
import { ReactNode, useContext, useState } from 'react';
|
import { ReactNode, useContext, useState } from 'react';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { EntityChipVariant, IconEye } from 'twenty-ui';
|
import { AvatarChipVariant, IconEye } from 'twenty-ui';
|
||||||
|
|
||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
|
||||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||||
@ -14,6 +13,7 @@ import {
|
|||||||
RecordUpdateHookParams,
|
RecordUpdateHookParams,
|
||||||
} from '@/object-record/record-field/contexts/FieldContext';
|
} from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||||
|
import { RecordIndexRecordChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
|
||||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||||
@ -222,10 +222,10 @@ export const RecordBoardCard = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledBoardCardHeader showCompactView={isCompactModeActive}>
|
<StyledBoardCardHeader showCompactView={isCompactModeActive}>
|
||||||
<RecordChip
|
<RecordIndexRecordChip
|
||||||
objectNameSingular={objectMetadataItem.nameSingular}
|
objectNameSingular={objectMetadataItem.nameSingular}
|
||||||
record={record}
|
record={record}
|
||||||
variant={EntityChipVariant.Transparent}
|
variant={AvatarChipVariant.Transparent}
|
||||||
/>
|
/>
|
||||||
{isCompactModeActive && (
|
{isCompactModeActive && (
|
||||||
<StyledCompactIconContainer className="compact-icon-container">
|
<StyledCompactIconContainer className="compact-icon-container">
|
||||||
|
|||||||
@ -4,14 +4,13 @@ import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/dis
|
|||||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
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 { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
|
||||||
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
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 { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
||||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||||
import { isFieldChipDisplay } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
|
||||||
|
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
||||||
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
|
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
|
||||||
|
|||||||
@ -1,23 +1,21 @@
|
|||||||
import { EntityChip } from 'twenty-ui';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
|
|
||||||
import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay';
|
import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay';
|
||||||
|
import { RecordIndexRecordChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
|
||||||
|
|
||||||
export const ChipFieldDisplay = () => {
|
export const ChipFieldDisplay = () => {
|
||||||
const { recordValue, generateRecordChipData } = useChipFieldDisplay();
|
const { recordValue, objectNameSingular, isLabelIdentifier } =
|
||||||
|
useChipFieldDisplay();
|
||||||
|
|
||||||
if (!recordValue) {
|
if (!recordValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordChipData = generateRecordChipData(recordValue);
|
return isLabelIdentifier ? (
|
||||||
|
<RecordIndexRecordChip
|
||||||
return (
|
objectNameSingular={objectNameSingular}
|
||||||
<EntityChip
|
record={recordValue}
|
||||||
entityId={recordValue.id}
|
|
||||||
name={recordChipData.name as any}
|
|
||||||
avatarType={recordChipData.avatarType}
|
|
||||||
avatarUrl={recordChipData.avatarUrl ?? ''}
|
|
||||||
linkToEntity={recordChipData.linkToShowPage}
|
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<RecordChip objectNameSingular={objectNameSingular} record={recordValue} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,37 +1,27 @@
|
|||||||
import { EntityChip } from 'twenty-ui';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
|
|
||||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
|
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
|
||||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
|
||||||
|
|
||||||
export const RelationFromManyFieldDisplay = () => {
|
export const RelationFromManyFieldDisplay = () => {
|
||||||
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
|
||||||
useRelationFromManyFieldDisplay();
|
|
||||||
const { isFocused } = useFieldFocus();
|
const { isFocused } = useFieldFocus();
|
||||||
|
|
||||||
if (
|
const relationObjectNameSingular =
|
||||||
!fieldValue ||
|
fieldDefinition?.metadata.relationObjectMetadataNameSingular;
|
||||||
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
|
||||||
) {
|
if (!fieldValue || !relationObjectNameSingular) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordChipsData = fieldValue.map((fieldValueItem) =>
|
|
||||||
generateRecordChipData(fieldValueItem),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
{recordChipsData.map((record) => {
|
{fieldValue.map((record) => {
|
||||||
return (
|
return (
|
||||||
<EntityChip
|
<RecordChip
|
||||||
key={record.recordId}
|
key={record.id}
|
||||||
entityId={record.recordId}
|
objectNameSingular={relationObjectNameSingular}
|
||||||
name={record.name as any}
|
record={record}
|
||||||
avatarType={record.avatarType}
|
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(record.avatarUrl) || ''}
|
|
||||||
linkToEntity={record.linkToShowPage}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { EntityChip } from 'twenty-ui';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
|
|
||||||
import { useRelationToOneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay';
|
import { useRelationToOneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay';
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
|
||||||
|
|
||||||
export const RelationToOneFieldDisplay = () => {
|
export const RelationToOneFieldDisplay = () => {
|
||||||
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
||||||
@ -17,12 +15,10 @@ export const RelationToOneFieldDisplay = () => {
|
|||||||
const recordChipData = generateRecordChipData(fieldValue);
|
const recordChipData = generateRecordChipData(fieldValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityChip
|
<RecordChip
|
||||||
entityId={fieldValue.id}
|
key={recordChipData.recordId}
|
||||||
name={recordChipData.name as any}
|
objectNameSingular={recordChipData.objectNameSingular}
|
||||||
avatarType={recordChipData.avatarType}
|
record={fieldValue}
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(recordChipData.avatarUrl) || ''}
|
|
||||||
linkToEntity={recordChipData.linkToShowPage}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||||
|
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||||
|
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||||
|
|
||||||
|
export const isFieldChipDisplay = (
|
||||||
|
field: Pick<FieldMetadataItem, 'type'>,
|
||||||
|
isLabelIdentifier: boolean,
|
||||||
|
) =>
|
||||||
|
isLabelIdentifier &&
|
||||||
|
(isFieldText(field) || isFieldFullName(field) || isFieldNumber(field));
|
||||||
@ -1,8 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||||
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
|
|
||||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||||
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||||
@ -12,7 +11,8 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
|
|
||||||
export const useChipFieldDisplay = () => {
|
export const useChipFieldDisplay = () => {
|
||||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
const { entityId, fieldDefinition, isLabelIdentifier } =
|
||||||
|
useContext(FieldContext);
|
||||||
|
|
||||||
const { chipGeneratorPerObjectPerField } = useContext(
|
const { chipGeneratorPerObjectPerField } = useContext(
|
||||||
PreComputedChipGeneratorsContext,
|
PreComputedChipGeneratorsContext,
|
||||||
@ -31,18 +31,13 @@ export const useChipFieldDisplay = () => {
|
|||||||
|
|
||||||
const recordValue = useRecordValue(entityId);
|
const recordValue = useRecordValue(entityId);
|
||||||
|
|
||||||
if (!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)) {
|
if (!isNonEmptyString(objectNameSingular)) {
|
||||||
throw new Error('Object metadata name singular is not a non-empty string');
|
throw new Error('Object metadata name singular is not a non-empty string');
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateRecordChipData =
|
|
||||||
chipGeneratorPerObjectPerField[
|
|
||||||
fieldDefinition.metadata.objectMetadataNameSingular
|
|
||||||
]?.[fieldDefinition.metadata.fieldName] ?? generateDefaultRecordChipData;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordValue,
|
recordValue,
|
||||||
generateRecordChipData,
|
isLabelIdentifier,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const lastShowPageRecordIdState = createState<string | null>({
|
||||||
|
key: 'lastShowPageRecordIdState',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const recordPositionInternalState = createState<number | null>({
|
||||||
|
key: 'recordPositionInternalState',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
@ -2,8 +2,9 @@ import { AvatarType } from 'twenty-ui';
|
|||||||
|
|
||||||
export type RecordChipData = {
|
export type RecordChipData = {
|
||||||
recordId: string;
|
recordId: string;
|
||||||
name: string | number;
|
name: string;
|
||||||
avatarType: AvatarType;
|
avatarType: AvatarType;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
linkToShowPage: string;
|
isLabelIdentifier: boolean;
|
||||||
|
objectNameSingular: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
import {
|
||||||
|
useRecoilCallback,
|
||||||
|
useRecoilState,
|
||||||
|
useRecoilValue,
|
||||||
|
useSetRecoilState,
|
||||||
|
} from 'recoil';
|
||||||
|
|
||||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
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 { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
|
||||||
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
|
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
|
||||||
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
|
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
|
||||||
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
|
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
|
||||||
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
|
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
|
||||||
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
|
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
|
||||||
|
import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
|
||||||
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
|
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
|
||||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||||
@ -17,15 +26,22 @@ import { recordIndexIsCompactModeActiveState } from '@/object-record/record-inde
|
|||||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
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 { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
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 { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
|
||||||
import { ViewBar } from '@/views/components/ViewBar';
|
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 { ViewField } from '@/views/types/ViewField';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
||||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -108,6 +124,63 @@ export const RecordIndexContainer = ({
|
|||||||
[columnDefinitions, setTableColumns],
|
[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(
|
||||||
|
objectMetadataItem,
|
||||||
|
mapViewSortsToSorts(view?.viewSorts ?? [], sortDefinitions),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { findCursorInCache } = useFindRecordCursorFromFindManyCacheRootQuery({
|
||||||
|
fieldVariables: {
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
},
|
||||||
|
objectNamePlural: objectNamePlural,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIndexIdentifierClick = (recordId: string) => {
|
||||||
|
const cursor = findCursorInCache(recordId);
|
||||||
|
|
||||||
|
// TODO: use URL builder
|
||||||
|
navigate(
|
||||||
|
`/object/${objectNameSingular}/${recordId}?view=${currentViewId}`,
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
cursor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIndexRecordsLoaded = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
() => {
|
||||||
|
// TODO: find a better way to reset this state ?
|
||||||
|
set(lastShowPageRecordIdState, null);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<RecordFieldValueSelectorContextProvider>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
@ -153,41 +226,46 @@ export const RecordIndexContainer = ({
|
|||||||
/>
|
/>
|
||||||
</StyledContainerWithPadding>
|
</StyledContainerWithPadding>
|
||||||
</SpreadsheetImportProvider>
|
</SpreadsheetImportProvider>
|
||||||
|
<RecordIndexEventContext.Provider
|
||||||
{recordIndexViewType === ViewType.Table && (
|
value={{
|
||||||
<>
|
onIndexIdentifierClick: handleIndexIdentifierClick,
|
||||||
<RecordIndexTableContainer
|
onIndexRecordsLoaded: handleIndexRecordsLoaded,
|
||||||
recordTableId={recordIndexId}
|
}}
|
||||||
viewBarId={recordIndexId}
|
>
|
||||||
objectNameSingular={objectNameSingular}
|
{recordIndexViewType === ViewType.Table && (
|
||||||
createRecord={createRecord}
|
<>
|
||||||
/>
|
<RecordIndexTableContainer
|
||||||
<RecordIndexTableContainerEffect
|
recordTableId={recordIndexId}
|
||||||
objectNameSingular={objectNameSingular}
|
viewBarId={recordIndexId}
|
||||||
recordTableId={recordIndexId}
|
objectNameSingular={objectNameSingular}
|
||||||
viewBarId={recordIndexId}
|
createRecord={createRecord}
|
||||||
/>
|
/>
|
||||||
</>
|
<RecordIndexTableContainerEffect
|
||||||
)}
|
objectNameSingular={objectNameSingular}
|
||||||
|
recordTableId={recordIndexId}
|
||||||
{recordIndexViewType === ViewType.Kanban && (
|
viewBarId={recordIndexId}
|
||||||
<StyledContainerWithPadding>
|
/>
|
||||||
<RecordIndexBoardContainer
|
</>
|
||||||
recordBoardId={recordIndexId}
|
)}
|
||||||
viewBarId={recordIndexId}
|
{recordIndexViewType === ViewType.Kanban && (
|
||||||
objectNameSingular={objectNameSingular}
|
<StyledContainerWithPadding>
|
||||||
createRecord={createRecord}
|
<RecordIndexBoardContainer
|
||||||
/>
|
recordBoardId={recordIndexId}
|
||||||
<RecordIndexBoardDataLoader
|
viewBarId={recordIndexId}
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
recordBoardId={recordIndexId}
|
createRecord={createRecord}
|
||||||
/>
|
/>
|
||||||
<RecordIndexBoardDataLoaderEffect
|
<RecordIndexBoardDataLoader
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
recordBoardId={recordIndexId}
|
recordBoardId={recordIndexId}
|
||||||
/>
|
/>
|
||||||
</StyledContainerWithPadding>
|
<RecordIndexBoardDataLoaderEffect
|
||||||
)}
|
objectNameSingular={objectNameSingular}
|
||||||
|
recordBoardId={recordIndexId}
|
||||||
|
/>
|
||||||
|
</StyledContainerWithPadding>
|
||||||
|
)}
|
||||||
|
</RecordIndexEventContext.Provider>
|
||||||
</RecordFieldValueSelectorContextProvider>
|
</RecordFieldValueSelectorContextProvider>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { AvatarChip, AvatarChipVariant } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
||||||
|
import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
export type RecordIndexRecordChipProps = {
|
||||||
|
objectNameSingular: string;
|
||||||
|
record: ObjectRecord;
|
||||||
|
variant?: AvatarChipVariant;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordIndexRecordChip = ({
|
||||||
|
objectNameSingular,
|
||||||
|
record,
|
||||||
|
variant,
|
||||||
|
}: RecordIndexRecordChipProps) => {
|
||||||
|
const { onIndexIdentifierClick } = useContext(RecordIndexEventContext);
|
||||||
|
|
||||||
|
const { recordChipData } = useRecordChipData({
|
||||||
|
objectNameSingular,
|
||||||
|
record,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAvatarChipClick = () => {
|
||||||
|
onIndexIdentifierClick(record.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvatarChip
|
||||||
|
placeholderColorSeed={record.id}
|
||||||
|
name={recordChipData.name}
|
||||||
|
avatarType={recordChipData.avatarType}
|
||||||
|
avatarUrl={recordChipData.avatarUrl ?? ''}
|
||||||
|
onClick={handleAvatarChipClick}
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { createEventContext } from '~/utils/createEventContext';
|
||||||
|
|
||||||
|
export type RecordIndexEventContextProps = {
|
||||||
|
onIndexIdentifierClick: (recordId: string) => void;
|
||||||
|
onIndexRecordsLoaded: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordIndexEventContext =
|
||||||
|
createEventContext<RecordIndexEventContextProps>();
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
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 };
|
||||||
|
};
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { createApolloStoreFieldName } from '~/utils/createApolloStoreFieldName';
|
||||||
|
|
||||||
|
export const useRecordIdsFromFindManyCacheRootQuery = ({
|
||||||
|
objectNamePlural,
|
||||||
|
fieldVariables,
|
||||||
|
}: {
|
||||||
|
objectNamePlural: string;
|
||||||
|
fieldVariables: {
|
||||||
|
filter: any;
|
||||||
|
orderBy: any;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const apollo = useApolloClient();
|
||||||
|
|
||||||
|
const testsFieldNameOnRootQuery = createApolloStoreFieldName({
|
||||||
|
fieldName: objectNamePlural,
|
||||||
|
fieldVariables: fieldVariables,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extractedCache = apollo.cache.extract() as any;
|
||||||
|
|
||||||
|
const recordIdsInCache: string[] =
|
||||||
|
extractedCache?.['ROOT_QUERY']?.[testsFieldNameOnRootQuery]?.edges?.map(
|
||||||
|
(edge: RecordGqlRefEdge) => edge.node?.__ref.split(':')[1],
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
return { recordIdsInCache };
|
||||||
|
};
|
||||||
@ -0,0 +1,253 @@
|
|||||||
|
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
useLocation,
|
||||||
|
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 { 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,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
objectNameSingular: paramObjectNameSingular,
|
||||||
|
objectRecordId: paramObjectRecordId,
|
||||||
|
} = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const viewIdQueryParam = searchParams.get('view');
|
||||||
|
|
||||||
|
const setLastShowPageRecordId = useSetRecoilState(lastShowPageRecordIdState);
|
||||||
|
|
||||||
|
const [isLoadedRecords, setIsLoadedRecords] = useState(false);
|
||||||
|
|
||||||
|
const objectNameSingular = propsObjectNameSingular || paramObjectNameSingular;
|
||||||
|
const objectRecordId = propsObjectRecordId || paramObjectRecordId;
|
||||||
|
|
||||||
|
if (!objectNameSingular || !objectRecordId) {
|
||||||
|
throw new Error('Object name or Record id is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 cursorFromIndexPage = state.cursor;
|
||||||
|
|
||||||
|
const { loading: loadingCurrentRecord, pageInfo: currentRecordsPageInfo } =
|
||||||
|
useFindManyRecords({
|
||||||
|
filter: {
|
||||||
|
id: { eq: objectRecordId },
|
||||||
|
},
|
||||||
|
orderBy,
|
||||||
|
skip: isLoadedRecords,
|
||||||
|
limit: 1,
|
||||||
|
objectNameSingular,
|
||||||
|
recordGqlFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentRecordCursor = currentRecordsPageInfo?.endCursor;
|
||||||
|
|
||||||
|
const cursor = cursorFromIndexPage ?? currentRecordCursor;
|
||||||
|
|
||||||
|
const {
|
||||||
|
loading: loadingRecordBefore,
|
||||||
|
records: recordsBefore,
|
||||||
|
pageInfo: pageInfoBefore,
|
||||||
|
totalCount: totalCountBefore,
|
||||||
|
} = useFindManyRecords({
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
skip: isLoadedRecords,
|
||||||
|
cursorFilter: isNonEmptyString(cursor)
|
||||||
|
? {
|
||||||
|
cursorDirection: 'before',
|
||||||
|
cursor: cursor,
|
||||||
|
limit: 1,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
objectNameSingular,
|
||||||
|
recordGqlFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
loading: loadingRecordAfter,
|
||||||
|
records: recordsAfter,
|
||||||
|
pageInfo: pageInfoAfter,
|
||||||
|
totalCount: totalCountAfter,
|
||||||
|
} = useFindManyRecords({
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
skip: isLoadedRecords,
|
||||||
|
cursorFilter: cursor
|
||||||
|
? {
|
||||||
|
cursorDirection: 'after',
|
||||||
|
cursor: cursor,
|
||||||
|
limit: 1,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
objectNameSingular,
|
||||||
|
recordGqlFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCount = Math.max(totalCountBefore ?? 0, totalCountAfter ?? 0);
|
||||||
|
|
||||||
|
const loading =
|
||||||
|
loadingRecordAfter || loadingRecordBefore || loadingCurrentRecord;
|
||||||
|
|
||||||
|
const isThereARecordBefore = recordsBefore.length > 0;
|
||||||
|
const isThereARecordAfter = recordsAfter.length > 0;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToNextRecord = () => {
|
||||||
|
navigate(
|
||||||
|
`/object/${objectNameSingular}/${recordAfter.id}${
|
||||||
|
viewIdQueryParam ? `?view=${viewIdQueryParam}` : ''
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
cursor: recordAfterCursor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToIndexView = () => {
|
||||||
|
const indexPath = `/objects/${objectMetadataItem.namePlural}${
|
||||||
|
viewIdQueryParam ? `?view=${viewIdQueryParam}` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
setLastShowPageRecordId(objectRecordId);
|
||||||
|
|
||||||
|
navigate(indexPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
|
||||||
|
objectNamePlural: objectMetadataItem.namePlural,
|
||||||
|
fieldVariables: {
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId);
|
||||||
|
|
||||||
|
const rankFoundInFiew = rankInView > -1;
|
||||||
|
|
||||||
|
const objectLabel = capitalize(objectMetadataItem.namePlural);
|
||||||
|
|
||||||
|
const viewNameWithCount = rankFoundInFiew
|
||||||
|
? `${rankInView + 1} of ${totalCount} in ${objectLabel}`
|
||||||
|
: `${objectLabel} (${totalCount})`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewName: viewNameWithCount,
|
||||||
|
hasPreviousRecord: isThereARecordBefore,
|
||||||
|
isLoadingPagination: loading,
|
||||||
|
hasNextRecord: isThereARecordAfter,
|
||||||
|
navigateToPreviousRecord,
|
||||||
|
navigateToNextRecord,
|
||||||
|
navigateToIndexView,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -21,6 +21,7 @@ const StyledTable = styled.table`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type RecordTableProps = {
|
type RecordTableProps = {
|
||||||
|
viewBarId: string;
|
||||||
recordTableId: string;
|
recordTableId: string;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
onColumnsChange: (columns: any) => void;
|
onColumnsChange: (columns: any) => void;
|
||||||
@ -28,6 +29,7 @@ type RecordTableProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RecordTable = ({
|
export const RecordTable = ({
|
||||||
|
viewBarId,
|
||||||
recordTableId,
|
recordTableId,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
onColumnsChange,
|
onColumnsChange,
|
||||||
@ -68,6 +70,7 @@ export const RecordTable = ({
|
|||||||
<RecordTableContextProvider
|
<RecordTableContextProvider
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
recordTableId={recordTableId}
|
recordTableId={recordTableId}
|
||||||
|
viewBarId={viewBarId}
|
||||||
>
|
>
|
||||||
<RecordTableBodyEffect />
|
<RecordTableBodyEffect />
|
||||||
{!isRecordTableInitialLoading &&
|
{!isRecordTableInitialLoading &&
|
||||||
|
|||||||
@ -18,10 +18,12 @@ import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocus
|
|||||||
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||||
|
|
||||||
export const RecordTableContextProvider = ({
|
export const RecordTableContextProvider = ({
|
||||||
|
viewBarId,
|
||||||
recordTableId,
|
recordTableId,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
viewBarId: string;
|
||||||
recordTableId: string;
|
recordTableId: string;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -90,6 +92,7 @@ export const RecordTableContextProvider = ({
|
|||||||
return (
|
return (
|
||||||
<RecordTableContext.Provider
|
<RecordTableContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
viewBarId,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
onUpsertRecord: handleUpsertRecord,
|
onUpsertRecord: handleUpsertRecord,
|
||||||
onOpenTableCell: handleOpenTableCell,
|
onOpenTableCell: handleOpenTableCell,
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const RecordTableInternalEffect = ({
|
|||||||
callback: () => {
|
callback: () => {
|
||||||
leaveTableFocus();
|
leaveTableFocus();
|
||||||
},
|
},
|
||||||
mode: ClickOutsideMode.comparePixels,
|
mode: ClickOutsideMode.compareHTMLRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export const RecordTableWithWrappers = ({
|
|||||||
<StyledTableContainer>
|
<StyledTableContainer>
|
||||||
<StyledTableInternalContainer ref={tableBodyRef}>
|
<StyledTableInternalContainer ref={tableBodyRef}>
|
||||||
<RecordTable
|
<RecordTable
|
||||||
|
viewBarId={viewBarId}
|
||||||
recordTableId={recordTableId}
|
recordTableId={recordTableId}
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
onColumnsChange={handleColumnsChange}
|
onColumnsChange={handleColumnsChange}
|
||||||
|
|||||||
@ -64,6 +64,7 @@ const meta: Meta = {
|
|||||||
<RecordFieldValueSelectorContextProvider>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
<RecordTableContext.Provider
|
<RecordTableContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
viewBarId: mockPerformance.entityId,
|
||||||
objectMetadataItem: mockPerformance.objectMetadataItem as any,
|
objectMetadataItem: mockPerformance.objectMetadataItem as any,
|
||||||
onUpsertRecord: () => {},
|
onUpsertRecord: () => {},
|
||||||
onOpenTableCell: () => {},
|
onOpenTableCell: () => {},
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocus
|
|||||||
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||||
|
|
||||||
export type RecordTableContextProps = {
|
export type RecordTableContextProps = {
|
||||||
|
viewBarId: string;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
onUpsertRecord: ({
|
onUpsertRecord: ({
|
||||||
persistField,
|
persistField,
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
|
||||||
import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
|
import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
@ -11,11 +12,17 @@ import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetch
|
|||||||
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
|
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
|
||||||
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
|
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
|
||||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
|
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
|
||||||
|
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
|
||||||
|
|
||||||
|
export const ROW_HEIGHT = 32;
|
||||||
|
|
||||||
export const RecordTableBodyEffect = () => {
|
export const RecordTableBodyEffect = () => {
|
||||||
const { objectNameSingular } = useContext(RecordTableContext);
|
const { objectNameSingular } = useContext(RecordTableContext);
|
||||||
|
|
||||||
|
const [hasInitializedScroll, setHasInitiazedScroll] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fetchMoreRecords: fetchMoreObjects,
|
fetchMoreRecords: fetchMoreObjects,
|
||||||
records,
|
records,
|
||||||
@ -76,9 +83,44 @@ export const RecordTableBodyEffect = () => {
|
|||||||
}
|
}
|
||||||
}, [scrollLeft, setIsRecordTableScrolledLeft]);
|
}, [scrollLeft, setIsRecordTableScrolledLeft]);
|
||||||
|
|
||||||
const rowHeight = 32;
|
const rowHeight = ROW_HEIGHT;
|
||||||
const viewportHeight = records.length * rowHeight;
|
const viewportHeight = records.length * rowHeight;
|
||||||
|
|
||||||
|
const [lastShowPageRecordId, setLastShowPageRecordId] = useRecoilState(
|
||||||
|
lastShowPageRecordIdState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { scrollToPosition } = useScrollToPosition();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNonEmptyString(lastShowPageRecordId) && !hasInitializedScroll) {
|
||||||
|
const isRecordAlreadyFetched = records.some(
|
||||||
|
(record) => record.id === lastShowPageRecordId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRecordAlreadyFetched) {
|
||||||
|
const recordPosition = records.findIndex(
|
||||||
|
(record) => record.id === lastShowPageRecordId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const positionInPx = recordPosition * ROW_HEIGHT;
|
||||||
|
|
||||||
|
scrollToPosition(positionInPx);
|
||||||
|
|
||||||
|
setHasInitiazedScroll(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
loading,
|
||||||
|
isFetchingMoreObjects,
|
||||||
|
lastShowPageRecordId,
|
||||||
|
fetchMoreObjects,
|
||||||
|
records,
|
||||||
|
scrollToPosition,
|
||||||
|
hasInitializedScroll,
|
||||||
|
setLastShowPageRecordId,
|
||||||
|
]);
|
||||||
|
|
||||||
useScrollRestoration(viewportHeight);
|
useScrollRestoration(viewportHeight);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
|
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
|
||||||
@ -21,6 +20,8 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
|
|||||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||||
|
|
||||||
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
||||||
@ -40,13 +41,13 @@ export type OpenTableCellArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||||
|
const { onIndexIdentifierClick } = useContext(RecordIndexEventContext);
|
||||||
const moveEditModeToTableCellPosition =
|
const moveEditModeToTableCellPosition =
|
||||||
useMoveEditModeToTableCellPosition(tableScopeId);
|
useMoveEditModeToTableCellPosition(tableScopeId);
|
||||||
|
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
const { setDragSelectionStartEnabled } = useDragSelect();
|
const { setDragSelectionStartEnabled } = useDragSelect();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const leaveTableFocus = useLeaveTableFocus(tableScopeId);
|
const leaveTableFocus = useLeaveTableFocus(tableScopeId);
|
||||||
const { toggleClickOutsideListener } = useClickOutsideListener(
|
const { toggleClickOutsideListener } = useClickOutsideListener(
|
||||||
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
|
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
|
||||||
@ -66,7 +67,6 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
|||||||
initialValue,
|
initialValue,
|
||||||
cellPosition,
|
cellPosition,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
pathToShowPage,
|
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
customCellHotkeyScope,
|
customCellHotkeyScope,
|
||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
@ -94,7 +94,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
|||||||
|
|
||||||
if (isFirstColumnCell && !isEmpty && !isActionButtonClick) {
|
if (isFirstColumnCell && !isEmpty && !isActionButtonClick) {
|
||||||
leaveTableFocus();
|
leaveTableFocus();
|
||||||
navigate(pathToShowPage);
|
|
||||||
|
onIndexIdentifierClick(entityId);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -142,7 +143,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
|||||||
openRightDrawer,
|
openRightDrawer,
|
||||||
setViewableRecordId,
|
setViewableRecordId,
|
||||||
setViewableRecordNameSingular,
|
setViewableRecordNameSingular,
|
||||||
navigate,
|
onIndexIdentifierClick,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { ReactNode, useContext } from 'react';
|
|
||||||
import { useInView } from 'react-intersection-observer';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { Draggable } from '@hello-pangea/dnd';
|
import { Draggable } from '@hello-pangea/dnd';
|
||||||
|
import { ReactNode, useContext, useEffect } from 'react';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||||
|
import { RecordIndexEventContext } from '@/object-record/record-index/contexts/RecordIndexEventContext';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
@ -23,6 +24,7 @@ export const RecordTableRowWrapper = ({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const { objectMetadataItem } = useContext(RecordTableContext);
|
const { objectMetadataItem } = useContext(RecordTableContext);
|
||||||
|
const { onIndexRecordsLoaded } = useContext(RecordIndexEventContext);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -38,6 +40,13 @@ export const RecordTableRowWrapper = ({
|
|||||||
rootMargin: '1000px',
|
rootMargin: '1000px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: find a better way to emit this event
|
||||||
|
useEffect(() => {
|
||||||
|
if (inView) {
|
||||||
|
onIndexRecordsLoaded?.();
|
||||||
|
}
|
||||||
|
}, [inView, onIndexRecordsLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable key={recordId} draggableId={recordId} index={rowIndex}>
|
<Draggable key={recordId} draggableId={recordId} index={rowIndex}>
|
||||||
{(draggableProvided, draggableSnapshot) => (
|
{(draggableProvided, draggableSnapshot) => (
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export const MultipleObjectRecordSelectItem = ({
|
|||||||
avatar={
|
avatar={
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(recordIdentifier.avatarUrl)}
|
avatarUrl={getImageAbsoluteURIOrBase64(recordIdentifier.avatarUrl)}
|
||||||
entityId={objectRecordId}
|
placeholderColorSeed={objectRecordId}
|
||||||
placeholder={recordIdentifier.name}
|
placeholder={recordIdentifier.name}
|
||||||
size="md"
|
size="md"
|
||||||
type={recordIdentifier.avatarType ?? 'rounded'}
|
type={recordIdentifier.avatarType ?? 'rounded'}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export const SelectableMenuItemSelect = ({
|
|||||||
avatar={
|
avatar={
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(entity.avatarUrl)}
|
avatarUrl={getImageAbsoluteURIOrBase64(entity.avatarUrl)}
|
||||||
entityId={entity.id}
|
placeholderColorSeed={entity.id}
|
||||||
placeholder={entity.name}
|
placeholder={entity.name}
|
||||||
size="md"
|
size="md"
|
||||||
type={entity.avatarType ?? 'rounded'}
|
type={entity.avatarType ?? 'rounded'}
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export const MultipleRecordSelectDropdown = ({
|
|||||||
avatar={
|
avatar={
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(record.avatarUrl)}
|
avatarUrl={getImageAbsoluteURIOrBase64(record.avatarUrl)}
|
||||||
entityId={record.id}
|
placeholderColorSeed={record.id}
|
||||||
placeholder={record.name}
|
placeholder={record.name}
|
||||||
size="md"
|
size="md"
|
||||||
type={record.avatarType ?? 'rounded'}
|
type={record.avatarType ?? 'rounded'}
|
||||||
|
|||||||
@ -1,21 +1,24 @@
|
|||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
|
|
||||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||||
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export type QueryCursorDirection = 'before' | 'after';
|
||||||
|
|
||||||
export const generateFindManyRecordsQuery = ({
|
export const generateFindManyRecordsQuery = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
computeReferences,
|
computeReferences,
|
||||||
|
cursorDirection,
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
objectMetadataItems: ObjectMetadataItem[];
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
computeReferences?: boolean;
|
computeReferences?: boolean;
|
||||||
|
cursorDirection?: QueryCursorDirection;
|
||||||
}) => gql`
|
}) => gql`
|
||||||
query FindMany${capitalize(
|
query FindMany${capitalize(
|
||||||
objectMetadataItem.namePlural,
|
objectMetadataItem.namePlural,
|
||||||
@ -24,9 +27,11 @@ query FindMany${capitalize(
|
|||||||
)}FilterInput, $orderBy: [${capitalize(
|
)}FilterInput, $orderBy: [${capitalize(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
)}OrderByInput], $lastCursor: String, $limit: Int) {
|
)}OrderByInput], $lastCursor: String, $limit: Int) {
|
||||||
${
|
${objectMetadataItem.namePlural}(filter: $filter, orderBy: $orderBy, ${
|
||||||
objectMetadataItem.namePlural
|
cursorDirection === 'before'
|
||||||
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){
|
? 'last: $limit, before: $lastCursor'
|
||||||
|
: 'first: $limit, after: $lastCursor'
|
||||||
|
} ){
|
||||||
edges {
|
edges {
|
||||||
node ${mapObjectMetadataToGraphQLQuery({
|
node ${mapObjectMetadataToGraphQLQuery({
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
@ -37,11 +42,12 @@ query FindMany${capitalize(
|
|||||||
cursor
|
cursor
|
||||||
}
|
}
|
||||||
pageInfo {
|
pageInfo {
|
||||||
${isAggregationEnabled(objectMetadataItem) ? 'hasNextPage' : ''}
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
startCursor
|
startCursor
|
||||||
endCursor
|
endCursor
|
||||||
}
|
}
|
||||||
${isAggregationEnabled(objectMetadataItem) ? 'totalCount' : ''}
|
totalCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -5,7 +5,12 @@ export const getQueryIdentifier = ({
|
|||||||
filter,
|
filter,
|
||||||
orderBy,
|
orderBy,
|
||||||
limit,
|
limit,
|
||||||
|
cursorFilter,
|
||||||
}: RecordGqlOperationVariables & {
|
}: RecordGqlOperationVariables & {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
}) =>
|
}) =>
|
||||||
objectNameSingular + JSON.stringify(filter) + JSON.stringify(orderBy) + limit;
|
objectNameSingular +
|
||||||
|
JSON.stringify(filter) +
|
||||||
|
JSON.stringify(orderBy) +
|
||||||
|
limit +
|
||||||
|
(cursorFilter ? JSON.stringify(cursorFilter) : undefined);
|
||||||
|
|||||||
@ -1,116 +0,0 @@
|
|||||||
import { ChipGeneratorPerObjectPerField } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
|
|
||||||
import { getAvatarUrl } from '@/object-metadata/utils/getAvatarUrl';
|
|
||||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
|
||||||
import { getLabelIdentifierFieldValue } from '@/object-metadata/utils/getLabelIdentifierFieldValue';
|
|
||||||
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
|
||||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
|
||||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
|
||||||
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
|
||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
|
||||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
|
||||||
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const isFieldChipDisplay = (
|
|
||||||
field: Pick<FieldMetadataItem, 'type'>,
|
|
||||||
isLabelIdentifier: boolean,
|
|
||||||
) =>
|
|
||||||
isLabelIdentifier &&
|
|
||||||
(isFieldText(field) || isFieldFullName(field) || isFieldNumber(field));
|
|
||||||
|
|
||||||
export const getRecordChipGeneratorPerObjectPerField = (
|
|
||||||
objectMetadataItems: ObjectMetadataItem[],
|
|
||||||
) => {
|
|
||||||
const recordChipGeneratorPerObjectPerField: ChipGeneratorPerObjectPerField =
|
|
||||||
{};
|
|
||||||
|
|
||||||
for (const objectMetadataItem of objectMetadataItems) {
|
|
||||||
const generatorPerField = Object.fromEntries<
|
|
||||||
(record: ObjectRecord) => RecordChipData
|
|
||||||
>(
|
|
||||||
objectMetadataItem.fields
|
|
||||||
.filter(
|
|
||||||
(fieldMetadataItem) =>
|
|
||||||
isLabelIdentifierField({
|
|
||||||
fieldMetadataItem: fieldMetadataItem,
|
|
||||||
objectMetadataItem,
|
|
||||||
}) ||
|
|
||||||
fieldMetadataItem.type === FieldMetadataType.Relation ||
|
|
||||||
isFieldChipDisplay(
|
|
||||||
fieldMetadataItem,
|
|
||||||
isLabelIdentifierField({
|
|
||||||
fieldMetadataItem: fieldMetadataItem,
|
|
||||||
objectMetadataItem,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((fieldMetadataItem) => {
|
|
||||||
const objectNameSingularToFind = isLabelIdentifierField({
|
|
||||||
fieldMetadataItem: fieldMetadataItem,
|
|
||||||
objectMetadataItem: objectMetadataItem,
|
|
||||||
})
|
|
||||||
? objectMetadataItem.nameSingular
|
|
||||||
: isFieldRelation(fieldMetadataItem)
|
|
||||||
? fieldMetadataItem.relationDefinition?.targetObjectMetadata
|
|
||||||
.nameSingular ?? undefined
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const objectMetadataItemToUse = objectMetadataItems.find(
|
|
||||||
(objectMetadataItem) =>
|
|
||||||
objectMetadataItem.nameSingular === objectNameSingularToFind,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isDefined(objectMetadataItemToUse) ||
|
|
||||||
!isDefined(objectNameSingularToFind)
|
|
||||||
) {
|
|
||||||
return ['', () => ({}) as any];
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelIdentifierFieldMetadataItem =
|
|
||||||
getLabelIdentifierFieldMetadataItem(objectMetadataItemToUse);
|
|
||||||
|
|
||||||
const imageIdentifierFieldMetadata =
|
|
||||||
objectMetadataItemToUse.fields.find(
|
|
||||||
(field) =>
|
|
||||||
field.id ===
|
|
||||||
objectMetadataItemToUse.imageIdentifierFieldMetadataId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const avatarType = getAvatarType(objectNameSingularToFind);
|
|
||||||
|
|
||||||
return [
|
|
||||||
fieldMetadataItem.name,
|
|
||||||
(record: ObjectRecord) => ({
|
|
||||||
recordId: record.id,
|
|
||||||
name: getLabelIdentifierFieldValue(
|
|
||||||
record,
|
|
||||||
labelIdentifierFieldMetadataItem,
|
|
||||||
objectMetadataItemToUse.nameSingular,
|
|
||||||
),
|
|
||||||
avatarUrl: getAvatarUrl(
|
|
||||||
objectMetadataItemToUse.nameSingular,
|
|
||||||
record,
|
|
||||||
imageIdentifierFieldMetadata,
|
|
||||||
),
|
|
||||||
avatarType,
|
|
||||||
linkToShowPage: getLinkToShowPage(
|
|
||||||
objectMetadataItemToUse.nameSingular,
|
|
||||||
record,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
recordChipGeneratorPerObjectPerField[objectMetadataItem.nameSingular] =
|
|
||||||
generatorPerField;
|
|
||||||
}
|
|
||||||
|
|
||||||
return recordChipGeneratorPerObjectPerField;
|
|
||||||
};
|
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
ChipGeneratorPerObjectNameSingularPerFieldName,
|
||||||
|
IdentifierChipGeneratorPerObject,
|
||||||
|
} from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
|
||||||
|
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 { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const getRecordChipGenerators = (
|
||||||
|
objectMetadataItems: ObjectMetadataItem[],
|
||||||
|
) => {
|
||||||
|
const chipGeneratorPerObjectPerField: ChipGeneratorPerObjectNameSingularPerFieldName =
|
||||||
|
{};
|
||||||
|
|
||||||
|
const identifierChipGeneratorPerObject: IdentifierChipGeneratorPerObject = {};
|
||||||
|
|
||||||
|
for (const objectMetadataItem of objectMetadataItems) {
|
||||||
|
const labelIdentifierFieldMetadataItem =
|
||||||
|
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||||
|
|
||||||
|
const generatorPerField = Object.fromEntries<
|
||||||
|
(record: ObjectRecord) => RecordChipData
|
||||||
|
>(
|
||||||
|
objectMetadataItem.fields
|
||||||
|
.filter(
|
||||||
|
(fieldMetadataItem) =>
|
||||||
|
labelIdentifierFieldMetadataItem?.id === fieldMetadataItem.id ||
|
||||||
|
fieldMetadataItem.type === FieldMetadataType.Relation ||
|
||||||
|
isFieldChipDisplay(
|
||||||
|
fieldMetadataItem,
|
||||||
|
isLabelIdentifierField({
|
||||||
|
fieldMetadataItem: fieldMetadataItem,
|
||||||
|
objectMetadataItem,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map((fieldMetadataItem) => {
|
||||||
|
const isLabelIdentifier =
|
||||||
|
labelIdentifierFieldMetadataItem?.id === fieldMetadataItem.id;
|
||||||
|
|
||||||
|
const currentObjectNameSingular = objectMetadataItem.nameSingular;
|
||||||
|
const fieldRelationObjectNameSingular =
|
||||||
|
fieldMetadataItem.relationDefinition?.targetObjectMetadata
|
||||||
|
.nameSingular ?? undefined;
|
||||||
|
|
||||||
|
const objectNameSingularToFind = isLabelIdentifier
|
||||||
|
? currentObjectNameSingular
|
||||||
|
: fieldRelationObjectNameSingular;
|
||||||
|
|
||||||
|
const objectMetadataItemToUse = objectMetadataItems.find(
|
||||||
|
(objectMetadataItem) =>
|
||||||
|
objectMetadataItem.nameSingular === objectNameSingularToFind,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDefined(objectMetadataItemToUse) ||
|
||||||
|
!isDefined(objectNameSingularToFind)
|
||||||
|
) {
|
||||||
|
return ['', () => ({}) as any];
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelIdentifierFieldMetadataItemToUse =
|
||||||
|
getLabelIdentifierFieldMetadataItem(objectMetadataItemToUse);
|
||||||
|
|
||||||
|
const imageIdentifierFieldMetadataToUse =
|
||||||
|
objectMetadataItemToUse.fields.find(
|
||||||
|
(field) =>
|
||||||
|
field.id ===
|
||||||
|
objectMetadataItemToUse.imageIdentifierFieldMetadataId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const avatarType = getAvatarType(objectNameSingularToFind);
|
||||||
|
|
||||||
|
return [
|
||||||
|
fieldMetadataItem.name,
|
||||||
|
(record: ObjectRecord) =>
|
||||||
|
({
|
||||||
|
recordId: record.id,
|
||||||
|
name: getLabelIdentifierFieldValue(
|
||||||
|
record,
|
||||||
|
labelIdentifierFieldMetadataItemToUse,
|
||||||
|
objectMetadataItemToUse.nameSingular,
|
||||||
|
),
|
||||||
|
avatarUrl: getAvatarUrl(
|
||||||
|
objectMetadataItemToUse.nameSingular,
|
||||||
|
record,
|
||||||
|
imageIdentifierFieldMetadataToUse,
|
||||||
|
),
|
||||||
|
avatarType,
|
||||||
|
isLabelIdentifier,
|
||||||
|
objectNameSingular: objectNameSingularToFind,
|
||||||
|
}) satisfies RecordChipData,
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
chipGeneratorPerObjectPerField[objectMetadataItem.nameSingular] =
|
||||||
|
generatorPerField;
|
||||||
|
|
||||||
|
if (isDefined(labelIdentifierFieldMetadataItem)) {
|
||||||
|
identifierChipGeneratorPerObject[objectMetadataItem.nameSingular] =
|
||||||
|
chipGeneratorPerObjectPerField[objectMetadataItem.nameSingular]?.[
|
||||||
|
labelIdentifierFieldMetadataItem.name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
chipGeneratorPerObjectPerField,
|
||||||
|
identifierChipGeneratorPerObject,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -4,14 +4,15 @@ import { useTheme } from '@emotion/react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import {
|
import {
|
||||||
IconChevronLeft,
|
IconChevronDown,
|
||||||
|
IconChevronUp,
|
||||||
IconComponent,
|
IconComponent,
|
||||||
|
IconX,
|
||||||
MOBILE_VIEWPORT,
|
MOBILE_VIEWPORT,
|
||||||
OverflowingTextWithTooltip,
|
OverflowingTextWithTooltip,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||||
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
|
||||||
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
|
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
|
||||||
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
|
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
@ -53,6 +54,7 @@ const StyledLeftContainer = styled.div`
|
|||||||
const StyledTitleContainer = styled.div`
|
const StyledTitleContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: ${({ theme }) => theme.font.size.md};
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
`;
|
`;
|
||||||
@ -61,6 +63,7 @@ const StyledTopBarIconStyledTitleContainer = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -89,7 +92,13 @@ const StyledSkeletonLoader = () => {
|
|||||||
|
|
||||||
type PageHeaderProps = ComponentProps<'div'> & {
|
type PageHeaderProps = ComponentProps<'div'> & {
|
||||||
title: string;
|
title: string;
|
||||||
hasBackButton?: boolean;
|
hasClosePageButton?: boolean;
|
||||||
|
onClosePage?: () => void;
|
||||||
|
hasPaginationButtons?: boolean;
|
||||||
|
hasPreviousRecord?: boolean;
|
||||||
|
hasNextRecord?: boolean;
|
||||||
|
navigateToPreviousRecord?: () => void;
|
||||||
|
navigateToNextRecord?: () => void;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@ -97,7 +106,13 @@ type PageHeaderProps = ComponentProps<'div'> & {
|
|||||||
|
|
||||||
export const PageHeader = ({
|
export const PageHeader = ({
|
||||||
title,
|
title,
|
||||||
hasBackButton,
|
hasClosePageButton,
|
||||||
|
onClosePage,
|
||||||
|
hasPaginationButtons,
|
||||||
|
hasPreviousRecord,
|
||||||
|
hasNextRecord,
|
||||||
|
navigateToPreviousRecord,
|
||||||
|
navigateToNextRecord,
|
||||||
Icon,
|
Icon,
|
||||||
children,
|
children,
|
||||||
loading,
|
loading,
|
||||||
@ -114,19 +129,36 @@ export const PageHeader = ({
|
|||||||
<NavigationDrawerCollapseButton direction="right" />
|
<NavigationDrawerCollapseButton direction="right" />
|
||||||
</StyledTopBarButtonContainer>
|
</StyledTopBarButtonContainer>
|
||||||
)}
|
)}
|
||||||
{hasBackButton && (
|
{hasClosePageButton && (
|
||||||
<UndecoratedLink to={-1}>
|
<IconButton
|
||||||
<IconButton
|
Icon={IconX}
|
||||||
Icon={IconChevronLeft}
|
size="small"
|
||||||
size="small"
|
variant="tertiary"
|
||||||
variant="tertiary"
|
onClick={() => onClosePage?.()}
|
||||||
/>
|
/>
|
||||||
</UndecoratedLink>
|
|
||||||
)}
|
)}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<StyledSkeletonLoader />
|
<StyledSkeletonLoader />
|
||||||
) : (
|
) : (
|
||||||
<StyledTopBarIconStyledTitleContainer>
|
<StyledTopBarIconStyledTitleContainer>
|
||||||
|
{hasPaginationButtons && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
Icon={IconChevronUp}
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={!hasPreviousRecord}
|
||||||
|
onClick={() => navigateToPreviousRecord?.()}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
Icon={IconChevronDown}
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={!hasNextRecord}
|
||||||
|
onClick={() => navigateToNextRecord?.()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{Icon && <Icon size={theme.icon.size.md} />}
|
{Icon && <Icon size={theme.icon.size.md} />}
|
||||||
<StyledTitleContainer data-testid="top-bar-title">
|
<StyledTitleContainer data-testid="top-bar-title">
|
||||||
<OverflowingTextWithTooltip text={title} />
|
<OverflowingTextWithTooltip text={title} />
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ChangeEvent, ReactNode, useRef } from 'react';
|
|
||||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ChangeEvent, ReactNode, useRef } from 'react';
|
||||||
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
import { AppTooltip, Avatar, AvatarType } from 'twenty-ui';
|
import { AppTooltip, Avatar, AvatarType } from 'twenty-ui';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ export const ShowPageSummaryCard = ({
|
|||||||
avatarUrl={logoOrAvatar}
|
avatarUrl={logoOrAvatar}
|
||||||
onClick={onUploadPicture ? handleAvatarClick : undefined}
|
onClick={onUploadPicture ? handleAvatarClick : undefined}
|
||||||
size="xl"
|
size="xl"
|
||||||
entityId={id}
|
placeholderColorSeed={id}
|
||||||
placeholder={avatarPlaceholder}
|
placeholder={avatarPlaceholder}
|
||||||
type={avatarType}
|
type={avatarType}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { EntityChip } from 'twenty-ui';
|
import { AvatarChip } from 'twenty-ui';
|
||||||
|
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
|
|
||||||
@ -9,8 +9,8 @@ export type UserChipProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UserChip = ({ id, name, avatarUrl }: UserChipProps) => (
|
export const UserChip = ({ id, name, avatarUrl }: UserChipProps) => (
|
||||||
<EntityChip
|
<AvatarChip
|
||||||
entityId={id}
|
placeholderColorSeed={id}
|
||||||
name={name}
|
name={name}
|
||||||
avatarType="rounded"
|
avatarType="rounded"
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl) || ''}
|
avatarUrl={getImageAbsoluteURIOrBase64(avatarUrl) || ''}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export const WorkspaceMemberCard = ({
|
|||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(workspaceMember.avatarUrl)}
|
avatarUrl={getImageAbsoluteURIOrBase64(workspaceMember.avatarUrl)}
|
||||||
entityId={workspaceMember.id}
|
placeholderColorSeed={workspaceMember.id}
|
||||||
placeholder={workspaceMember.name.firstName || ''}
|
placeholder={workspaceMember.name.firstName || ''}
|
||||||
type="squared"
|
type="squared"
|
||||||
size="xl"
|
size="xl"
|
||||||
@ -53,7 +53,6 @@ export const WorkspaceMemberCard = ({
|
|||||||
/>
|
/>
|
||||||
<StyledEmailText>{workspaceMember.userEmail}</StyledEmailText>
|
<StyledEmailText>{workspaceMember.userEmail}</StyledEmailText>
|
||||||
</StyledContent>
|
</StyledContent>
|
||||||
|
|
||||||
{accessory}
|
{accessory}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
||||||
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
||||||
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
||||||
|
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
|
||||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
import { PageBody } from '@/ui/layout/page/PageBody';
|
||||||
@ -35,16 +36,35 @@ export const RecordShowPage = () => {
|
|||||||
parameters.objectRecordId ?? '',
|
parameters.objectRecordId ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
viewName,
|
||||||
|
hasPreviousRecord,
|
||||||
|
hasNextRecord,
|
||||||
|
navigateToPreviousRecord,
|
||||||
|
navigateToNextRecord,
|
||||||
|
navigateToIndexView,
|
||||||
|
isLoadingPagination,
|
||||||
|
} = useRecordShowPagePagination(
|
||||||
|
parameters.objectNameSingular ?? '',
|
||||||
|
parameters.objectRecordId ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordFieldValueSelectorContextProvider>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
<RecordValueSetterEffect recordId={objectRecordId} />
|
<RecordValueSetterEffect recordId={objectRecordId} />
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageTitle title={pageTitle} />
|
<PageTitle title={pageTitle} />
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={pageName ?? ''}
|
title={viewName}
|
||||||
hasBackButton
|
hasPaginationButtons
|
||||||
|
hasClosePageButton
|
||||||
|
onClosePage={navigateToIndexView}
|
||||||
|
hasPreviousRecord={hasPreviousRecord}
|
||||||
|
navigateToPreviousRecord={navigateToPreviousRecord}
|
||||||
|
hasNextRecord={hasNextRecord}
|
||||||
|
navigateToNextRecord={navigateToNextRecord}
|
||||||
Icon={headerIcon}
|
Icon={headerIcon}
|
||||||
loading={loading}
|
loading={loading || isLoadingPagination}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<PageFavoriteButton
|
<PageFavoriteButton
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
PageDecoratorArgs,
|
PageDecoratorArgs,
|
||||||
} from '~/testing/decorators/PageDecorator';
|
} from '~/testing/decorators/PageDecorator';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { getPeopleMock } from '~/testing/mock-data/people';
|
import { getPeopleMock, peopleQueryResult } from '~/testing/mock-data/people';
|
||||||
import { mockedWorkspaceMemberData } from '~/testing/mock-data/users';
|
import { mockedWorkspaceMemberData } from '~/testing/mock-data/users';
|
||||||
|
|
||||||
import { RecordShowPage } from '../RecordShowPage';
|
import { RecordShowPage } from '../RecordShowPage';
|
||||||
@ -22,12 +22,17 @@ const meta: Meta<PageDecoratorArgs> = {
|
|||||||
routePath: '/object/:objectNameSingular/:objectRecordId',
|
routePath: '/object/:objectNameSingular/:objectRecordId',
|
||||||
routeParams: {
|
routeParams: {
|
||||||
':objectNameSingular': 'person',
|
':objectNameSingular': 'person',
|
||||||
':objectRecordId': '1234',
|
':objectRecordId': peopleMock[0].id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: [
|
||||||
|
graphql.query('FindManyPeople', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: peopleQueryResult,
|
||||||
|
});
|
||||||
|
}),
|
||||||
graphql.query('FindOnePerson', () => {
|
graphql.query('FindOnePerson', () => {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
data: {
|
data: {
|
||||||
@ -64,8 +69,8 @@ const meta: Meta<PageDecoratorArgs> = {
|
|||||||
edges: [],
|
edges: [],
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
startCursor: '1234',
|
startCursor: peopleMock[0].id,
|
||||||
endCursor: '1234',
|
endCursor: peopleMock[0].id,
|
||||||
},
|
},
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { Decorator } from '@storybook/react';
|
import { Decorator } from '@storybook/react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||||
import { getRecordChipGeneratorPerObjectPerField } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
import { getRecordChipGenerators } from '@/object-record/utils/getRecordChipGenerators';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems';
|
||||||
|
|
||||||
export const ChipGeneratorsDecorator: Decorator = (Story) => {
|
export const ChipGeneratorsDecorator: Decorator = (Story) => {
|
||||||
const chipGeneratorPerObjectPerField = useMemo(() => {
|
const { chipGeneratorPerObjectPerField, identifierChipGeneratorPerObject } =
|
||||||
return getRecordChipGeneratorPerObjectPerField(
|
useMemo(() => {
|
||||||
generatedMockObjectMetadataItems,
|
return getRecordChipGenerators(generatedMockObjectMetadataItems);
|
||||||
);
|
}, []);
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreComputedChipGeneratorsContext.Provider
|
<PreComputedChipGeneratorsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
chipGeneratorPerObjectPerField,
|
chipGeneratorPerObjectPerField,
|
||||||
|
identifierChipGeneratorPerObject,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Story />
|
<Story />
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
|
||||||
import { Decorator } from '@storybook/react';
|
import { Decorator } from '@storybook/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
||||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { getRecordChipGeneratorPerObjectPerField } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
|
||||||
import { mockedUserData } from '~/testing/mock-data/users';
|
import { mockedUserData } from '~/testing/mock-data/users';
|
||||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||||
|
|
||||||
@ -23,20 +22,12 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => {
|
|||||||
setCurrentUser(mockedUserData);
|
setCurrentUser(mockedUserData);
|
||||||
}, [setCurrentUser, setCurrentWorkspaceMember]);
|
}, [setCurrentUser, setCurrentWorkspaceMember]);
|
||||||
|
|
||||||
const chipGeneratorPerObjectPerField = useMemo(() => {
|
|
||||||
return getRecordChipGeneratorPerObjectPerField(objectMetadataItems);
|
|
||||||
}, [objectMetadataItems]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ObjectMetadataItemsLoadEffect />
|
<ObjectMetadataItemsLoadEffect />
|
||||||
<PreComputedChipGeneratorsContext.Provider
|
<PreComputedChipGeneratorsProvider>
|
||||||
value={{
|
|
||||||
chipGeneratorPerObjectPerField,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!!objectMetadataItems.length && <Story />}
|
{!!objectMetadataItems.length && <Story />}
|
||||||
</PreComputedChipGeneratorsContext.Provider>
|
</PreComputedChipGeneratorsProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
export const createApolloStoreFieldName = ({
|
||||||
|
fieldName,
|
||||||
|
fieldVariables,
|
||||||
|
}: {
|
||||||
|
fieldName: string;
|
||||||
|
fieldVariables: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
return `${fieldName}(${JSON.stringify(fieldVariables)})`;
|
||||||
|
};
|
||||||
12
packages/twenty-front/src/utils/createEventContext.ts
Normal file
12
packages/twenty-front/src/utils/createEventContext.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Context, createContext } from 'react';
|
||||||
|
|
||||||
|
type ObjectOfFunctions = {
|
||||||
|
[key: string]: (...args: any[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EventContext<T extends ObjectOfFunctions> =
|
||||||
|
T extends ObjectOfFunctions ? T : never;
|
||||||
|
|
||||||
|
export const createEventContext = <T extends ObjectOfFunctions>(): Context<
|
||||||
|
EventContext<T>
|
||||||
|
> => createContext<EventContext<T>>({} as EventContext<T>);
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
|
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
|
import { invalidAvatarUrlsState } from '@ui/display/avatar/components/states/isInvalidAvatarUrlState';
|
||||||
@ -50,7 +50,7 @@ export type AvatarProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
size?: AvatarSize;
|
size?: AvatarSize;
|
||||||
placeholder: string | undefined;
|
placeholder: string | undefined;
|
||||||
entityId?: string;
|
placeholderColorSeed?: string;
|
||||||
type?: Nullable<AvatarType>;
|
type?: Nullable<AvatarType>;
|
||||||
color?: string;
|
color?: string;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
@ -62,7 +62,7 @@ export const Avatar = ({
|
|||||||
avatarUrl,
|
avatarUrl,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
placeholder,
|
placeholder,
|
||||||
entityId = placeholder,
|
placeholderColorSeed = placeholder,
|
||||||
onClick,
|
onClick,
|
||||||
type = 'squared',
|
type = 'squared',
|
||||||
color,
|
color,
|
||||||
@ -85,9 +85,10 @@ export const Avatar = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fixedColor = color ?? stringToHslColor(entityId ?? '', 75, 25);
|
const fixedColor =
|
||||||
|
color ?? stringToHslColor(placeholderColorSeed ?? '', 75, 25);
|
||||||
const fixedBackgroundColor =
|
const fixedBackgroundColor =
|
||||||
backgroundColor ?? stringToHslColor(entityId ?? '', 75, 85);
|
backgroundColor ?? stringToHslColor(placeholderColorSeed ?? '', 75, 85);
|
||||||
|
|
||||||
const showBackgroundColor = showPlaceholder;
|
const showBackgroundColor = showPlaceholder;
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { AvatarGroup, AvatarGroupProps } from '../AvatarGroup';
|
|||||||
|
|
||||||
const makeAvatar = (userName: string, props: Partial<AvatarProps> = {}) => (
|
const makeAvatar = (userName: string, props: Partial<AvatarProps> = {}) => (
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<Avatar placeholder={userName} entityId={userName} {...props} />
|
<Avatar placeholder={userName} placeholderColorSeed={userName} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const getAvatars = (commonProps: Partial<AvatarProps> = {}) => [
|
const getAvatars = (commonProps: Partial<AvatarProps> = {}) => [
|
||||||
|
|||||||
@ -1,56 +1,47 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
|
|
||||||
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
||||||
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
||||||
import { Chip, ChipVariant } from '@ui/display/chip/components/Chip';
|
import { Chip, ChipVariant } from '@ui/display/chip/components/Chip';
|
||||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||||
|
import { isDefined } from '@ui/utilities/isDefined';
|
||||||
import { Nullable } from '@ui/utilities/types/Nullable';
|
import { Nullable } from '@ui/utilities/types/Nullable';
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
export type EntityChipProps = {
|
export type AvatarChipProps = {
|
||||||
linkToEntity?: string;
|
|
||||||
entityId: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
avatarType?: Nullable<AvatarType>;
|
avatarType?: Nullable<AvatarType>;
|
||||||
variant?: EntityChipVariant;
|
variant?: AvatarChipVariant;
|
||||||
LeftIcon?: IconComponent;
|
LeftIcon?: IconComponent;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
placeholderColorSeed?: string;
|
||||||
|
onClick?: (event: MouseEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum EntityChipVariant {
|
export enum AvatarChipVariant {
|
||||||
Regular = 'regular',
|
Regular = 'regular',
|
||||||
Transparent = 'transparent',
|
Transparent = 'transparent',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EntityChip = ({
|
export const AvatarChip = ({
|
||||||
linkToEntity,
|
|
||||||
entityId,
|
|
||||||
name,
|
name,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
avatarType = 'rounded',
|
avatarType = 'rounded',
|
||||||
variant = EntityChipVariant.Regular,
|
variant = AvatarChipVariant.Regular,
|
||||||
LeftIcon,
|
LeftIcon,
|
||||||
className,
|
className,
|
||||||
}: EntityChipProps) => {
|
placeholderColorSeed,
|
||||||
const navigate = useNavigate();
|
onClick,
|
||||||
|
}: AvatarChipProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const handleLinkClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
if (isNonEmptyString(linkToEntity)) {
|
|
||||||
event.stopPropagation();
|
|
||||||
navigate(linkToEntity);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
label={name}
|
label={name}
|
||||||
variant={
|
variant={
|
||||||
linkToEntity
|
isDefined(onClick)
|
||||||
? variant === EntityChipVariant.Regular
|
? variant === AvatarChipVariant.Regular
|
||||||
? ChipVariant.Highlighted
|
? ChipVariant.Highlighted
|
||||||
: ChipVariant.Regular
|
: ChipVariant.Regular
|
||||||
: ChipVariant.Transparent
|
: ChipVariant.Transparent
|
||||||
@ -61,15 +52,15 @@ export const EntityChip = ({
|
|||||||
) : (
|
) : (
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
entityId={entityId}
|
placeholderColorSeed={placeholderColorSeed}
|
||||||
placeholder={name}
|
placeholder={name}
|
||||||
size="sm"
|
size="sm"
|
||||||
type={avatarType}
|
type={avatarType}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
clickable={!!linkToEntity}
|
clickable={isDefined(onClick)}
|
||||||
onClick={handleLinkClick}
|
onClick={onClick}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -1,21 +1,19 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { AvatarChip } from '@ui/display/chip/components/AvatarChip';
|
||||||
|
|
||||||
import { ComponentDecorator, RouterDecorator } from '@ui/testing';
|
import { ComponentDecorator, RouterDecorator } from '@ui/testing';
|
||||||
|
|
||||||
import { EntityChip } from '../EntityChip';
|
const meta: Meta<typeof AvatarChip> = {
|
||||||
|
title: 'UI/Display/Chip/AvatarChip',
|
||||||
const meta: Meta<typeof EntityChip> = {
|
component: AvatarChip,
|
||||||
title: 'UI/Display/Chip/EntityChip',
|
|
||||||
component: EntityChip,
|
|
||||||
decorators: [RouterDecorator, ComponentDecorator],
|
decorators: [RouterDecorator, ComponentDecorator],
|
||||||
args: {
|
args: {
|
||||||
name: 'Entity name',
|
name: 'Entity name',
|
||||||
linkToEntity: '/entity-link',
|
|
||||||
avatarType: 'squared',
|
avatarType: 'squared',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof EntityChip>;
|
type Story = StoryObj<typeof AvatarChip>;
|
||||||
|
|
||||||
export const Default: Story = {};
|
export const Default: Story = {};
|
||||||
|
|||||||
@ -6,8 +6,8 @@ export * from './avatar/types/AvatarSize';
|
|||||||
export * from './avatar/types/AvatarType';
|
export * from './avatar/types/AvatarType';
|
||||||
export * from './checkmark/components/AnimatedCheckmark';
|
export * from './checkmark/components/AnimatedCheckmark';
|
||||||
export * from './checkmark/components/Checkmark';
|
export * from './checkmark/components/Checkmark';
|
||||||
|
export * from './chip/components/AvatarChip';
|
||||||
export * from './chip/components/Chip';
|
export * from './chip/components/Chip';
|
||||||
export * from './chip/components/EntityChip';
|
|
||||||
export * from './color/components/ColorSample';
|
export * from './color/components/ColorSample';
|
||||||
export * from './icon/components/IconAddressBook';
|
export * from './icon/components/IconAddressBook';
|
||||||
export * from './icon/components/IconGmail';
|
export * from './icon/components/IconGmail';
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './color/utils/stringToHslColor';
|
export * from './color/utils/stringToHslColor';
|
||||||
|
export * from './isDefined';
|
||||||
export * from './state/utils/createState';
|
export * from './state/utils/createState';
|
||||||
export * from './types/Nullable';
|
export * from './types/Nullable';
|
||||||
|
|||||||
4
packages/twenty-ui/src/utilities/isDefined.ts
Normal file
4
packages/twenty-ui/src/utilities/isDefined.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { isNull, isUndefined } from '@sniptt/guards';
|
||||||
|
|
||||||
|
export const isDefined = <T>(value: T | null | undefined): value is T =>
|
||||||
|
!isUndefined(value) && !isNull(value);
|
||||||
Reference in New Issue
Block a user