Harden calendar recording policy handling
This commit is contained in:
@@ -21,6 +21,12 @@ export enum CalendarChannelVisibility {
|
||||
SHARE_EVERYTHING = 'SHARE_EVERYTHING'
|
||||
}
|
||||
|
||||
export type CalendarEventRecordingPreference = {
|
||||
__typename?: 'CalendarEventRecordingPreference';
|
||||
calendarEventId: Scalars['UUID'];
|
||||
recordingPreference: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ComputeStepOutputSchemaInput = {
|
||||
/** Step JSON format */
|
||||
step: Scalars['JSON'];
|
||||
@@ -144,6 +150,7 @@ export type Mutation = {
|
||||
stopWorkflowRun: WorkflowRun;
|
||||
submitFormStep: Scalars['Boolean'];
|
||||
testHttpRequest: TestHttpRequest;
|
||||
updateCalendarEventRecordingPreference: CalendarEventRecordingPreference;
|
||||
updateWorkflowRunStep: WorkflowAction;
|
||||
updateWorkflowVersionPositions: Scalars['Boolean'];
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
@@ -225,6 +232,11 @@ export type MutationTestHttpRequestArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateCalendarEventRecordingPreferenceArgs = {
|
||||
input: UpdateCalendarEventRecordingPreferenceInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkflowRunStepArgs = {
|
||||
input: UpdateWorkflowRunStepInput;
|
||||
};
|
||||
@@ -251,6 +263,7 @@ export type ObjectRecordFilterInput = {
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
canUpdateCalendarEventRecordingPreference: Scalars['Boolean'];
|
||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineCalendarEventsFromOpportunityId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
||||
@@ -263,6 +276,11 @@ export type Query = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryCanUpdateCalendarEventRecordingPreferenceArgs = {
|
||||
calendarEventId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
|
||||
companyId: Scalars['UUID'];
|
||||
page: Scalars['Int'];
|
||||
@@ -475,6 +493,11 @@ export type UuidFilter = {
|
||||
neq?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type UpdateCalendarEventRecordingPreferenceInput = {
|
||||
calendarEventId: Scalars['UUID'];
|
||||
recordingPreference: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UpdateWorkflowRunStepInput = {
|
||||
/** Step to update in JSON format */
|
||||
step: Scalars['JSON'];
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { type UserWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { CalendarEventRecordingPreferenceService } from 'src/engine/core-modules/calendar/calendar-event-recording-preference.service';
|
||||
import {
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||
|
||||
const WORKSPACE_ID = 'workspace-id';
|
||||
const USER_WORKSPACE_ID = 'user-workspace-id';
|
||||
const WORKSPACE_MEMBER_ID = 'workspace-member-id';
|
||||
const CALENDAR_EVENT_ID = 'calendar-event-id';
|
||||
const AUTH_CONTEXT = {} as UserWorkspaceAuthContext;
|
||||
|
||||
const buildCalendarEvent = (
|
||||
overrides: Partial<CalendarEventWorkspaceEntity> = {},
|
||||
): CalendarEventWorkspaceEntity => ({
|
||||
id: CALENDAR_EVENT_ID,
|
||||
title: 'Customer call',
|
||||
description: '',
|
||||
isCanceled: false,
|
||||
isFullDay: false,
|
||||
startsAt: '2026-06-05T11:00:00.000Z',
|
||||
endsAt: '2026-06-05T12:00:00.000Z',
|
||||
location: '',
|
||||
conferenceLink: {
|
||||
primaryLinkLabel: 'Google Meet',
|
||||
primaryLinkUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
secondaryLinks: null,
|
||||
},
|
||||
externalCreatedAt: '2026-06-01T10:00:00.000Z',
|
||||
externalUpdatedAt: '2026-06-01T10:00:00.000Z',
|
||||
deletedAt: null,
|
||||
createdAt: '2026-06-01T10:00:00.000Z',
|
||||
updatedAt: '2026-06-01T10:00:00.000Z',
|
||||
iCalUid: 'ical-uid',
|
||||
conferenceSolution: 'googleMeet',
|
||||
recordingPreference: 'AUTO',
|
||||
calendarChannelEventAssociations: [],
|
||||
calendarEventParticipants: [
|
||||
{
|
||||
workspaceMemberId: WORKSPACE_MEMBER_ID,
|
||||
} as CalendarEventParticipantWorkspaceEntity,
|
||||
],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('CalendarEventRecordingPreferenceService', () => {
|
||||
it('should not update the preference when the user fails custom calendar authorization', async () => {
|
||||
const calendarEventRepository = {
|
||||
findOne: jest.fn().mockResolvedValue(
|
||||
buildCalendarEvent({
|
||||
calendarEventParticipants: [],
|
||||
}),
|
||||
),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const globalWorkspaceOrmManager = {
|
||||
getRepository: jest.fn().mockResolvedValue(calendarEventRepository),
|
||||
executeInWorkspaceContext: jest.fn((callback: () => unknown) =>
|
||||
callback(),
|
||||
),
|
||||
};
|
||||
|
||||
const service = new CalendarEventRecordingPreferenceService(
|
||||
globalWorkspaceOrmManager as unknown as GlobalWorkspaceOrmManager,
|
||||
{} as Repository<CalendarChannelEntity>,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.updateCalendarEventRecordingPreference({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
userWorkspaceId: USER_WORKSPACE_ID,
|
||||
workspaceMemberId: WORKSPACE_MEMBER_ID,
|
||||
calendarEventId: CALENDAR_EVENT_ID,
|
||||
recordingPreference: 'ON',
|
||||
authContext: AUTH_CONTEXT,
|
||||
}),
|
||||
).rejects.toThrow(ForbiddenError);
|
||||
|
||||
expect(calendarEventRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when the preference update affects no rows', async () => {
|
||||
const calendarEventRepository = {
|
||||
findOne: jest.fn().mockResolvedValue(buildCalendarEvent()),
|
||||
update: jest.fn().mockResolvedValue({ affected: 0 }),
|
||||
};
|
||||
|
||||
const globalWorkspaceOrmManager = {
|
||||
getRepository: jest.fn().mockResolvedValue(calendarEventRepository),
|
||||
executeInWorkspaceContext: jest.fn((callback: () => unknown) =>
|
||||
callback(),
|
||||
),
|
||||
};
|
||||
|
||||
const service = new CalendarEventRecordingPreferenceService(
|
||||
globalWorkspaceOrmManager as unknown as GlobalWorkspaceOrmManager,
|
||||
{} as Repository<CalendarChannelEntity>,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.updateCalendarEventRecordingPreference({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
userWorkspaceId: USER_WORKSPACE_ID,
|
||||
workspaceMemberId: WORKSPACE_MEMBER_ID,
|
||||
calendarEventId: CALENDAR_EVENT_ID,
|
||||
recordingPreference: 'ON',
|
||||
authContext: AUTH_CONTEXT,
|
||||
}),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
|
||||
expect(calendarEventRepository.update).toHaveBeenCalledWith(
|
||||
CALENDAR_EVENT_ID,
|
||||
{
|
||||
recordingPreference: 'ON',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
+14
-5
@@ -6,7 +6,10 @@ import { In, Repository } from 'typeorm';
|
||||
import { type UserWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { type CalendarEventRecordingPreferenceDTO } from 'src/engine/core-modules/calendar/dtos/calendar-event-recording-preference.dto';
|
||||
import { type CalendarEventRecordingPreference } from 'src/engine/core-modules/calendar/types/calendar-event-recording-preference.type';
|
||||
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
@@ -72,12 +75,18 @@ export class CalendarEventRecordingPreferenceService {
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarEventWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarEvent',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
await calendarEventRepository.update(calendarEventId, {
|
||||
recordingPreference,
|
||||
});
|
||||
const updateResult = await calendarEventRepository.update(
|
||||
calendarEventId,
|
||||
{
|
||||
recordingPreference,
|
||||
},
|
||||
);
|
||||
|
||||
if ((updateResult.affected ?? 0) === 0) {
|
||||
throw new NotFoundError('Calendar event not found.');
|
||||
}
|
||||
},
|
||||
authContext,
|
||||
{ lite: true },
|
||||
|
||||
+34
-24
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { type CalendarEventRecordingPreference } from 'src/engine/core-modules/calendar/types/calendar-event-recording-preference.type';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type CalendarEventRecordingPolicyReason } from 'src/modules/calendar/calendar-event-recording-manager/types/calendar-event-recording-policy-reason.type';
|
||||
@@ -18,7 +19,7 @@ type FoundCalendarEventRecordingPolicyResult = {
|
||||
workspaceId: string;
|
||||
calendarEventId: string;
|
||||
found: true;
|
||||
recordingPreference: string;
|
||||
recordingPreference: CalendarEventRecordingPreference;
|
||||
realMeetingKey: string;
|
||||
shouldRecord: boolean;
|
||||
reason: CalendarEventRecordingPolicyReason;
|
||||
@@ -127,15 +128,19 @@ export class CalendarEventRecordingPolicyService {
|
||||
|
||||
const affectedMeetingKeys = new Set<string>();
|
||||
const occurrenceStartsAtAnchors = new Set<string>();
|
||||
|
||||
for (const changedCalendarEvent of changedCalendarEvents) {
|
||||
affectedMeetingKeys.add(
|
||||
buildCalendarEventRecordingPolicyResult(changedCalendarEvent, {
|
||||
const changedCalendarEventPolicyResults = changedCalendarEvents.map(
|
||||
(calendarEvent) =>
|
||||
buildCalendarEventRecordingPolicyResult(calendarEvent, {
|
||||
isRecordingEnabledForWorkspace,
|
||||
now,
|
||||
}).realMeetingKey,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
for (const policyResult of changedCalendarEventPolicyResults) {
|
||||
affectedMeetingKeys.add(policyResult.realMeetingKey);
|
||||
}
|
||||
|
||||
for (const changedCalendarEvent of changedCalendarEvents) {
|
||||
if (isDefined(changedCalendarEvent.startsAt)) {
|
||||
occurrenceStartsAtAnchors.add(changedCalendarEvent.startsAt);
|
||||
}
|
||||
@@ -163,29 +168,34 @@ export class CalendarEventRecordingPolicyService {
|
||||
})
|
||||
: [];
|
||||
|
||||
// A changed event with a null start is not returned by the anchor query; keep it so a
|
||||
// link-less, iCalUid-less occurrence still resolves against itself.
|
||||
const occurrenceEventsById = new Map<
|
||||
string,
|
||||
CalendarEventWorkspaceEntity
|
||||
>();
|
||||
const perEventRecordingPolicyResultsByCalendarEventId = new Map(
|
||||
changedCalendarEventPolicyResults.map((policyResult) => [
|
||||
policyResult.calendarEventId,
|
||||
policyResult,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const calendarEvent of [
|
||||
...occurrenceSiblingEvents,
|
||||
...changedCalendarEvents,
|
||||
]) {
|
||||
occurrenceEventsById.set(calendarEvent.id, calendarEvent);
|
||||
}
|
||||
for (const calendarEvent of occurrenceSiblingEvents) {
|
||||
if (
|
||||
perEventRecordingPolicyResultsByCalendarEventId.has(
|
||||
calendarEvent.id,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const perEventRecordingPolicyResults = [
|
||||
...occurrenceEventsById.values(),
|
||||
]
|
||||
.map((calendarEvent) =>
|
||||
perEventRecordingPolicyResultsByCalendarEventId.set(
|
||||
calendarEvent.id,
|
||||
buildCalendarEventRecordingPolicyResult(calendarEvent, {
|
||||
isRecordingEnabledForWorkspace,
|
||||
now,
|
||||
}),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const perEventRecordingPolicyResults = [
|
||||
...perEventRecordingPolicyResultsByCalendarEventId.values(),
|
||||
]
|
||||
.filter((policyResult) =>
|
||||
affectedMeetingKeys.has(policyResult.realMeetingKey),
|
||||
)
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
import { type CalendarEventRecordingPreference } from 'src/engine/core-modules/calendar/types/calendar-event-recording-preference.type';
|
||||
|
||||
export type CalendarEventRecordingPolicyInput = {
|
||||
recordingPreference: string;
|
||||
recordingPreference: CalendarEventRecordingPreference;
|
||||
isCanceled: boolean;
|
||||
startsAt: string | null;
|
||||
endsAt: string | null;
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
import { type CalendarEventRecordingPreference } from 'src/engine/core-modules/calendar/types/calendar-event-recording-preference.type';
|
||||
import { type CalendarEventRecordingPolicyResult } from 'src/modules/calendar/calendar-event-recording-manager/types/calendar-event-recording-policy-result.type';
|
||||
|
||||
export type CalendarEventRecordingPolicyResultForEvent =
|
||||
CalendarEventRecordingPolicyResult & {
|
||||
calendarEventId: string;
|
||||
recordingPreference: string;
|
||||
recordingPreference: CalendarEventRecordingPreference;
|
||||
realMeetingKey: string;
|
||||
};
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { buildCalendarEventRecordingPolicyResult } from 'src/modules/calendar/calendar-event-recording-manager/utils/build-calendar-event-recording-policy-result.util';
|
||||
import { type CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||
|
||||
const NOW = new Date('2026-06-05T10:00:00.000Z');
|
||||
|
||||
const buildCalendarEvent = (
|
||||
overrides: Partial<CalendarEventWorkspaceEntity> = {},
|
||||
): CalendarEventWorkspaceEntity => ({
|
||||
id: 'calendar-event-id',
|
||||
title: 'Customer call',
|
||||
description: '',
|
||||
isCanceled: false,
|
||||
isFullDay: false,
|
||||
startsAt: '2026-06-05T11:00:00.000Z',
|
||||
endsAt: '2026-06-05T12:00:00.000Z',
|
||||
location: '',
|
||||
conferenceLink: {
|
||||
primaryLinkLabel: 'Google Meet',
|
||||
primaryLinkUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
secondaryLinks: null,
|
||||
},
|
||||
externalCreatedAt: '2026-06-01T10:00:00.000Z',
|
||||
externalUpdatedAt: '2026-06-01T10:00:00.000Z',
|
||||
deletedAt: null,
|
||||
createdAt: '2026-06-01T10:00:00.000Z',
|
||||
updatedAt: '2026-06-01T10:00:00.000Z',
|
||||
iCalUid: 'ical-uid',
|
||||
conferenceSolution: 'googleMeet',
|
||||
recordingPreference: 'AUTO',
|
||||
calendarChannelEventAssociations: [],
|
||||
calendarEventParticipants: [
|
||||
{
|
||||
workspaceMemberId: null,
|
||||
} as CalendarEventParticipantWorkspaceEntity,
|
||||
],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('buildCalendarEventRecordingPolicyResult', () => {
|
||||
it('should normalize unknown recording preferences to AUTO before resolving policy', () => {
|
||||
const result = buildCalendarEventRecordingPolicyResult(
|
||||
buildCalendarEvent({
|
||||
recordingPreference: 'SOMETHING_ELSE',
|
||||
}),
|
||||
{
|
||||
isRecordingEnabledForWorkspace: true,
|
||||
now: NOW,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.recordingPreference).toBe('AUTO');
|
||||
expect(result.shouldRecord).toBe(true);
|
||||
expect(result.reason).toBe('AUTO_POLICY_MATCHED');
|
||||
});
|
||||
});
|
||||
+13
@@ -59,6 +59,19 @@ describe('computeRealMeetingKey', () => {
|
||||
).toBe('ical:recurring-uid@google.com:2026-06-05T11:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should ignore malformed conference link provider data', () => {
|
||||
expect(
|
||||
computeRealMeetingKey({
|
||||
calendarEventId: 'event-1',
|
||||
conferenceLinkUrl: {
|
||||
primaryLinkUrl: 'not-a-string',
|
||||
} as unknown as string,
|
||||
iCalUid: 'recurring-uid@google.com',
|
||||
startsAt: '2026-06-05T11:00:00.000Z',
|
||||
}),
|
||||
).toBe('ical:recurring-uid@google.com:2026-06-05T11:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should separate recurring occurrences by start time', () => {
|
||||
const firstOccurrence = computeRealMeetingKey({
|
||||
calendarEventId: 'event-1',
|
||||
|
||||
-11
@@ -65,17 +65,6 @@ describe('resolveCalendarEventRecordingPolicyResult', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should treat an unknown preference as AUTO', () => {
|
||||
expect(
|
||||
resolvePolicyResult(
|
||||
buildPolicyInput({ recordingPreference: 'SOMETHING_ELSE' }),
|
||||
),
|
||||
).toEqual({
|
||||
shouldRecord: true,
|
||||
reason: 'AUTO_POLICY_MATCHED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep an in-progress meeting eligible via its end time', () => {
|
||||
expect(
|
||||
resolvePolicyResult(
|
||||
|
||||
+24
-2
@@ -1,5 +1,9 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
CALENDAR_EVENT_RECORDING_PREFERENCES,
|
||||
type CalendarEventRecordingPreference,
|
||||
} from 'src/engine/core-modules/calendar/types/calendar-event-recording-preference.type';
|
||||
import { type CalendarEventRecordingPolicyResultForEvent } from 'src/modules/calendar/calendar-event-recording-manager/types/calendar-event-recording-policy-result-for-event.type';
|
||||
import { computeRealMeetingKey } from 'src/modules/calendar/calendar-event-recording-manager/utils/compute-real-meeting-key.util';
|
||||
import { resolveCalendarEventRecordingPolicyResult } from 'src/modules/calendar/calendar-event-recording-manager/utils/resolve-calendar-event-recording-policy-result.util';
|
||||
@@ -30,10 +34,13 @@ export const buildCalendarEventRecordingPolicyResult = (
|
||||
const hasExternalParticipant = (
|
||||
calendarEvent.calendarEventParticipants ?? []
|
||||
).some((participant) => !isDefined(participant.workspaceMemberId));
|
||||
const recordingPreference = normalizeCalendarEventRecordingPreference(
|
||||
calendarEvent.recordingPreference,
|
||||
);
|
||||
|
||||
const policyResult = resolveCalendarEventRecordingPolicyResult({
|
||||
input: {
|
||||
recordingPreference: calendarEvent.recordingPreference,
|
||||
recordingPreference,
|
||||
isCanceled: calendarEvent.isCanceled,
|
||||
startsAt: calendarEvent.startsAt,
|
||||
endsAt: calendarEvent.endsAt,
|
||||
@@ -46,8 +53,23 @@ export const buildCalendarEventRecordingPolicyResult = (
|
||||
|
||||
return {
|
||||
calendarEventId: calendarEvent.id,
|
||||
recordingPreference: calendarEvent.recordingPreference,
|
||||
recordingPreference,
|
||||
realMeetingKey,
|
||||
...policyResult,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeCalendarEventRecordingPreference = (
|
||||
recordingPreference: string,
|
||||
): CalendarEventRecordingPreference =>
|
||||
isCalendarEventRecordingPreference(recordingPreference)
|
||||
? recordingPreference
|
||||
: 'AUTO';
|
||||
|
||||
const isCalendarEventRecordingPreference = (
|
||||
recordingPreference: string,
|
||||
): recordingPreference is CalendarEventRecordingPreference =>
|
||||
CALENDAR_EVENT_RECORDING_PREFERENCES.some(
|
||||
(calendarEventRecordingPreference) =>
|
||||
calendarEventRecordingPreference === recordingPreference,
|
||||
);
|
||||
|
||||
+9
-3
@@ -1,3 +1,4 @@
|
||||
import { isNonEmptyString, isString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type ComputeRealMeetingKeyInput = {
|
||||
@@ -30,12 +31,17 @@ export const computeRealMeetingKey = ({
|
||||
const normalizeConferenceLink = (
|
||||
conferenceLinkUrl: string | null,
|
||||
): string | null => {
|
||||
if (!isDefined(conferenceLinkUrl) || conferenceLinkUrl.trim() === '') {
|
||||
if (!isString(conferenceLinkUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const withoutProtocol = conferenceLinkUrl
|
||||
.trim()
|
||||
const trimmedConferenceLinkUrl = conferenceLinkUrl.trim();
|
||||
|
||||
if (!isNonEmptyString(trimmedConferenceLinkUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const withoutProtocol = trimmedConferenceLinkUrl
|
||||
.toLowerCase()
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/^www\./, '');
|
||||
|
||||
Reference in New Issue
Block a user