diff --git a/client/package.json b/client/package.json index 16aa9c4309..c7a78b3986 100644 --- a/client/package.json +++ b/client/package.json @@ -39,6 +39,7 @@ "react-dom": "18.2.0", "react-markdown": "8.0.7", "react-masonry-css": "1.0.16", + "react-quill": "2.0.0", "react-use": "17.4.0", "remark-gfm": "3.0.1", "serverless-http": "3.2.0", diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 62c0e37883..30f4c0370b 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -3385,6 +3385,37 @@ export interface InterviewFeedbackDto { */ 'maxScore': number; } +/** + * + * @export + * @interface InviteMentorsDto + */ +export interface InviteMentorsDto { + /** + * + * @type {Array} + * @memberof InviteMentorsDto + */ + 'preselectedCourses': Array; + /** + * + * @type {boolean} + * @memberof InviteMentorsDto + */ + 'certificate': boolean; + /** + * + * @type {boolean} + * @memberof InviteMentorsDto + */ + 'mentor': boolean; + /** + * + * @type {string} + * @memberof InviteMentorsDto + */ + 'text': string; +} /** * * @export @@ -15735,6 +15766,41 @@ export const RegistryApiAxiosParamCreator = function (configuration?: Configurat let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {InviteMentorsDto} inviteMentorsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + inviteMentors: async (inviteMentorsDto: InviteMentorsDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'inviteMentorsDto' is not null or undefined + assertParamExists('inviteMentors', 'inviteMentorsDto', inviteMentorsDto) + const localVarPath = `/registry/mentors/invite`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(inviteMentorsDto, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -15798,6 +15864,16 @@ export const RegistryApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getMentorRegistries(pageSize, currentPage, githubId, cityName, preferedCourses, preselectedCourses, technicalMentoring, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {InviteMentorsDto} inviteMentorsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async inviteMentors(inviteMentorsDto: InviteMentorsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.inviteMentors(inviteMentorsDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -15852,6 +15928,15 @@ export const RegistryApiFactory = function (configuration?: Configuration, baseP getMentorRegistries(pageSize?: number, currentPage?: number, githubId?: string, cityName?: string, preferedCourses?: Array, preselectedCourses?: Array, technicalMentoring?: Array, options?: any): AxiosPromise { return localVarFp.getMentorRegistries(pageSize, currentPage, githubId, cityName, preferedCourses, preselectedCourses, technicalMentoring, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {InviteMentorsDto} inviteMentorsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + inviteMentors(inviteMentorsDto: InviteMentorsDto, options?: any): AxiosPromise { + return localVarFp.inviteMentors(inviteMentorsDto, options).then((request) => request(axios, basePath)); + }, }; }; @@ -15913,6 +15998,17 @@ export class RegistryApi extends BaseAPI { public getMentorRegistries(pageSize?: number, currentPage?: number, githubId?: string, cityName?: string, preferedCourses?: Array, preselectedCourses?: Array, technicalMentoring?: Array, options?: AxiosRequestConfig) { return RegistryApiFp(this.configuration).getMentorRegistries(pageSize, currentPage, githubId, cityName, preferedCourses, preselectedCourses, technicalMentoring, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {InviteMentorsDto} inviteMentorsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof RegistryApi + */ + public inviteMentors(inviteMentorsDto: InviteMentorsDto, options?: AxiosRequestConfig) { + return RegistryApiFp(this.configuration).inviteMentors(inviteMentorsDto, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/client/src/modules/MentorRegistry/components/InviteMentorsModal.tsx b/client/src/modules/MentorRegistry/components/InviteMentorsModal.tsx new file mode 100644 index 0000000000..bd5ed1c9a1 --- /dev/null +++ b/client/src/modules/MentorRegistry/components/InviteMentorsModal.tsx @@ -0,0 +1,71 @@ +import { Alert, Checkbox, Form, message, Select, Space, Spin } from 'antd'; +import { useAsync } from 'react-use'; +import { InviteMentorsDto } from 'api'; +import { ModalForm } from 'components/Forms'; +import { useLoading } from 'components/useLoading'; +import ReactQuill from 'react-quill'; +import { MentorRegistryService } from 'services/mentorRegistry'; +import { DisciplinesApi } from 'api'; + +import 'react-quill/dist/quill.snow.css'; + +type Props = { + onCancel: () => void; +}; +const mentorRegistryService = new MentorRegistryService(); +const disciplinesApi = new DisciplinesApi(); + +function InviteMentorsModal({ onCancel }: Props) { + const [loading, withLoading] = useLoading(false); + const submit = withLoading(async (data: InviteMentorsDto) => { + await mentorRegistryService.inviteMentors(data); + message.success('Invitation successfully send.'); + onCancel(); + }); + + const { loading: disciplinesLoading, value: disciplines = [] } = useAsync(async () => { + const { data } = await disciplinesApi.getDisciplines(); + return data; + }, []); + + return ( + + + + + + + + Mentor in the Past + + + + + + + ); +} + +const formItemStyle = { marginBottom: 0 }; + +export default InviteMentorsModal; diff --git a/client/src/pages/admin/mentor-registry.tsx b/client/src/pages/admin/mentor-registry.tsx index 14fccea9ac..23ed94396f 100644 --- a/client/src/pages/admin/mentor-registry.tsx +++ b/client/src/pages/admin/mentor-registry.tsx @@ -1,7 +1,7 @@ -import { useCallback, useState, useMemo, useEffect } from 'react'; +import { useCallback, useState, useMemo, useEffect, useContext } from 'react'; import { useAsync } from 'react-use'; import FileExcelOutlined from '@ant-design/icons/FileExcelOutlined'; -import { Alert, Button, Col, Form, message, notification, Row, Select, Tabs, Typography } from 'antd'; +import { Alert, Button, Col, Form, message, notification, Row, Select, Space, Tabs, Typography } from 'antd'; import { DisciplineDto, DisciplinesApi, MentorRegistryDto } from 'api'; @@ -18,8 +18,13 @@ import { AdminPageLayout } from 'components/PageLayout'; import { tabRenderer } from 'components/TabsWithCounter/renderers'; import css from 'styled-jsx/css'; import { CommentModal } from 'components/CommentModal'; -import { ActiveCourseProvider, SessionProvider } from 'modules/Course/contexts'; +import { ActiveCourseProvider, SessionContext, SessionProvider } from 'modules/Course/contexts'; import { CoursesService } from 'services/courses'; +import dynamic from 'next/dynamic'; + +const InviteMentorsModal = dynamic(() => import('modules/MentorRegistry/components/InviteMentorsModal'), { + ssr: false, +}); type NotificationType = 'success' | 'info' | 'warning' | 'error'; @@ -28,6 +33,7 @@ export enum ModalDataMode { Resend = 'resend', Delete = 'delete', Comment = 'comment', + BatchInvite = 'batchInvite', } type ModalData = Partial<{ @@ -41,6 +47,7 @@ const disciplinesApi = new DisciplinesApi(); function Page() { const [loading, withLoading] = useLoading(false); + const session = useContext(SessionContext); const [api, contextHolder] = notification.useNotification(); @@ -195,13 +202,16 @@ function Page() { - + + + {session.isAdmin && ( + + )} + @@ -254,6 +264,7 @@ function Page() { }} /> )} + {modalData?.mode === ModalDataMode.BatchInvite && } {contextHolder} ); diff --git a/client/src/services/mentorRegistry.ts b/client/src/services/mentorRegistry.ts index d57c1fee74..445a5b11db 100644 --- a/client/src/services/mentorRegistry.ts +++ b/client/src/services/mentorRegistry.ts @@ -1,5 +1,5 @@ import axios, { AxiosInstance, AxiosResponse } from 'axios'; -import { MentorRegistryDto, RegistryApi } from 'api'; +import { MentorRegistryDto, RegistryApi, InviteMentorsDto } from 'api'; import { PreferredStudentsLocation } from 'common/enums/mentor'; export type MentorResponse = { @@ -88,4 +88,8 @@ export class MentorRegistryService { const response = await this.axios.get>(`/mentor`); return response.data.data; } + + public async inviteMentors(data: InviteMentorsDto) { + await this.registryApi.inviteMentors(data); + } } diff --git a/nestjs/src/notifications/email-template.ts b/nestjs/src/notifications/email-template.ts index 68348bbb10..97ce958584 100644 --- a/nestjs/src/notifications/email-template.ts +++ b/nestjs/src/notifications/email-template.ts @@ -239,7 +239,7 @@ export const emailTemplate = ` valign="top" align="center" > -

© The Rolling Scopes 2022

+

© The Rolling Scopes 2024

discipline.name); } + + @Post('mentors/invite') + @ApiOperation({ operationId: 'inviteMentors' }) + @RequiredRoles([Role.Admin]) + public async inviteMentors(@Body() body: InviteMentorsDto) { + await this.registryService.sendInvitationsToMentors(body); + } } diff --git a/nestjs/src/registry/registry.module.ts b/nestjs/src/registry/registry.module.ts index 0e2c36e663..eaf7d83ac8 100644 --- a/nestjs/src/registry/registry.module.ts +++ b/nestjs/src/registry/registry.module.ts @@ -7,6 +7,8 @@ import { UsersNotificationsModule } from 'src/users-notifications/users-notifica import { RegistryController } from './registry.controller'; import { RegistryService } from './registry.service'; import { DisciplinesModule } from 'src/disciplines'; +import { NotificationsModule } from 'src/notifications/notifications.module'; +import { Student } from '@entities/student'; @Module({ imports: [ @@ -15,6 +17,8 @@ import { DisciplinesModule } from 'src/disciplines'; UsersNotificationsModule, CoursesModule, DisciplinesModule, + NotificationsModule, + TypeOrmModule.forFeature([Student]), ], controllers: [RegistryController], providers: [RegistryService], diff --git a/nestjs/src/registry/registry.service.ts b/nestjs/src/registry/registry.service.ts index 5841bd67c5..ebf79ce80a 100644 --- a/nestjs/src/registry/registry.service.ts +++ b/nestjs/src/registry/registry.service.ts @@ -1,19 +1,27 @@ import { User } from '@entities/user'; import { MentorRegistry } from '@entities/mentorRegistry'; -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { CoursesService } from 'src/courses/courses.service'; import { UsersService } from 'src/users/users.service'; import { Brackets, Repository } from 'typeorm'; import { paginate } from 'src/core/paginate'; +import { InviteMentorsDto } from './dto/invite-mentors.dto'; +import { NotificationsService } from 'src/notifications/notifications.service'; +import { Student } from '@entities/student'; @Injectable() export class RegistryService { + private readonly logger = new Logger('registry'); + constructor( @InjectRepository(MentorRegistry) private mentorsRegistryRepository: Repository, + @InjectRepository(Student) + readonly studentRepository: Repository, private usersService: UsersService, private coursesService: CoursesService, + private notificationsService: NotificationsService, ) {} public async approveMentor(githubId: string, preselectedCourses: string[]): Promise { @@ -164,4 +172,67 @@ export class RegistryService { mentors: response.items, }; } + + public async sendInvitationsToMentors(data: InviteMentorsDto) { + const { text, disciplines, isMentor } = data; + + const query = await this.studentRepository + .createQueryBuilder('student') + .innerJoin('student.course', 'course') + .innerJoin('course.discipline', 'discipline') + .innerJoin( + 'notification_user_connection', + 'notification', + 'notification.userId = student.userId and notification.channelId = :channelId and notification.enabled = :enabled', + { + channelId: 'email', + enabled: true, + }, + ) + .innerJoin('student.certificate', 'certificate') + .where('discipline.id IN (:...ids)', { ids: disciplines }) + .select(['student.userId', 'notification.externalId']) + .distinct(true); + + if (isMentor) { + query.innerJoin('mentor', 'mentor', 'mentor.userId = student.userId'); + } + + const users = await query.getRawMany(); + + Promise.resolve().then( + () => + new Promise(async () => { + this.logger.log({ message: 'processing invitations...' }); + + const batchSize = 10; + + for (let i = 0; i < users.length; i += batchSize) { + const batch = users.slice(i, i + batchSize); + + const promises = batch.map(async user => { + const userId = user.student_userId; + const email = user.notification_externalId; + + try { + await this.notificationsService.sendMessage({ + notificationId: 'mentorsInvitation', + userId, + data: { + text, + }, + channelId: 'email', + channelValue: email, + noEscape: true, + }); + } catch (e) { + this.logger.log({ message: (e as Error).message, userId }); + } + }); + + await Promise.all(promises); + } + }), + ); + } } diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 9a02fc53c8..24329b8410 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -1962,6 +1962,19 @@ "tags": ["registry"] } }, + "/registry/mentors/invite": { + "post": { + "operationId": "inviteMentors", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InviteMentorsDto" } } } + }, + "responses": { "201": { "description": "" } }, + "tags": ["registry"] + } + }, "/certificate/{publicId}": { "get": { "operationId": "getCertificate", @@ -4428,6 +4441,16 @@ }, "required": ["mentors", "total"] }, + "InviteMentorsDto": { + "type": "object", + "properties": { + "preselectedCourses": { "type": "array", "items": { "type": "string" } }, + "certificate": { "type": "boolean" }, + "mentor": { "type": "boolean" }, + "text": { "type": "string" } + }, + "required": ["preselectedCourses", "certificate", "mentor", "text"] + }, "SaveCertificateDto": { "type": "object", "properties": { diff --git a/package-lock.json b/package-lock.json index 26ff10380b..601df1fbc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "react-dom": "18.2.0", "react-markdown": "8.0.7", "react-masonry-css": "1.0.16", + "react-quill": "2.0.0", "react-use": "17.4.0", "remark-gfm": "3.0.1", "serverless-http": "3.2.0", @@ -6638,6 +6639,14 @@ "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", "dev": true }, + "node_modules/@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "dependencies": { + "parchment": "^1.1.2" + } + }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -11478,6 +11487,11 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, "node_modules/events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -17329,6 +17343,11 @@ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -18340,6 +18359,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/quill-delta/node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quill-delta/node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, + "node_modules/quill/node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -19079,6 +19167,20 @@ "react": ">=16.0.0" } }, + "node_modules/react-quill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", + "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", + "dependencies": { + "@types/quill": "^1.3.10", + "lodash": "^4.17.4", + "quill": "^1.3.7" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/react-universal-interface": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", @@ -28693,6 +28795,14 @@ "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", "dev": true }, + "@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "requires": { + "parchment": "^1.1.2" + } + }, "@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -30482,6 +30592,7 @@ "react-dom": "18.2.0", "react-markdown": "8.0.7", "react-masonry-css": "1.0.16", + "react-quill": "2.0.0", "react-use": "17.4.0", "remark-gfm": "3.0.1", "serverless-http": "3.2.0", @@ -32355,6 +32466,11 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -36650,6 +36766,11 @@ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" }, + "parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -37383,6 +37504,64 @@ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" }, + "quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "requires": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + }, + "dependencies": { + "deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "requires": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + } + } + } + }, + "quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "requires": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "dependencies": { + "deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "requires": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + } + }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + } + } + }, "raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -37937,6 +38116,16 @@ "integrity": "sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ==", "requires": {} }, + "react-quill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", + "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", + "requires": { + "@types/quill": "^1.3.10", + "lodash": "^4.17.4", + "quill": "^1.3.7" + } + }, "react-universal-interface": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", diff --git a/server/src/models/notification.ts b/server/src/models/notification.ts index e164f876c3..40de7bb011 100644 --- a/server/src/models/notification.ts +++ b/server/src/models/notification.ts @@ -26,7 +26,8 @@ export type NotificationId = | 'interviewerAssigned' | 'emailConfirmation' | 'mentor:assigned' - | 'messages'; + | 'messages' + | 'mentorsInvitation'; @Entity() export class Notification {