Fix calendar events & messages fetching + fix timeline design (#11840)
Preview : <img width="501" alt="Screenshot 2025-05-02 at 16 24 34" src="https://github.com/user-attachments/assets/0c649df1-0e26-4ddc-8e13-ebd78af7ec09" /> Done : - Fix getCalendarEventsFromPersonIds and getCalendarEventsFromCompanyId (include accountOwner check) - Fix permission check on pre-hook - Pre-hook seems useless, calendar events are always on METADATA or SHARE_EVERYTHING visibility, else post hook always has the responsibility of returning the data user can access. >> To delete or to keep in case other visibility options are added ? - Add post hook to secure finOne / findMany calendarEvents resolver - Update design To do : - same on messages (PR to arrive) closes : https://github.com/twentyhq/twenty/issues/9826
This commit is contained in:
@ -0,0 +1,36 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconLock } from 'twenty-ui/display';
|
||||||
|
import { Card, CardContent } from 'twenty-ui/layout';
|
||||||
|
|
||||||
|
const StyledVisibilityCard = styled(Card)`
|
||||||
|
border-color: ${({ theme }) => theme.border.color.light};
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
flex: 1;
|
||||||
|
transition: color ${({ theme }) => theme.animation.duration.normal} ease;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledVisibilityCardContent = styled(CardContent)`
|
||||||
|
align-items: center;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(0, 1)};
|
||||||
|
height: ${({ theme }) => theme.spacing(6)};
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CalendarEventNotSharedContent = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledVisibilityCard>
|
||||||
|
<StyledVisibilityCardContent>
|
||||||
|
<IconLock size={theme.icon.size.sm} />
|
||||||
|
Not shared
|
||||||
|
</StyledVisibilityCardContent>
|
||||||
|
</StyledVisibilityCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
|
||||||
|
import { isTimelineCalendarEventParticipant } from '@/activities/calendar/types/guards/IsTimelineCalendarEventParticipant';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Avatar, AvatarGroup } from 'twenty-ui/display';
|
||||||
|
import { TimelineCalendarEventParticipant } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
type CalendarEventParticipantsAvatarGroupProps = {
|
||||||
|
participants: CalendarEventParticipant[] | TimelineCalendarEventParticipant[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarEventParticipantsAvatarGroup = ({
|
||||||
|
participants,
|
||||||
|
}: CalendarEventParticipantsAvatarGroupProps) => {
|
||||||
|
const timelineParticipants: TimelineCalendarEventParticipant[] =
|
||||||
|
participants.map((participant) => {
|
||||||
|
if (isTimelineCalendarEventParticipant(participant)) {
|
||||||
|
return participant;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
personId: participant.person?.id ?? null,
|
||||||
|
workspaceMemberId: participant.workspaceMember?.id ?? null,
|
||||||
|
firstName:
|
||||||
|
participant.person?.name?.firstName ||
|
||||||
|
participant.workspaceMember?.name.firstName ||
|
||||||
|
'',
|
||||||
|
lastName:
|
||||||
|
participant.person?.name?.lastName ||
|
||||||
|
participant.workspaceMember?.name.lastName ||
|
||||||
|
'',
|
||||||
|
displayName:
|
||||||
|
participant.person?.name?.firstName ||
|
||||||
|
participant.person?.name?.lastName ||
|
||||||
|
participant.workspaceMember?.name.firstName ||
|
||||||
|
participant.workspaceMember?.name.lastName ||
|
||||||
|
participant.displayName ||
|
||||||
|
participant.handle ||
|
||||||
|
'',
|
||||||
|
avatarUrl:
|
||||||
|
participant.person?.avatarUrl ||
|
||||||
|
participant.workspaceMember?.avatarUrl ||
|
||||||
|
'',
|
||||||
|
handle: participant.handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvatarGroup
|
||||||
|
avatars={timelineParticipants.map((participant) => (
|
||||||
|
<Avatar
|
||||||
|
key={[participant.workspaceMemberId, participant.displayName]
|
||||||
|
.filter(isDefined)
|
||||||
|
.join('-')}
|
||||||
|
avatarUrl={participant.avatarUrl}
|
||||||
|
placeholder={
|
||||||
|
participant.firstName && participant.lastName
|
||||||
|
? `${participant.firstName} ${participant.lastName}`
|
||||||
|
: participant.displayName
|
||||||
|
}
|
||||||
|
placeholderColorSeed={
|
||||||
|
participant.workspaceMemberId || participant.personId
|
||||||
|
}
|
||||||
|
type="rounded"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -5,24 +5,19 @@ import { useContext } from 'react';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
|
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
|
||||||
|
import { CalendarEventNotSharedContent } from '@/activities/calendar/components/CalendarEventNotSharedContent';
|
||||||
|
import { CalendarEventParticipantsAvatarGroup } from '@/activities/calendar/components/CalendarEventParticipantsAvatarGroup';
|
||||||
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
|
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
|
||||||
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
|
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
|
||||||
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
|
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
|
||||||
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
|
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { useOpenCalendarEventInCommandMenu } from '@/command-menu/hooks/useOpenCalendarEventInCommandMenu';
|
import { useOpenCalendarEventInCommandMenu } from '@/command-menu/hooks/useOpenCalendarEventInCommandMenu';
|
||||||
|
import { IconArrowRight } from 'twenty-ui/display';
|
||||||
import {
|
import {
|
||||||
CalendarChannelVisibility,
|
CalendarChannelVisibility,
|
||||||
TimelineCalendarEvent,
|
TimelineCalendarEvent,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarGroup,
|
|
||||||
IconArrowRight,
|
|
||||||
IconLock,
|
|
||||||
} from 'twenty-ui/display';
|
|
||||||
import { Card, CardContent } from 'twenty-ui/layout';
|
|
||||||
|
|
||||||
type CalendarEventRowProps = {
|
type CalendarEventRowProps = {
|
||||||
calendarEvent: TimelineCalendarEvent;
|
calendarEvent: TimelineCalendarEvent;
|
||||||
@ -87,25 +82,6 @@ const StyledTitle = styled.div<{ active: boolean; canceled: boolean }>`
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledVisibilityCard = styled(Card)<{ active: boolean }>`
|
|
||||||
color: ${({ active, theme }) =>
|
|
||||||
active ? theme.font.color.primary : theme.font.color.light};
|
|
||||||
border-color: ${({ theme }) => theme.border.color.light};
|
|
||||||
flex: 1 0 auto;
|
|
||||||
transition: color ${({ theme }) => theme.animation.duration.normal} ease;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledVisibilityCardContent = styled(CardContent)`
|
|
||||||
align-items: center;
|
|
||||||
font-size: ${({ theme }) => theme.font.size.sm};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
padding: ${({ theme }) => theme.spacing(0, 1)};
|
|
||||||
height: ${({ theme }) => theme.spacing(6)};
|
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const CalendarEventRow = ({
|
export const CalendarEventRow = ({
|
||||||
calendarEvent,
|
calendarEvent,
|
||||||
className,
|
className,
|
||||||
@ -159,33 +135,12 @@ export const CalendarEventRow = ({
|
|||||||
{calendarEvent.title}
|
{calendarEvent.title}
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
) : (
|
) : (
|
||||||
<StyledVisibilityCard active={!hasEnded}>
|
<CalendarEventNotSharedContent />
|
||||||
<StyledVisibilityCardContent>
|
|
||||||
<IconLock size={theme.icon.size.sm} />
|
|
||||||
Not shared
|
|
||||||
</StyledVisibilityCardContent>
|
|
||||||
</StyledVisibilityCard>
|
|
||||||
)}
|
)}
|
||||||
</StyledLabels>
|
</StyledLabels>
|
||||||
{!!calendarEvent.participants?.length && (
|
{!!calendarEvent.participants?.length && (
|
||||||
<AvatarGroup
|
<CalendarEventParticipantsAvatarGroup
|
||||||
avatars={calendarEvent.participants.map((participant) => (
|
participants={calendarEvent.participants}
|
||||||
<Avatar
|
|
||||||
key={[participant.workspaceMemberId, participant.displayName]
|
|
||||||
.filter(isDefined)
|
|
||||||
.join('-')}
|
|
||||||
avatarUrl={participant.avatarUrl}
|
|
||||||
placeholder={
|
|
||||||
participant.firstName && participant.lastName
|
|
||||||
? `${participant.firstName} ${participant.lastName}`
|
|
||||||
: participant.displayName
|
|
||||||
}
|
|
||||||
placeholderColorSeed={
|
|
||||||
participant.workspaceMemberId || participant.personId
|
|
||||||
}
|
|
||||||
type="rounded"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{displayCurrentEventCursor && (
|
{displayCurrentEventCursor && (
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
|
||||||
|
import { TimelineCalendarEventParticipant } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const isTimelineCalendarEventParticipant = (
|
||||||
|
participant: CalendarEventParticipant | TimelineCalendarEventParticipant,
|
||||||
|
): participant is TimelineCalendarEventParticipant => {
|
||||||
|
return 'avatarUrl' in participant;
|
||||||
|
};
|
||||||
@ -1,19 +1,22 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isUndefined } from '@sniptt/guards';
|
import { isUndefined } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { CalendarEventNotSharedContent } from '@/activities/calendar/components/CalendarEventNotSharedContent';
|
||||||
|
import { CalendarEventParticipantsAvatarGroup } from '@/activities/calendar/components/CalendarEventParticipantsAvatarGroup';
|
||||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import {
|
import {
|
||||||
formatToHumanReadableDay,
|
formatToHumanReadableDay,
|
||||||
formatToHumanReadableMonth,
|
formatToHumanReadableMonth,
|
||||||
formatToHumanReadableTime,
|
formatToHumanReadableTime,
|
||||||
} from '~/utils/format/formatDate';
|
} from '~/utils/format/formatDate';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
|
|
||||||
const StyledEventCardCalendarEventContainer = styled.div`
|
const StyledEventCardCalendarEventContainer = styled.div`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -29,12 +32,14 @@ const StyledCalendarEventContent = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCalendarEventTop = styled.div`
|
const StyledCalendarEventTop = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: stretch;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -100,6 +105,12 @@ export const EventCardCalendarEvent = ({
|
|||||||
title: true,
|
title: true,
|
||||||
startsAt: true,
|
startsAt: true,
|
||||||
endsAt: true,
|
endsAt: true,
|
||||||
|
calendarEventParticipants: {
|
||||||
|
person: true,
|
||||||
|
workspaceMember: true,
|
||||||
|
handle: true,
|
||||||
|
displayName: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
upsertRecords([data]);
|
upsertRecords([data]);
|
||||||
@ -114,7 +125,7 @@ export const EventCardCalendarEvent = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (shouldHideMessageContent) {
|
if (shouldHideMessageContent) {
|
||||||
return <div>Calendar event not shared</div>;
|
return <CalendarEventNotSharedContent />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldHandleNotFound = error.graphQLErrors.some(
|
const shouldHandleNotFound = error.graphQLErrors.some(
|
||||||
@ -160,10 +171,21 @@ export const EventCardCalendarEvent = ({
|
|||||||
</StyledCalendarEventDateCard>
|
</StyledCalendarEventDateCard>
|
||||||
<StyledCalendarEventContent>
|
<StyledCalendarEventContent>
|
||||||
<StyledCalendarEventTop>
|
<StyledCalendarEventTop>
|
||||||
<StyledCalendarEventTitle>
|
{calendarEvent.title ===
|
||||||
{calendarEvent.title}
|
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED ? (
|
||||||
</StyledCalendarEventTitle>
|
<CalendarEventNotSharedContent />
|
||||||
|
) : (
|
||||||
|
<StyledCalendarEventTitle>
|
||||||
|
{calendarEvent.title}
|
||||||
|
</StyledCalendarEventTitle>
|
||||||
|
)}
|
||||||
|
{!!calendarEvent.calendarEventParticipants?.length && (
|
||||||
|
<CalendarEventParticipantsAvatarGroup
|
||||||
|
participants={calendarEvent.calendarEventParticipants}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledCalendarEventTop>
|
</StyledCalendarEventTop>
|
||||||
|
|
||||||
<StyledCalendarEventBody>
|
<StyledCalendarEventBody>
|
||||||
{startsAtHour} {endsAtHour && <>→ {endsAtHour}</>}
|
{startsAtHour} {endsAtHour && <>→ {endsAtHour}</>}
|
||||||
</StyledCalendarEventBody>
|
</StyledCalendarEventBody>
|
||||||
|
|||||||
@ -183,7 +183,9 @@ export abstract class GraphqlQueryBaseResolverService<
|
|||||||
resultWithGettersArray,
|
resultWithGettersArray,
|
||||||
);
|
);
|
||||||
|
|
||||||
return resultWithGetters;
|
return Array.isArray(resultWithGetters)
|
||||||
|
? resultWithGettersArray
|
||||||
|
: resultWithGettersArray[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
workspaceQueryRunnerGraphqlApiExceptionHandler(error, options);
|
workspaceQueryRunnerGraphqlApiExceptionHandler(error, options);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
|||||||
import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
|
import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
|
||||||
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
|
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
|
||||||
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
|
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
|
||||||
|
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
@ -46,13 +47,15 @@ export class TimelineCalendarEventResolver {
|
|||||||
async getTimelineCalendarEventsFromPersonId(
|
async getTimelineCalendarEventsFromPersonId(
|
||||||
@Args()
|
@Args()
|
||||||
{ personId, page, pageSize }: GetTimelineCalendarEventsFromPersonIdArgs,
|
{ personId, page, pageSize }: GetTimelineCalendarEventsFromPersonIdArgs,
|
||||||
|
@AuthWorkspaceMemberId() workspaceMemberId: string,
|
||||||
) {
|
) {
|
||||||
const timelineCalendarEvents =
|
const timelineCalendarEvents =
|
||||||
await this.timelineCalendarEventService.getCalendarEventsFromPersonIds(
|
await this.timelineCalendarEventService.getCalendarEventsFromPersonIds({
|
||||||
[personId],
|
currentWorkspaceMemberId: workspaceMemberId,
|
||||||
|
personIds: [personId],
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
);
|
});
|
||||||
|
|
||||||
return timelineCalendarEvents;
|
return timelineCalendarEvents;
|
||||||
}
|
}
|
||||||
@ -61,13 +64,15 @@ export class TimelineCalendarEventResolver {
|
|||||||
async getTimelineCalendarEventsFromCompanyId(
|
async getTimelineCalendarEventsFromCompanyId(
|
||||||
@Args()
|
@Args()
|
||||||
{ companyId, page, pageSize }: GetTimelineCalendarEventsFromCompanyIdArgs,
|
{ companyId, page, pageSize }: GetTimelineCalendarEventsFromCompanyIdArgs,
|
||||||
|
@AuthWorkspaceMemberId() workspaceMemberId: string,
|
||||||
) {
|
) {
|
||||||
const timelineCalendarEvents =
|
const timelineCalendarEvents =
|
||||||
await this.timelineCalendarEventService.getCalendarEventsFromCompanyId(
|
await this.timelineCalendarEventService.getCalendarEventsFromCompanyId({
|
||||||
|
currentWorkspaceMemberId: workspaceMemberId,
|
||||||
companyId,
|
companyId,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
);
|
});
|
||||||
|
|
||||||
return timelineCalendarEvents;
|
return timelineCalendarEvents;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,176 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||||
|
|
||||||
|
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||||
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
|
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||||
|
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||||
|
|
||||||
|
import { TimelineCalendarEventService } from './timeline-calendar-event.service';
|
||||||
|
|
||||||
|
type MockWorkspaceRepository = Partial<
|
||||||
|
WorkspaceRepository<CalendarEventWorkspaceEntity>
|
||||||
|
> & {
|
||||||
|
find: jest.Mock;
|
||||||
|
findAndCount: jest.Mock;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('TimelineCalendarEventService', () => {
|
||||||
|
let service: TimelineCalendarEventService;
|
||||||
|
let mockCalendarEventRepository: MockWorkspaceRepository;
|
||||||
|
|
||||||
|
const mockCalendarEvent: Partial<CalendarEventWorkspaceEntity> = {
|
||||||
|
id: '1',
|
||||||
|
title: 'Test Event',
|
||||||
|
description: 'Test Description',
|
||||||
|
startsAt: '2024-01-01T00:00:00.000Z',
|
||||||
|
endsAt: '2024-01-01T01:00:00.000Z',
|
||||||
|
calendarEventParticipants: [],
|
||||||
|
calendarChannelEventAssociations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCalendarEventRepository = {
|
||||||
|
find: jest.fn(),
|
||||||
|
findAndCount: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTwentyORMManager = {
|
||||||
|
getRepository: jest.fn().mockResolvedValue(mockCalendarEventRepository),
|
||||||
|
};
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
TimelineCalendarEventService,
|
||||||
|
{
|
||||||
|
provide: TwentyORMManager,
|
||||||
|
useValue: mockTwentyORMManager,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<TimelineCalendarEventService>(
|
||||||
|
TimelineCalendarEventService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return non-obfuscated calendar events if visibility is SHARE_EVERYTHING', async () => {
|
||||||
|
const currentWorkspaceMemberId = 'current-workspace-member-id';
|
||||||
|
const personIds = ['person-1'];
|
||||||
|
|
||||||
|
mockCalendarEventRepository.find.mockResolvedValue([
|
||||||
|
{ id: '1', startsAt: new Date() },
|
||||||
|
]);
|
||||||
|
mockCalendarEventRepository.findAndCount.mockResolvedValue([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
...mockCalendarEvent,
|
||||||
|
calendarChannelEventAssociations: [
|
||||||
|
{
|
||||||
|
calendarChannel: {
|
||||||
|
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||||
|
connectedAccount: {
|
||||||
|
accountOwnerId: 'other-workspace-member-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.getCalendarEventsFromPersonIds({
|
||||||
|
currentWorkspaceMemberId,
|
||||||
|
personIds,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.timelineCalendarEvents[0].title).toBe('Test Event');
|
||||||
|
expect(result.timelineCalendarEvents[0].description).toBe(
|
||||||
|
'Test Description',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return obfuscated calendar events if visibility is METADATA', async () => {
|
||||||
|
const currentWorkspaceMemberId = 'current-workspace-member-id';
|
||||||
|
const personIds = ['person-1'];
|
||||||
|
|
||||||
|
mockCalendarEventRepository.find.mockResolvedValue([
|
||||||
|
{ id: '1', startsAt: new Date() },
|
||||||
|
]);
|
||||||
|
mockCalendarEventRepository.findAndCount.mockResolvedValue([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
...mockCalendarEvent,
|
||||||
|
calendarChannelEventAssociations: [
|
||||||
|
{
|
||||||
|
calendarChannel: {
|
||||||
|
visibility: CalendarChannelVisibility.METADATA,
|
||||||
|
connectedAccount: {
|
||||||
|
accountOwnerId: 'other-workspace-member-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.getCalendarEventsFromPersonIds({
|
||||||
|
currentWorkspaceMemberId,
|
||||||
|
personIds,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.timelineCalendarEvents[0].title).toBe(
|
||||||
|
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||||
|
);
|
||||||
|
expect(result.timelineCalendarEvents[0].description).toBe(
|
||||||
|
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return non-obfuscated calendar events if visibility is METADATA and user is calendar events owner', async () => {
|
||||||
|
const currentWorkspaceMemberId = 'current-workspace-member-id';
|
||||||
|
const personIds = ['person-1'];
|
||||||
|
|
||||||
|
mockCalendarEventRepository.find.mockResolvedValue([
|
||||||
|
{ id: '1', startsAt: new Date() },
|
||||||
|
]);
|
||||||
|
mockCalendarEventRepository.findAndCount.mockResolvedValue([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
...mockCalendarEvent,
|
||||||
|
calendarChannelEventAssociations: [
|
||||||
|
{
|
||||||
|
calendarChannel: {
|
||||||
|
visibility: CalendarChannelVisibility.METADATA,
|
||||||
|
connectedAccount: {
|
||||||
|
accountOwnerId: 'current-workspace-member-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.getCalendarEventsFromPersonIds({
|
||||||
|
currentWorkspaceMemberId,
|
||||||
|
personIds,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.timelineCalendarEvents[0].title).toBe('Test Event');
|
||||||
|
expect(result.timelineCalendarEvents[0].description).toBe(
|
||||||
|
'Test Description',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
|
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||||
import { Any } from 'typeorm';
|
import { Any } from 'typeorm';
|
||||||
|
|
||||||
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
|
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
|
||||||
@ -15,11 +16,17 @@ export class TimelineCalendarEventService {
|
|||||||
constructor(private readonly twentyORMManager: TwentyORMManager) {}
|
constructor(private readonly twentyORMManager: TwentyORMManager) {}
|
||||||
|
|
||||||
// TODO: Align return type with the entities to avoid mapping
|
// TODO: Align return type with the entities to avoid mapping
|
||||||
async getCalendarEventsFromPersonIds(
|
async getCalendarEventsFromPersonIds({
|
||||||
personIds: string[],
|
currentWorkspaceMemberId,
|
||||||
|
personIds,
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
||||||
): Promise<TimelineCalendarEventsWithTotal> {
|
}: {
|
||||||
|
currentWorkspaceMemberId: string;
|
||||||
|
personIds: string[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}): Promise<TimelineCalendarEventsWithTotal> {
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
const calendarEventRepository =
|
const calendarEventRepository =
|
||||||
@ -64,7 +71,11 @@ export class TimelineCalendarEventService {
|
|||||||
workspaceMember: true,
|
workspaceMember: true,
|
||||||
},
|
},
|
||||||
calendarChannelEventAssociations: {
|
calendarChannelEventAssociations: {
|
||||||
calendarChannel: true,
|
calendarChannel: {
|
||||||
|
connectedAccount: {
|
||||||
|
accountOwner: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -103,17 +114,35 @@ export class TimelineCalendarEventService {
|
|||||||
handle: participant.handle,
|
handle: participant.handle,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const visibility = event.calendarChannelEventAssociations.some(
|
|
||||||
(association) => association.calendarChannel.visibility === 'METADATA',
|
const isCalendarEventImportedByCurrentWorkspaceMember =
|
||||||
)
|
event.calendarChannelEventAssociations.some(
|
||||||
? CalendarChannelVisibility.METADATA
|
(association) =>
|
||||||
: CalendarChannelVisibility.SHARE_EVERYTHING;
|
association.calendarChannel.connectedAccount.accountOwnerId ===
|
||||||
|
currentWorkspaceMemberId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibility =
|
||||||
|
event.calendarChannelEventAssociations.some(
|
||||||
|
(association) =>
|
||||||
|
association.calendarChannel.visibility === 'SHARE_EVERYTHING',
|
||||||
|
) || isCalendarEventImportedByCurrentWorkspaceMember
|
||||||
|
? CalendarChannelVisibility.SHARE_EVERYTHING
|
||||||
|
: CalendarChannelVisibility.METADATA;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...omit(event, [
|
...omit(event, [
|
||||||
'calendarEventParticipants',
|
'calendarEventParticipants',
|
||||||
'calendarChannelEventAssociations',
|
'calendarChannelEventAssociations',
|
||||||
]),
|
]),
|
||||||
|
title:
|
||||||
|
visibility === CalendarChannelVisibility.METADATA
|
||||||
|
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
|
||||||
|
: event.title,
|
||||||
|
description:
|
||||||
|
visibility === CalendarChannelVisibility.METADATA
|
||||||
|
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
|
||||||
|
: event.description,
|
||||||
startsAt: event.startsAt as unknown as Date,
|
startsAt: event.startsAt as unknown as Date,
|
||||||
endsAt: event.endsAt as unknown as Date,
|
endsAt: event.endsAt as unknown as Date,
|
||||||
participants,
|
participants,
|
||||||
@ -127,11 +156,17 @@ export class TimelineCalendarEventService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCalendarEventsFromCompanyId(
|
async getCalendarEventsFromCompanyId({
|
||||||
companyId: string,
|
currentWorkspaceMemberId,
|
||||||
|
companyId,
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
||||||
): Promise<TimelineCalendarEventsWithTotal> {
|
}: {
|
||||||
|
currentWorkspaceMemberId: string;
|
||||||
|
companyId: string;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}): Promise<TimelineCalendarEventsWithTotal> {
|
||||||
const personRepository =
|
const personRepository =
|
||||||
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
|
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
|
||||||
'person',
|
'person',
|
||||||
@ -155,12 +190,13 @@ export class TimelineCalendarEventService {
|
|||||||
|
|
||||||
const formattedPersonIds = personIds.map(({ id }) => id);
|
const formattedPersonIds = personIds.map(({ id }) => id);
|
||||||
|
|
||||||
const messageThreads = await this.getCalendarEventsFromPersonIds(
|
const calendarEvents = await this.getCalendarEventsFromPersonIds({
|
||||||
formattedPersonIds,
|
currentWorkspaceMemberId,
|
||||||
|
personIds: formattedPersonIds,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
);
|
});
|
||||||
|
|
||||||
return messageThreads;
|
return calendarEvents;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { WorkspaceQueryPostHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||||
|
|
||||||
|
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||||
|
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
|
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
|
||||||
|
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||||
|
|
||||||
|
@WorkspaceQueryHook({
|
||||||
|
key: `calendarEvent.findMany`,
|
||||||
|
type: WorkspaceQueryHookType.PostHook,
|
||||||
|
})
|
||||||
|
export class CalendarEventFindManyPostQueryHook
|
||||||
|
implements WorkspaceQueryPostHookInstance
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly applyCalendarEventsVisibilityRestrictionsService: ApplyCalendarEventsVisibilityRestrictionsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
authContext: AuthContext,
|
||||||
|
_objectName: string,
|
||||||
|
payload: CalendarEventWorkspaceEntity[],
|
||||||
|
): Promise<void> {
|
||||||
|
if (!authContext.workspaceMemberId) {
|
||||||
|
throw new UserInputError('Workspace member id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.applyCalendarEventsVisibilityRestrictionsService.applyCalendarEventsVisibilityRestrictions(
|
||||||
|
authContext.workspaceMemberId,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-
|
|||||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
import { CanAccessCalendarEventService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service';
|
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
|
||||||
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceQueryHook({
|
@WorkspaceQueryHook({
|
||||||
@ -18,7 +18,7 @@ export class CalendarEventFindManyPreQueryHook
|
|||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
private readonly canAccessCalendarEventService: CanAccessCalendarEventService,
|
private readonly canAccessCalendarEventsService: CanAccessCalendarEventsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
@ -51,7 +51,7 @@ export class CalendarEventFindManyPreQueryHook
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.canAccessCalendarEventService.canAccessCalendarEvent(
|
await this.canAccessCalendarEventsService.canAccessCalendarEvents(
|
||||||
authContext.user.id,
|
authContext.user.id,
|
||||||
authContext.workspace.id,
|
authContext.workspace.id,
|
||||||
calendarChannelCalendarEventAssociations,
|
calendarChannelCalendarEventAssociations,
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { WorkspaceQueryPostHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||||
|
|
||||||
|
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||||
|
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
|
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
|
||||||
|
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||||
|
|
||||||
|
@WorkspaceQueryHook({
|
||||||
|
key: `calendarEvent.findOne`,
|
||||||
|
type: WorkspaceQueryHookType.PostHook,
|
||||||
|
})
|
||||||
|
export class CalendarEventFindOnePostQueryHook
|
||||||
|
implements WorkspaceQueryPostHookInstance
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly applyCalendarEventsVisibilityRestrictionsService: ApplyCalendarEventsVisibilityRestrictionsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
authContext: AuthContext,
|
||||||
|
_objectName: string,
|
||||||
|
payload: CalendarEventWorkspaceEntity[],
|
||||||
|
): Promise<void> {
|
||||||
|
if (!authContext.workspaceMemberId) {
|
||||||
|
throw new UserInputError('Workspace member id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.applyCalendarEventsVisibilityRestrictionsService.applyCalendarEventsVisibilityRestrictions(
|
||||||
|
authContext.workspaceMemberId,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-b
|
|||||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
import { CanAccessCalendarEventService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service';
|
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
|
||||||
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceQueryHook({
|
@WorkspaceQueryHook({
|
||||||
@ -18,7 +18,7 @@ export class CalendarEventFindOnePreQueryHook
|
|||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
private readonly canAccessCalendarEventService: CanAccessCalendarEventService,
|
private readonly canAccessCalendarEventsService: CanAccessCalendarEventsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
@ -51,7 +51,7 @@ export class CalendarEventFindOnePreQueryHook
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.canAccessCalendarEventService.canAccessCalendarEvent(
|
await this.canAccessCalendarEventsService.canAccessCalendarEvents(
|
||||||
authContext.user.id,
|
authContext.user.id,
|
||||||
authContext.workspace.id,
|
authContext.workspace.id,
|
||||||
calendarChannelCalendarEventAssociations,
|
calendarChannelCalendarEventAssociations,
|
||||||
|
|||||||
@ -0,0 +1,249 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||||
|
|
||||||
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
|
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||||
|
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||||
|
|
||||||
|
import { ApplyCalendarEventsVisibilityRestrictionsService } from './apply-calendar-events-visibility-restrictions.service';
|
||||||
|
|
||||||
|
const createMockCalendarEvent = (
|
||||||
|
id: string,
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
): CalendarEventWorkspaceEntity => ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
isCanceled: false,
|
||||||
|
isFullDay: false,
|
||||||
|
startsAt: '2024-03-20T10:00:00Z',
|
||||||
|
endsAt: '2024-03-20T11:00:00Z',
|
||||||
|
location: '',
|
||||||
|
conferenceLink: {
|
||||||
|
primaryLinkLabel: '',
|
||||||
|
primaryLinkUrl: '',
|
||||||
|
secondaryLinks: null,
|
||||||
|
},
|
||||||
|
externalCreatedAt: '2024-03-20T09:00:00Z',
|
||||||
|
externalUpdatedAt: '2024-03-20T09:00:00Z',
|
||||||
|
deletedAt: null,
|
||||||
|
createdAt: '2024-03-20T09:00:00Z',
|
||||||
|
updatedAt: '2024-03-20T09:00:00Z',
|
||||||
|
iCalUID: '',
|
||||||
|
conferenceSolution: '',
|
||||||
|
calendarChannelEventAssociations: [],
|
||||||
|
calendarEventParticipants: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||||
|
let service: ApplyCalendarEventsVisibilityRestrictionsService;
|
||||||
|
|
||||||
|
const mockCalendarEventAssociationRepository = {
|
||||||
|
find: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockConnectedAccountRepository = {
|
||||||
|
find: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTwentyORMManager = {
|
||||||
|
getRepository: jest.fn().mockImplementation((name) => {
|
||||||
|
if (name === 'calendarChannelEventAssociation') {
|
||||||
|
return mockCalendarEventAssociationRepository;
|
||||||
|
}
|
||||||
|
if (name === 'connectedAccount') {
|
||||||
|
return mockConnectedAccountRepository;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ApplyCalendarEventsVisibilityRestrictionsService,
|
||||||
|
{
|
||||||
|
provide: TwentyORMManager,
|
||||||
|
useValue: mockTwentyORMManager,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ApplyCalendarEventsVisibilityRestrictionsService>(
|
||||||
|
ApplyCalendarEventsVisibilityRestrictionsService,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear all mocks before each test
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return calendar event without obfuscated title and description if the visibility is SHARE_EVERYTHING', async () => {
|
||||||
|
const calendarEvents = [
|
||||||
|
createMockCalendarEvent('1', 'Test Event', 'Test Description'),
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCalendarEventAssociationRepository.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
calendarEventId: '1',
|
||||||
|
calendarChannel: {
|
||||||
|
id: '1',
|
||||||
|
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.applyCalendarEventsVisibilityRestrictions(
|
||||||
|
'workspace-member-id',
|
||||||
|
calendarEvents,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(calendarEvents);
|
||||||
|
expect(
|
||||||
|
result.every(
|
||||||
|
(item) =>
|
||||||
|
item.title !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED &&
|
||||||
|
item.description !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return calendar event with obfuscated title and description if the visibility is METADATA', async () => {
|
||||||
|
const calendarEvents = [
|
||||||
|
createMockCalendarEvent('1', 'Test Event', 'Test Description'),
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCalendarEventAssociationRepository.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
calendarEventId: '1',
|
||||||
|
calendarChannel: {
|
||||||
|
id: '1',
|
||||||
|
visibility: CalendarChannelVisibility.METADATA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await service.applyCalendarEventsVisibilityRestrictions(
|
||||||
|
'workspace-member-id',
|
||||||
|
calendarEvents,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
...calendarEvents[0],
|
||||||
|
title: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||||
|
description: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return calendar event without obfuscated title and description if the workspace member is the owner of the calendar event', async () => {
|
||||||
|
const calendarEvents = [
|
||||||
|
createMockCalendarEvent('1', 'Test Event', 'Test Description'),
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCalendarEventAssociationRepository.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
calendarEventId: '1',
|
||||||
|
calendarChannel: {
|
||||||
|
id: '1',
|
||||||
|
visibility: CalendarChannelVisibility.METADATA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
|
||||||
|
|
||||||
|
const result = await service.applyCalendarEventsVisibilityRestrictions(
|
||||||
|
'workspace-member-id',
|
||||||
|
calendarEvents,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(calendarEvents);
|
||||||
|
expect(
|
||||||
|
result.every(
|
||||||
|
(item) =>
|
||||||
|
item.title !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED &&
|
||||||
|
item.description !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return calendar event if visibility is not SHARE_EVERYTHING or METADATA and the workspace member is not the owner of the calendar event', async () => {
|
||||||
|
const calendarEvents = [
|
||||||
|
createMockCalendarEvent('1', 'Test Event', 'Test Description'),
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCalendarEventAssociationRepository.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
calendarEventId: '1',
|
||||||
|
calendarChannel: {
|
||||||
|
id: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await service.applyCalendarEventsVisibilityRestrictions(
|
||||||
|
'workspace-member-id',
|
||||||
|
calendarEvents,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all calendar events with the right visibility', async () => {
|
||||||
|
const calendarEvents = [
|
||||||
|
createMockCalendarEvent('1', 'Event 1', 'Description 1'),
|
||||||
|
createMockCalendarEvent('2', 'Event 2', 'Description 2'),
|
||||||
|
createMockCalendarEvent('3', 'Event 3', 'Description 3'),
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCalendarEventAssociationRepository.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
calendarEventId: '1',
|
||||||
|
calendarChannel: {
|
||||||
|
id: '1',
|
||||||
|
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calendarEventId: '2',
|
||||||
|
calendarChannel: {
|
||||||
|
id: '2',
|
||||||
|
visibility: CalendarChannelVisibility.METADATA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calendarEventId: '3',
|
||||||
|
calendarChannel: {
|
||||||
|
id: '3',
|
||||||
|
visibility: CalendarChannelVisibility.METADATA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockConnectedAccountRepository.find
|
||||||
|
.mockResolvedValueOnce([]) // request for calendar event 3
|
||||||
|
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
|
||||||
|
|
||||||
|
const result = await service.applyCalendarEventsVisibilityRestrictions(
|
||||||
|
'workspace-member-id',
|
||||||
|
calendarEvents,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
calendarEvents[0],
|
||||||
|
calendarEvents[1],
|
||||||
|
{
|
||||||
|
...calendarEvents[2],
|
||||||
|
title: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||||
|
description: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import groupBy from 'lodash.groupby';
|
||||||
|
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
|
||||||
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
|
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||||
|
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||||
|
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApplyCalendarEventsVisibilityRestrictionsService {
|
||||||
|
constructor(private readonly twentyORMManager: TwentyORMManager) {}
|
||||||
|
|
||||||
|
public async applyCalendarEventsVisibilityRestrictions(
|
||||||
|
workspaceMemberId: string,
|
||||||
|
calendarEvents: CalendarEventWorkspaceEntity[],
|
||||||
|
) {
|
||||||
|
const calendarChannelEventAssociationRepository =
|
||||||
|
await this.twentyORMManager.getRepository<CalendarChannelEventAssociationWorkspaceEntity>(
|
||||||
|
'calendarChannelEventAssociation',
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarChannelCalendarEventsAssociations =
|
||||||
|
await calendarChannelEventAssociationRepository.find({
|
||||||
|
where: {
|
||||||
|
calendarEventId: In(calendarEvents.map((event) => event.id)),
|
||||||
|
},
|
||||||
|
relations: ['calendarChannel', 'calendarChannel.connectedAccount'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectedAccountRepository =
|
||||||
|
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||||
|
'connectedAccount',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = calendarEvents.length - 1; i >= 0; i--) {
|
||||||
|
const calendarChannelCalendarEventAssociations =
|
||||||
|
calendarChannelCalendarEventsAssociations.filter(
|
||||||
|
(association) => association.calendarEventId === calendarEvents[i].id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarChannels = calendarChannelCalendarEventAssociations.map(
|
||||||
|
(association) => association.calendarChannel,
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarChannelsGroupByVisibility = groupBy(
|
||||||
|
calendarChannels,
|
||||||
|
(channel) => channel.visibility,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
calendarChannelsGroupByVisibility[
|
||||||
|
CalendarChannelVisibility.SHARE_EVERYTHING
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectedAccounts = await connectedAccountRepository.find({
|
||||||
|
select: ['id'],
|
||||||
|
where: {
|
||||||
|
calendarChannels: {
|
||||||
|
id: In(calendarChannels.map((channel) => channel.id)),
|
||||||
|
},
|
||||||
|
accountOwnerId: workspaceMemberId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (connectedAccounts.length > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
calendarChannelsGroupByVisibility[CalendarChannelVisibility.METADATA]
|
||||||
|
) {
|
||||||
|
calendarEvents[i].title =
|
||||||
|
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
|
||||||
|
calendarEvents[i].description =
|
||||||
|
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarEvents.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return calendarEvents;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import groupBy from 'lodash.groupby';
|
import groupBy from 'lodash.groupby';
|
||||||
import { Any } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
|
||||||
|
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||||
@ -12,14 +13,14 @@ import { WorkspaceMemberRepository } from 'src/modules/workspace-member/reposito
|
|||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CanAccessCalendarEventService {
|
export class CanAccessCalendarEventsService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||||
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async canAccessCalendarEvent(
|
public async canAccessCalendarEvents(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
calendarChannelCalendarEventAssociations: CalendarChannelEventAssociationWorkspaceEntity[],
|
calendarChannelCalendarEventAssociations: CalendarChannelEventAssociationWorkspaceEntity[],
|
||||||
@ -36,7 +37,8 @@ export class CanAccessCalendarEventService {
|
|||||||
if (
|
if (
|
||||||
calendarChannelsGroupByVisibility[
|
calendarChannelsGroupByVisibility[
|
||||||
CalendarChannelVisibility.SHARE_EVERYTHING
|
CalendarChannelVisibility.SHARE_EVERYTHING
|
||||||
]
|
] ||
|
||||||
|
calendarChannelsGroupByVisibility[CalendarChannelVisibility.METADATA]
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -52,7 +54,9 @@ export class CanAccessCalendarEventService {
|
|||||||
const connectedAccounts = await connectedAccountRepository.find({
|
const connectedAccounts = await connectedAccountRepository.find({
|
||||||
select: ['id'],
|
select: ['id'],
|
||||||
where: {
|
where: {
|
||||||
calendarChannels: Any(calendarChannels.map((channel) => channel.id)),
|
calendarChannels: {
|
||||||
|
id: In(calendarChannels.map((channel) => channel.id)),
|
||||||
|
},
|
||||||
accountOwnerId: currentWorkspaceMember.id,
|
accountOwnerId: currentWorkspaceMember.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -61,6 +65,6 @@ export class CanAccessCalendarEventService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenError('Calendar events not shared');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
|
import { CalendarEventFindManyPostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.post-query.hook';
|
||||||
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
|
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
|
||||||
|
import { CalendarEventFindOnePostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.post-query.hook';
|
||||||
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
|
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
|
||||||
import { CanAccessCalendarEventService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service';
|
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
|
||||||
|
import { CanAccessCalendarEventsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-events.service';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -11,9 +14,12 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
|||||||
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CanAccessCalendarEventService,
|
CanAccessCalendarEventsService,
|
||||||
|
ApplyCalendarEventsVisibilityRestrictionsService,
|
||||||
CalendarEventFindOnePreQueryHook,
|
CalendarEventFindOnePreQueryHook,
|
||||||
CalendarEventFindManyPreQueryHook,
|
CalendarEventFindManyPreQueryHook,
|
||||||
|
CalendarEventFindOnePostQueryHook,
|
||||||
|
CalendarEventFindManyPostQueryHook,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CalendarQueryHookModule {}
|
export class CalendarQueryHookModule {}
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
export const FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED =
|
||||||
|
'FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED';
|
||||||
@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from './FieldForTotalCountAggregateOperation';
|
export { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from './FieldForTotalCountAggregateOperation';
|
||||||
|
export { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from './FieldRestrictedAdditionalPermissionsRequired';
|
||||||
export { PermissionsOnAllObjectRecords } from './PermissionsOnAllObjectRecords';
|
export { PermissionsOnAllObjectRecords } from './PermissionsOnAllObjectRecords';
|
||||||
export { STANDARD_OBJECT_RECORDS_UNDER_OBJECT_RECORDS_PERMISSIONS } from './StandardObjectRecordsUnderObjectRecordsPermissions';
|
export { STANDARD_OBJECT_RECORDS_UNDER_OBJECT_RECORDS_PERMISSIONS } from './StandardObjectRecordsUnderObjectRecordsPermissions';
|
||||||
export { TWENTY_COMPANIES_BASE_URL } from './TwentyCompaniesBaseUrl';
|
export { TWENTY_COMPANIES_BASE_URL } from './TwentyCompaniesBaseUrl';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export type AvatarGroupProps = {
|
export type AvatarGroupProps = {
|
||||||
avatars: ReactNode[];
|
avatars: ReactNode[];
|
||||||
@ -12,6 +12,10 @@ const StyledContainer = styled.div`
|
|||||||
|
|
||||||
const StyledItemContainer = styled.div`
|
const StyledItemContainer = styled.div`
|
||||||
margin-right: -3px;
|
margin-right: -3px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MAX_AVATARS_NB = 4;
|
const MAX_AVATARS_NB = 4;
|
||||||
|
|||||||
Reference in New Issue
Block a user