mirror of
https://github.com/lingble/twenty.git
synced 2025-11-21 16:34:55 +00:00
Remove some dead code (#6611)
We could remove a lot more than this, this is just a start. There are various tools to help with this, knip is a good one
This commit is contained in:
@@ -1,28 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const ActivityBodyEffect = ({ activityId }: { activityId: string }) => {
|
|
||||||
const [activityFromStore] = useRecoilState(
|
|
||||||
recordStoreFamilyState(activityId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [activityBody, setActivityBody] = useRecoilState(
|
|
||||||
activityBodyFamilyState({ activityId }),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
activityBody === '' &&
|
|
||||||
isDefined(activityFromStore) &&
|
|
||||||
activityBody !== activityFromStore.body
|
|
||||||
) {
|
|
||||||
setActivityBody(activityFromStore.body);
|
|
||||||
}
|
|
||||||
}, [activityFromStore, activityBody, setActivityBody]);
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
|
||||||
|
|
||||||
export const activityTitleFamilyState = createFamilyState<
|
|
||||||
string,
|
|
||||||
{ activityId: string }
|
|
||||||
>({
|
|
||||||
key: 'activityTitleFamilyState',
|
|
||||||
defaultValue: '',
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
|
||||||
|
|
||||||
export const targetableObjectsInDrawerState = createState<
|
|
||||||
ActivityTargetableObject[]
|
|
||||||
>({
|
|
||||||
key: 'targetableObjectsInDrawerState',
|
|
||||||
defaultValue: [],
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { CommentChip, CommentChipProps } from './CommentChip';
|
|
||||||
|
|
||||||
type CellCommentChipProps = CommentChipProps;
|
|
||||||
|
|
||||||
// TODO: tie those fixed values to the other components in the cell
|
|
||||||
const StyledCellWrapper = styled.div``;
|
|
||||||
|
|
||||||
export const CellCommentChip = ({ count, onClick }: CellCommentChipProps) => {
|
|
||||||
if (count === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledCellWrapper>
|
|
||||||
<CommentChip count={count} onClick={onClick} />
|
|
||||||
</StyledCellWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { IconComment } from 'twenty-ui';
|
|
||||||
|
|
||||||
export type CommentChipProps = {
|
|
||||||
count: number;
|
|
||||||
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledChip = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
backdrop-filter: ${({ theme }) => theme.blur.medium};
|
|
||||||
|
|
||||||
background: ${({ theme }) => theme.background.transparent.primary};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
|
||||||
|
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
height: 26px;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
max-width: 42px;
|
|
||||||
|
|
||||||
padding-left: 4px;
|
|
||||||
|
|
||||||
padding-right: 4px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: ${({ theme }) => theme.background.tertiary};
|
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
|
||||||
}
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledCount = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
font-size: ${({ theme }) => theme.font.size.sm};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const CommentChip = ({ count, onClick }: CommentChipProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
if (count === 0) return null;
|
|
||||||
const formattedCount = count > 99 ? '99+' : count;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledChip data-testid="comment-chip" onClick={onClick}>
|
|
||||||
<StyledCount>{formattedCount}</StyledCount>
|
|
||||||
<IconComment size={theme.icon.size.md} />
|
|
||||||
</StyledChip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { ComponentDecorator } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { CommentChip } from '../CommentChip';
|
|
||||||
|
|
||||||
const meta: Meta<typeof CommentChip> = {
|
|
||||||
title: 'Modules/Comments/CommentChip',
|
|
||||||
component: CommentChip,
|
|
||||||
decorators: [ComponentDecorator],
|
|
||||||
args: { count: 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof CommentChip>;
|
|
||||||
|
|
||||||
const StyledTestCellContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
background: ${({ theme }) => theme.background.primary};
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
height: fit-content;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
max-width: 250px;
|
|
||||||
|
|
||||||
min-width: 250px;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledFakeCellText = styled.div`
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const OneComment: Story = {};
|
|
||||||
|
|
||||||
export const TenComments: Story = {
|
|
||||||
args: { count: 10 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TooManyComments: Story = {
|
|
||||||
args: { count: 1000 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InCellDefault: Story = {
|
|
||||||
args: { count: 12 },
|
|
||||||
decorators: [
|
|
||||||
(Story) => (
|
|
||||||
<StyledTestCellContainer>
|
|
||||||
<StyledFakeCellText>Fake short text</StyledFakeCellText>
|
|
||||||
<Story />
|
|
||||||
</StyledTestCellContainer>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InCellOverlappingBlur: Story = {
|
|
||||||
...InCellDefault,
|
|
||||||
decorators: [
|
|
||||||
(Story) => (
|
|
||||||
<StyledTestCellContainer>
|
|
||||||
<StyledFakeCellText>
|
|
||||||
Fake long text to demonstrate ellipsis
|
|
||||||
</StyledFakeCellText>
|
|
||||||
<Story />
|
|
||||||
</StyledTestCellContainer>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { atom } from 'recoil';
|
|
||||||
|
|
||||||
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
|
||||||
|
|
||||||
export const currentCompletedTaskQueryVariablesState =
|
|
||||||
atom<RecordGqlOperationVariables | null>({
|
|
||||||
default: null,
|
|
||||||
key: 'currentCompletedTaskQueryVariablesState',
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { atom } from 'recoil';
|
|
||||||
|
|
||||||
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
|
||||||
|
|
||||||
export const currentIncompleteTaskQueryVariablesState =
|
|
||||||
atom<RecordGqlOperationVariables | null>({
|
|
||||||
default: null,
|
|
||||||
key: 'currentIncompleteTaskQueryVariablesState',
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const currentUserDueTaskCountState = createState<number>({
|
|
||||||
defaultValue: 0,
|
|
||||||
key: 'currentUserDueTaskCountState',
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useActivities } from '@/activities/hooks/useActivities';
|
|
||||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy';
|
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const TimelineActivitiesQueryEffect = ({
|
|
||||||
targetableObject,
|
|
||||||
}: {
|
|
||||||
targetableObject: ActivityTargetableObject;
|
|
||||||
}) => {
|
|
||||||
useActivities({
|
|
||||||
objectNameSingular:
|
|
||||||
targetableObject.targetObjectNameSingular as CoreObjectNameSingular,
|
|
||||||
targetableObjects: [targetableObject],
|
|
||||||
activitiesFilters: {},
|
|
||||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
|
||||||
skip: !isDefined(targetableObject),
|
|
||||||
});
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
|
||||||
|
|
||||||
export type Comment = {
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
body: string;
|
|
||||||
updatedAt: string;
|
|
||||||
activityId: string;
|
|
||||||
author: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'>;
|
|
||||||
__typename: 'Comment';
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { expect } from '@storybook/test';
|
|
||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
|
||||||
|
|
||||||
import { useTrackEvent } from '../useTrackEvent';
|
|
||||||
|
|
||||||
const mockTrackMutation = jest.fn();
|
|
||||||
|
|
||||||
jest.mock('~/generated/graphql', () => ({
|
|
||||||
useTrackMutation: () => [mockTrackMutation],
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('useTrackEvent', () => {
|
|
||||||
it('should call useEventTracker with the correct arguments', async () => {
|
|
||||||
const eventType = 'exampleType';
|
|
||||||
const eventData = { location: { pathname: '/examplePath' } };
|
|
||||||
renderHook(() => useTrackEvent(eventType, eventData), {
|
|
||||||
wrapper: RecoilRoot,
|
|
||||||
});
|
|
||||||
expect(mockTrackMutation).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockTrackMutation).toHaveBeenCalledWith({
|
|
||||||
variables: { type: eventType, data: eventData },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { EventData, useEventTracker } from './useEventTracker';
|
|
||||||
|
|
||||||
export const useTrackEvent = (eventType: string, eventData: EventData) => {
|
|
||||||
const eventTracker = useEventTracker();
|
|
||||||
|
|
||||||
return eventTracker(eventType, eventData);
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { Favorite } from '@/favorites/types/Favorite';
|
|
||||||
|
|
||||||
export const favoritesState = createState<Favorite[]>({
|
|
||||||
key: 'favoritesState',
|
|
||||||
defaultValue: [],
|
|
||||||
});
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { mapFavorites } from '../mapFavorites';
|
|
||||||
|
|
||||||
describe('mapFavorites', () => {
|
|
||||||
it('should return the correct value', () => {
|
|
||||||
const favorites = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
person: {
|
|
||||||
id: '2',
|
|
||||||
name: {
|
|
||||||
firstName: 'John',
|
|
||||||
lastName: 'Doe',
|
|
||||||
},
|
|
||||||
avatarUrl: 'https://example.com/avatar.png',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
company: {
|
|
||||||
id: '4',
|
|
||||||
name: 'My Company',
|
|
||||||
domainName: 'example.com',
|
|
||||||
},
|
|
||||||
position: 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const res = mapFavorites(favorites);
|
|
||||||
|
|
||||||
expect(res).toHaveLength(2);
|
|
||||||
|
|
||||||
// Person
|
|
||||||
expect(res[0].id).toBe('1');
|
|
||||||
expect(res[0].labelIdentifier).toBe('John Doe');
|
|
||||||
expect(res[0].avatarUrl).toBe('https://example.com/avatar.png');
|
|
||||||
expect(res[0].avatarType).toBe('rounded');
|
|
||||||
expect(res[0].link).toBe('/object/person/2');
|
|
||||||
expect(res[0].recordId).toBe('2');
|
|
||||||
expect(res[0].position).toBeUndefined();
|
|
||||||
|
|
||||||
// Company
|
|
||||||
expect(res[1].id).toBe('3');
|
|
||||||
expect(res[1].labelIdentifier).toBe('My Company');
|
|
||||||
expect(res[1].avatarUrl).toBe('https://favicon.twenty.com/example.com');
|
|
||||||
expect(res[1].avatarType).toBe('squared');
|
|
||||||
expect(res[1].link).toBe('/object/company/4');
|
|
||||||
expect(res[1].recordId).toBe('4');
|
|
||||||
expect(res[1].position).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
|
|
||||||
import { getLogoUrlFromDomainName } from '~/utils';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const mapFavorites = (favorites: any) => {
|
|
||||||
return favorites
|
|
||||||
.map((favorite: any) => {
|
|
||||||
const recordInformation = isDefined(favorite?.person)
|
|
||||||
? {
|
|
||||||
id: favorite.person.id,
|
|
||||||
labelIdentifier:
|
|
||||||
favorite.person.name.firstName +
|
|
||||||
' ' +
|
|
||||||
favorite.person.name.lastName,
|
|
||||||
avatarUrl: favorite.person.avatarUrl,
|
|
||||||
avatarType: 'rounded',
|
|
||||||
link: `/object/person/${favorite.person.id}`,
|
|
||||||
}
|
|
||||||
: isDefined(favorite?.company)
|
|
||||||
? {
|
|
||||||
id: favorite.company.id,
|
|
||||||
labelIdentifier: favorite.company.name,
|
|
||||||
avatarUrl: getLogoUrlFromDomainName(
|
|
||||||
getCompanyDomainName(favorite.company),
|
|
||||||
),
|
|
||||||
avatarType: 'squared',
|
|
||||||
link: `/object/company/${favorite.company.id}`,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...recordInformation,
|
|
||||||
recordId: recordInformation?.id,
|
|
||||||
id: favorite?.id,
|
|
||||||
position: favorite?.position,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(isDefined)
|
|
||||||
.sort((a: any, b: any) => a.position - b.position);
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { formatInTimeZone } from 'date-fns-tz';
|
|
||||||
import { parseDate } from '~/utils/date-utils';
|
|
||||||
|
|
||||||
export const formatDatetime = (
|
|
||||||
date: Date | string,
|
|
||||||
timeZone: string,
|
|
||||||
dateFormat: string,
|
|
||||||
timeFormat: string,
|
|
||||||
) => {
|
|
||||||
return formatInTimeZone(
|
|
||||||
parseDate(date).toJSDate(),
|
|
||||||
timeZone,
|
|
||||||
`${dateFormat} ${timeFormat}`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { useIsTasksPage } from '../useIsTasksPage';
|
|
||||||
|
|
||||||
const getWrapper =
|
|
||||||
(initialIndex: 0 | 1) =>
|
|
||||||
({ children }: { children: React.ReactNode }) => (
|
|
||||||
<MemoryRouter
|
|
||||||
initialEntries={['/settings/', '/tasks']}
|
|
||||||
initialIndex={initialIndex}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('useIsSettingsPage', () => {
|
|
||||||
it('should return true for pages which has /tasks in pathname', () => {
|
|
||||||
const { result } = renderHook(() => useIsTasksPage(), {
|
|
||||||
wrapper: getWrapper(1),
|
|
||||||
});
|
|
||||||
expect(result.current).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for other pages which does not have /tasks in pathname', () => {
|
|
||||||
const { result } = renderHook(() => useIsTasksPage(), {
|
|
||||||
wrapper: getWrapper(0),
|
|
||||||
});
|
|
||||||
expect(result.current).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const useIsTasksPage = () => useLocation().pathname === '/tasks';
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
|
||||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
|
||||||
|
|
||||||
import { useFilterOutUnexistingObjectMetadataItems } from '../useFilterOutUnexistingObjectMetadataItems';
|
|
||||||
|
|
||||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
|
||||||
|
|
||||||
describe('useFilterOutUnexistingObjectMetadataItems', () => {
|
|
||||||
it('should work as expected', async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => {
|
|
||||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
|
||||||
|
|
||||||
setMetadataItems(mockObjectMetadataItems.slice(1));
|
|
||||||
return useFilterOutUnexistingObjectMetadataItems();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
wrapper: RecoilRoot,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const objectExists = result.current.filterOutUnexistingObjectMetadataItems(
|
|
||||||
mockObjectMetadataItems[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(objectExists).toBe(false);
|
|
||||||
|
|
||||||
const secondObjectExists =
|
|
||||||
result.current.filterOutUnexistingObjectMetadataItems(
|
|
||||||
mockObjectMetadataItems[1],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(secondObjectExists).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { objectMetadataItemsByNameSingularMapSelector } from '@/object-metadata/states/objectMetadataItemsByNameSingularMapSelector';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const useFilterOutUnexistingObjectMetadataItems = () => {
|
|
||||||
const objectMetadataItemsByNameSingularMap = useRecoilValue(
|
|
||||||
objectMetadataItemsByNameSingularMapSelector,
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterOutUnexistingObjectMetadataItems = (
|
|
||||||
objectMetadatItem: ObjectMetadataItem,
|
|
||||||
) =>
|
|
||||||
isDefined(
|
|
||||||
objectMetadataItemsByNameSingularMap.get(objectMetadatItem.nameSingular),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
filterOutUnexistingObjectMetadataItems,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export type Position = number | 'first' | 'last';
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
import { RelationDirections } from '@/object-record/record-field/types/FieldDefinition';
|
|
||||||
import {
|
|
||||||
FieldMetadataType,
|
|
||||||
RelationDefinitionType,
|
|
||||||
} from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
export const getFieldRelationDirections = (
|
|
||||||
field: Pick<FieldMetadataItem, 'type' | 'relationDefinition'> | undefined,
|
|
||||||
): RelationDirections => {
|
|
||||||
if (!field || field.type !== FieldMetadataType.Relation) {
|
|
||||||
throw new Error(`Field is not a relation field.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (field.relationDefinition?.direction) {
|
|
||||||
case RelationDefinitionType.ManyToMany:
|
|
||||||
throw new Error(`Many to many relations are not supported.`);
|
|
||||||
case RelationDefinitionType.OneToMany:
|
|
||||||
return {
|
|
||||||
from: 'FROM_ONE_OBJECT',
|
|
||||||
to: 'TO_MANY_OBJECTS',
|
|
||||||
};
|
|
||||||
case RelationDefinitionType.ManyToOne:
|
|
||||||
return {
|
|
||||||
from: 'FROM_MANY_OBJECTS',
|
|
||||||
to: 'TO_ONE_OBJECT',
|
|
||||||
};
|
|
||||||
case RelationDefinitionType.OneToOne:
|
|
||||||
return {
|
|
||||||
from: 'FROM_ONE_OBJECT',
|
|
||||||
to: 'TO_ONE_OBJECT',
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Invalid relation definition type direction : ${field.relationDefinition?.direction}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,7 @@ import { AddressInput } from '@/ui/field/input/components/AddressInput';
|
|||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
|
|
||||||
import { FieldInputEvent } from './DateFieldInput';
|
import { FieldInputEvent } from './DateTimeFieldInput';
|
||||||
|
|
||||||
export type AddressFieldInputProps = {
|
export type AddressFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputEvent;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useCurrencyField } from '../../hooks/useCurrencyField';
|
|||||||
|
|
||||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
import { FieldInputEvent } from './DateTimeFieldInput';
|
||||||
|
|
||||||
export type CurrencyFieldInputProps = {
|
type CurrencyFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
|
|
||||||
export type FieldInputEvent = (persist: () => void) => void;
|
type FieldInputEvent = (persist: () => void) => void;
|
||||||
|
|
||||||
export type DateFieldInputProps = {
|
type DateFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
|
|||||||
const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
|
const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
|
||||||
'Last name';
|
'Last name';
|
||||||
|
|
||||||
export type FullNameFieldInputProps = {
|
type FullNameFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useLinkField } from '../../hooks/useLinkField';
|
|||||||
|
|
||||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
import { FieldInputEvent } from './DateTimeFieldInput';
|
||||||
|
|
||||||
export type LinkFieldInputProps = {
|
type LinkFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useRef, useState } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { IconCheck, IconPlus } from 'twenty-ui';
|
import { IconCheck, IconPlus } from 'twenty-ui';
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ const StyledDropdownMenu = styled(DropdownMenu)`
|
|||||||
top: -1px;
|
top: -1px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type LinksFieldInputProps = {
|
type LinksFieldInputProps = {
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconBookmarkPlus,
|
IconBookmarkPlus,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const StyledRelationPickerContainer = styled.div`
|
|||||||
top: -1px;
|
top: -1px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type MultiSelectFieldInputProps = {
|
type MultiSelectFieldInputProps = {
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
|
|||||||
|
|
||||||
import { useJsonField } from '../../hooks/useJsonField';
|
import { useJsonField } from '../../hooks/useJsonField';
|
||||||
|
|
||||||
import { FieldInputEvent } from './DateFieldInput';
|
import { FieldInputEvent } from './DateTimeFieldInput';
|
||||||
|
|
||||||
export type RawJsonFieldInputProps = {
|
type RawJsonFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { MultiRecordSelect } from '@/object-record/relation-picker/components/Mu
|
|||||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
|
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||||
|
|
||||||
export type RelationFromManyFieldInputProps = {
|
type RelationFromManyFieldInputProps = {
|
||||||
onSubmit?: FieldInputEvent;
|
onSubmit?: FieldInputEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const StyledRelationPickerContainer = styled.div`
|
|||||||
top: -1px;
|
top: -1px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type SelectFieldInputProps = {
|
type SelectFieldInputProps = {
|
||||||
onSubmit?: FieldInputEvent;
|
onSubmit?: FieldInputEvent;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const recordPositionInternalState = createState<number | null>({
|
|
||||||
key: 'recordPositionInternalState',
|
|
||||||
defaultValue: null,
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { FieldDefinition } from './FieldDefinition';
|
|
||||||
import { FieldMetadata } from './FieldMetadata';
|
|
||||||
|
|
||||||
export type FieldDefinitionSerializable = Omit<
|
|
||||||
FieldDefinition<FieldMetadata>,
|
|
||||||
'Icon'
|
|
||||||
>;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const CREATE_BUTTON_ID = 'create-button';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const EMPTY_BUTTON_ID = 'empty-button';
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export const getPreselectedIdIndex = (
|
|
||||||
selectableOptionIds: string[],
|
|
||||||
preselectedOptionId: string,
|
|
||||||
) => {
|
|
||||||
const preselectedIdIndex = selectableOptionIds.findIndex(
|
|
||||||
(option) => option === preselectedOptionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return preselectedIdIndex === -1 ? 0 : preselectedIdIndex;
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
|
|
||||||
export const mapToRecordId = (objectRecord: ObjectRecord) => {
|
|
||||||
return objectRecord.id;
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
|
|
||||||
export const sortByObjectRecordId = (a: ObjectRecord, b: ObjectRecord) => {
|
|
||||||
return a.id.localeCompare(b.id);
|
|
||||||
};
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
|
|
||||||
import { sortObjectRecordByDateField } from './sortObjectRecordByDateField';
|
|
||||||
|
|
||||||
describe('sortByObjectRecordByCreatedAt', () => {
|
|
||||||
const recordOldest = {
|
|
||||||
id: '',
|
|
||||||
createdAt: '2022-01-01T00:00:00.000Z',
|
|
||||||
__typename: 'RecordType',
|
|
||||||
};
|
|
||||||
const recordNewest = {
|
|
||||||
id: '',
|
|
||||||
createdAt: '2022-01-02T00:00:00.000Z',
|
|
||||||
__typename: 'RecordType',
|
|
||||||
};
|
|
||||||
const recordNull1 = { id: '', createdAt: null, __typename: 'RecordType' };
|
|
||||||
const recordNull2 = { id: '', createdAt: null, __typename: 'RecordType' };
|
|
||||||
|
|
||||||
it('should sort in ascending order with null values first', () => {
|
|
||||||
const sortDirection = 'AscNullsFirst' satisfies OrderBy;
|
|
||||||
const sortedArray = [
|
|
||||||
recordNull2,
|
|
||||||
recordNewest,
|
|
||||||
recordNull1,
|
|
||||||
recordOldest,
|
|
||||||
].sort(sortObjectRecordByDateField('createdAt', sortDirection));
|
|
||||||
|
|
||||||
expect(sortedArray).toEqual([
|
|
||||||
recordNull1,
|
|
||||||
recordNull2,
|
|
||||||
recordOldest,
|
|
||||||
recordNewest,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sort in descending order with null values first', () => {
|
|
||||||
const sortDirection = 'DescNullsFirst' satisfies OrderBy;
|
|
||||||
const sortedArray = [
|
|
||||||
recordNull2,
|
|
||||||
recordOldest,
|
|
||||||
recordNewest,
|
|
||||||
recordNull1,
|
|
||||||
].sort(sortObjectRecordByDateField('createdAt', sortDirection));
|
|
||||||
|
|
||||||
expect(sortedArray).toEqual([
|
|
||||||
recordNull2,
|
|
||||||
recordNull1,
|
|
||||||
recordNewest,
|
|
||||||
recordOldest,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
it('should sort in ascending order with null values last', () => {
|
|
||||||
const sortDirection = 'AscNullsLast' satisfies OrderBy;
|
|
||||||
const sortedArray = [
|
|
||||||
recordOldest,
|
|
||||||
recordNull2,
|
|
||||||
recordNewest,
|
|
||||||
recordNull1,
|
|
||||||
].sort(sortObjectRecordByDateField('createdAt', sortDirection));
|
|
||||||
|
|
||||||
expect(sortedArray).toEqual([
|
|
||||||
recordOldest,
|
|
||||||
recordNewest,
|
|
||||||
recordNull1,
|
|
||||||
recordNull2,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sort in descending order with null values last', () => {
|
|
||||||
const sortDirection = 'DescNullsLast' satisfies OrderBy;
|
|
||||||
const sortedArray = [
|
|
||||||
recordNull1,
|
|
||||||
recordOldest,
|
|
||||||
recordNewest,
|
|
||||||
recordNull2,
|
|
||||||
].sort(sortObjectRecordByDateField('createdAt', sortDirection));
|
|
||||||
|
|
||||||
expect(sortedArray).toEqual([
|
|
||||||
recordNewest,
|
|
||||||
recordOldest,
|
|
||||||
recordNull1,
|
|
||||||
recordNull2,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
const SORT_BEFORE = -1;
|
|
||||||
const SORT_AFTER = 1;
|
|
||||||
const SORT_EQUAL = 0;
|
|
||||||
|
|
||||||
export const sortObjectRecordByDateField =
|
|
||||||
<T extends ObjectRecord>(dateField: keyof T, sortDirection: OrderBy) =>
|
|
||||||
(a: T, b: T) => {
|
|
||||||
const aDate = a[dateField];
|
|
||||||
const bDate = b[dateField];
|
|
||||||
|
|
||||||
if (!isDefined(aDate) && !isDefined(bDate)) {
|
|
||||||
return SORT_EQUAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDefined(aDate)) {
|
|
||||||
if (sortDirection === 'AscNullsFirst') {
|
|
||||||
return SORT_BEFORE;
|
|
||||||
} else if (sortDirection === 'DescNullsFirst') {
|
|
||||||
return SORT_BEFORE;
|
|
||||||
} else if (sortDirection === 'AscNullsLast') {
|
|
||||||
return SORT_AFTER;
|
|
||||||
} else if (sortDirection === 'DescNullsLast') {
|
|
||||||
return SORT_AFTER;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Invalid sortDirection: ${sortDirection}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDefined(bDate)) {
|
|
||||||
if (sortDirection === 'AscNullsFirst') {
|
|
||||||
return SORT_AFTER;
|
|
||||||
} else if (sortDirection === 'DescNullsFirst') {
|
|
||||||
return SORT_AFTER;
|
|
||||||
} else if (sortDirection === 'AscNullsLast') {
|
|
||||||
return SORT_BEFORE;
|
|
||||||
} else if (sortDirection === 'DescNullsLast') {
|
|
||||||
return SORT_BEFORE;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Invalid sortDirection: ${sortDirection}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const differenceInMs = DateTime.fromISO(aDate)
|
|
||||||
.diff(DateTime.fromISO(bDate))
|
|
||||||
.as('milliseconds');
|
|
||||||
|
|
||||||
if (differenceInMs === 0) {
|
|
||||||
return SORT_EQUAL;
|
|
||||||
} else if (
|
|
||||||
sortDirection === 'AscNullsFirst' ||
|
|
||||||
sortDirection === 'AscNullsLast'
|
|
||||||
) {
|
|
||||||
return differenceInMs > 0 ? SORT_AFTER : SORT_BEFORE;
|
|
||||||
} else if (
|
|
||||||
sortDirection === 'DescNullsFirst' ||
|
|
||||||
sortDirection === 'DescNullsLast'
|
|
||||||
) {
|
|
||||||
return differenceInMs > 0 ? SORT_BEFORE : SORT_AFTER;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Invalid sortDirection: ${sortDirection}`);
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|
||||||
|
|
||||||
export const useGetSyncStatusOptions = () => {
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
|
||||||
throw new Error('ObjectMetadataItem not found for MessageChannel');
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncStatusMetadata = objectMetadataItem.fields.find(
|
|
||||||
(field) => field.name === 'syncStatus',
|
|
||||||
);
|
|
||||||
|
|
||||||
const syncStatusOptions = syncStatusMetadata?.options;
|
|
||||||
|
|
||||||
if (isUndefinedOrNull(syncStatusMetadata)) {
|
|
||||||
throw new Error('syncStatusMetaData not found for MessageChannel');
|
|
||||||
}
|
|
||||||
|
|
||||||
return syncStatusOptions;
|
|
||||||
};
|
|
||||||
@@ -14,7 +14,7 @@ type StyledDropdownButtonProps = {
|
|||||||
isUnfolded: boolean;
|
isUnfolded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledDropdownButtonContainer = styled.div<StyledDropdownButtonProps>`
|
const StyledDropdownButtonContainer = styled.div<StyledDropdownButtonProps>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ color }) => color ?? 'none'};
|
color: ${({ color }) => color ?? 'none'};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { UserContext } from '@/users/contexts/UserContext';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
|
|
||||||
export const months = [
|
const months = [
|
||||||
{ label: 'January', value: 0 },
|
{ label: 'January', value: 0 },
|
||||||
{ label: 'February', value: 1 },
|
{ label: 'February', value: 1 },
|
||||||
{ label: 'March', value: 2 },
|
{ label: 'March', value: 2 },
|
||||||
@@ -36,7 +36,7 @@ export const months = [
|
|||||||
{ label: 'December', value: 11 },
|
{ label: 'December', value: 11 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const years = Array.from(
|
const years = Array.from(
|
||||||
{ length: 200 },
|
{ length: 200 },
|
||||||
(_, i) => new Date().getFullYear() + 5 - i,
|
(_, i) => new Date().getFullYear() + 5 - i,
|
||||||
).map((year) => ({ label: year.toString(), value: year }));
|
).map((year) => ({ label: year.toString(), value: year }));
|
||||||
@@ -299,7 +299,7 @@ const StyledCustomDatePickerHeader = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type InternalDatePickerProps = {
|
type InternalDatePickerProps = {
|
||||||
date: Date | null;
|
date: Date | null;
|
||||||
onMouseSelect?: (date: Date | null) => void;
|
onMouseSelect?: (date: Date | null) => void;
|
||||||
onChange?: (date: Date | null) => void;
|
onChange?: (date: Date | null) => void;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { IconChevronDown, IconWorld } from 'twenty-ui';
|
import { IconChevronDown, IconWorld } from 'twenty-ui';
|
||||||
|
|
||||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||||
@@ -19,7 +19,7 @@ type StyledDropdownButtonProps = {
|
|||||||
isUnfolded: boolean;
|
isUnfolded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledDropdownButtonContainer = styled.div<StyledDropdownButtonProps>`
|
const StyledDropdownButtonContainer = styled.div<StyledDropdownButtonProps>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: none;
|
background: none;
|
||||||
border-radius: ${({ theme }) => theme.border.radius.xs} 0 0
|
border-radius: ${({ theme }) => theme.border.radius.xs} 0 0
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { AvatarChip } from 'twenty-ui';
|
|
||||||
|
|
||||||
export type UserChipProps = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
avatarUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserChip = ({ id, name, avatarUrl }: UserChipProps) => (
|
|
||||||
<AvatarChip
|
|
||||||
placeholderColorSeed={id}
|
|
||||||
name={name}
|
|
||||||
avatarType="rounded"
|
|
||||||
avatarUrl={avatarUrl}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export enum ViewSortDirection {
|
|
||||||
Asc = 'asc',
|
|
||||||
Desc = 'desc',
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata
|
|||||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity';
|
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TypeOrmModule.forFeature([IndexFieldMetadataEntity], 'metadata')],
|
|
||||||
providers: [],
|
|
||||||
exports: [],
|
|
||||||
})
|
|
||||||
export class IndexFieldMetadataModule {}
|
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-field-metadata/index-field-metadata.entity';
|
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
|
||||||
@Entity('indexMetadata')
|
@Entity('indexMetadata')
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export const splitClassesAndStrings = <T>(
|
|
||||||
classesAndStrings: (string | T)[],
|
|
||||||
): [T[], string[]] => {
|
|
||||||
return [
|
|
||||||
classesAndStrings.filter((cls): cls is T => typeof cls !== 'string'),
|
|
||||||
classesAndStrings.filter((str): str is string => typeof str === 'string'),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
||||||
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
||||||
import { camelCase } from 'src/utils/camel-case';
|
|
||||||
|
|
||||||
// Compute composite field metadata by combining the composite field metadata with the field metadata
|
|
||||||
export const computeCompositeFieldMetadata = (
|
|
||||||
compositeFieldMetadata: FieldMetadataInterface,
|
|
||||||
fieldMetadata: FieldMetadataEntity,
|
|
||||||
): FieldMetadataEntity => ({
|
|
||||||
...fieldMetadata,
|
|
||||||
...compositeFieldMetadata,
|
|
||||||
objectMetadataId: fieldMetadata.objectMetadataId,
|
|
||||||
name: camelCase(`${fieldMetadata.name}-${compositeFieldMetadata.name}`),
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { WorkspaceHealthIssueType } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface';
|
|
||||||
|
|
||||||
export const isWorkspaceHealthNullableIssue = (
|
|
||||||
type: WorkspaceHealthIssueType,
|
|
||||||
): type is WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT => {
|
|
||||||
return type === WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isWorkspaceHealthTypeIssue = (
|
|
||||||
type: WorkspaceHealthIssueType,
|
|
||||||
): type is WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT => {
|
|
||||||
return type === WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isWorkspaceHealthDefaultValueIssue = (
|
|
||||||
type: WorkspaceHealthIssueType,
|
|
||||||
): type is WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT => {
|
|
||||||
return type === WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT;
|
|
||||||
};
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
|
||||||
import { InjectDataSource } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
|
||||||
import { StandardFieldFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory';
|
|
||||||
import { StandardObjectFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory';
|
|
||||||
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
|
||||||
import { computeStandardFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util';
|
|
||||||
|
|
||||||
interface RunCommandOptions {
|
|
||||||
workspaceId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'workspace:add-standard-id',
|
|
||||||
description: 'Add standard id to all metadata objects and fields',
|
|
||||||
})
|
|
||||||
export class AddStandardIdCommand extends CommandRunner {
|
|
||||||
private readonly logger = new Logger(AddStandardIdCommand.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectDataSource('metadata')
|
|
||||||
private readonly metadataDataSource: DataSource,
|
|
||||||
private readonly standardObjectFactory: StandardObjectFactory,
|
|
||||||
private readonly standardFieldFactory: StandardFieldFactory,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(_passedParam: string[], options: RunCommandOptions): Promise<void> {
|
|
||||||
const queryRunner = this.metadataDataSource.createQueryRunner();
|
|
||||||
const workspaceId = options.workspaceId;
|
|
||||||
|
|
||||||
await queryRunner.connect();
|
|
||||||
await queryRunner.startTransaction();
|
|
||||||
|
|
||||||
const manager = queryRunner.manager;
|
|
||||||
|
|
||||||
this.logger.log('Adding standardId to metadata objects and fields');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const standardObjectMetadataCollection =
|
|
||||||
this.standardObjectFactory.create(
|
|
||||||
standardObjectMetadataDefinitions,
|
|
||||||
{
|
|
||||||
// We don't need to provide the workspace id and data source id as we're only adding standardId
|
|
||||||
workspaceId: '',
|
|
||||||
dataSourceId: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IS_BLOCKLIST_ENABLED: true,
|
|
||||||
IS_EVENT_OBJECT_ENABLED: true,
|
|
||||||
IS_AIRTABLE_INTEGRATION_ENABLED: true,
|
|
||||||
IS_POSTGRESQL_INTEGRATION_ENABLED: true,
|
|
||||||
IS_STRIPE_INTEGRATION_ENABLED: false,
|
|
||||||
IS_COPILOT_ENABLED: false,
|
|
||||||
IS_MESSAGING_ALIAS_FETCHING_ENABLED: true,
|
|
||||||
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
|
|
||||||
IS_FREE_ACCESS_ENABLED: false,
|
|
||||||
IS_FUNCTION_SETTINGS_ENABLED: false,
|
|
||||||
IS_WORKFLOW_ENABLED: false,
|
|
||||||
IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const standardFieldMetadataCollection = this.standardFieldFactory.create(
|
|
||||||
CustomWorkspaceEntity,
|
|
||||||
{
|
|
||||||
workspaceId: '',
|
|
||||||
dataSourceId: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IS_BLOCKLIST_ENABLED: true,
|
|
||||||
IS_EVENT_OBJECT_ENABLED: true,
|
|
||||||
IS_AIRTABLE_INTEGRATION_ENABLED: true,
|
|
||||||
IS_POSTGRESQL_INTEGRATION_ENABLED: true,
|
|
||||||
IS_STRIPE_INTEGRATION_ENABLED: false,
|
|
||||||
IS_COPILOT_ENABLED: false,
|
|
||||||
IS_MESSAGING_ALIAS_FETCHING_ENABLED: true,
|
|
||||||
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
|
|
||||||
IS_FREE_ACCESS_ENABLED: false,
|
|
||||||
IS_FUNCTION_SETTINGS_ENABLED: false,
|
|
||||||
IS_WORKFLOW_ENABLED: false,
|
|
||||||
IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const objectMetadataRepository =
|
|
||||||
manager.getRepository(ObjectMetadataEntity);
|
|
||||||
const fieldMetadataRepository =
|
|
||||||
manager.getRepository(FieldMetadataEntity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update all object metadata with standard id
|
|
||||||
*/
|
|
||||||
const updateObjectMetadataCollection: Partial<ObjectMetadataEntity>[] =
|
|
||||||
[];
|
|
||||||
const updateFieldMetadataCollection: Partial<FieldMetadataEntity>[] = [];
|
|
||||||
const originalObjectMetadataCollection =
|
|
||||||
await objectMetadataRepository.find({
|
|
||||||
where: {
|
|
||||||
fields: { isCustom: false },
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
},
|
|
||||||
relations: ['fields'],
|
|
||||||
});
|
|
||||||
const customObjectMetadataCollection =
|
|
||||||
originalObjectMetadataCollection.filter(
|
|
||||||
(metadata) => metadata.isCustom,
|
|
||||||
);
|
|
||||||
|
|
||||||
const standardObjectMetadataMap = new Map(
|
|
||||||
standardObjectMetadataCollection.map((metadata) => [
|
|
||||||
metadata.nameSingular,
|
|
||||||
metadata,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const originalObjectMetadata of originalObjectMetadataCollection) {
|
|
||||||
const standardObjectMetadata = standardObjectMetadataMap.get(
|
|
||||||
originalObjectMetadata.nameSingular,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!standardObjectMetadata && !originalObjectMetadata.isCustom) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedStandardFieldMetadataCollection = computeStandardFields(
|
|
||||||
standardFieldMetadataCollection,
|
|
||||||
originalObjectMetadata,
|
|
||||||
customObjectMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!originalObjectMetadata.isCustom) {
|
|
||||||
updateObjectMetadataCollection.push({
|
|
||||||
id: originalObjectMetadata.id,
|
|
||||||
standardId: originalObjectMetadata.standardId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const fieldMetadata of originalObjectMetadata.fields) {
|
|
||||||
const standardFieldMetadata =
|
|
||||||
computedStandardFieldMetadataCollection.find(
|
|
||||||
(field) => field.name === fieldMetadata.name && !field.isCustom,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!standardFieldMetadata) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFieldMetadataCollection.push({
|
|
||||||
id: fieldMetadata.id,
|
|
||||||
standardId: standardFieldMetadata.standardId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await objectMetadataRepository.save(updateObjectMetadataCollection);
|
|
||||||
|
|
||||||
await fieldMetadataRepository.save(updateFieldMetadataCollection);
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
} catch (error) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
this.logger.error('Error adding standard id to metadata', error);
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Option({
|
|
||||||
flags: '-w, --workspace-id [workspace_id]',
|
|
||||||
description: 'workspace id',
|
|
||||||
required: false,
|
|
||||||
})
|
|
||||||
parseWorkspaceId(value: string): string {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
|||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
|
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
|
||||||
import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
||||||
import { AddStandardIdCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command';
|
|
||||||
import { ConvertRecordPositionsToIntegers } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/convert-record-positions-to-integers.command';
|
import { ConvertRecordPositionsToIntegers } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/convert-record-positions-to-integers.command';
|
||||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.ser
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SyncWorkspaceMetadataCommand,
|
SyncWorkspaceMetadataCommand,
|
||||||
AddStandardIdCommand,
|
|
||||||
ConvertRecordPositionsToIntegers,
|
ConvertRecordPositionsToIntegers,
|
||||||
SyncWorkspaceLoggerService,
|
SyncWorkspaceLoggerService,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export type ParticipantWithId = Participant & {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Attachment = {
|
type Attachment = {
|
||||||
id: string;
|
id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export type GmailThread = {
|
|
||||||
id: string;
|
|
||||||
subject: string;
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { isDefined } from 'class-validator';
|
|
||||||
|
|
||||||
// temporary, to remove once domainName has been fully migrated to Links type
|
|
||||||
export const getCompanyDomainName = (company: any) => {
|
|
||||||
if (!isDefined(company.domainName)) {
|
|
||||||
return company.domainName;
|
|
||||||
}
|
|
||||||
if (typeof company.domainName === 'string') {
|
|
||||||
return company.domainName;
|
|
||||||
} else {
|
|
||||||
return company.domainName.primaryLinkUrl;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user