Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Section" ADD COLUMN "codeSessionId" TEXT;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ model Section {
selectedInInterviews Interview[] @relation("CandidateSelectedSection")
attaches Attach[]
videoCallLink String?
codeSessionId String?

/// Related calendar slot
calendarSlot CalendarEventException? @relation(fields: [calendarSlotId], references: [id])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@
"Send feedback": "",
"Meeting": "",
"Ouch! You probably forgot to add these solutions:": "",
"Add problem": ""
"Add problem": "",
"Create session": "",
"Go to session": ""
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@
"Send feedback": "Отправить фидбек",
"Meeting": "Встреча",
"Ouch! You probably forgot to add these solutions:": "Упс! Кажется, вы забыли решения этих задач:",
"Add problem": "Добавить задачу"
"Add problem": "Добавить задачу",
"Create session": "Создать сессию",
"Go to session": "Открыть сессию"
}
29 changes: 28 additions & 1 deletion src/components/SectionFeedback/SectionFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
IconEditOutline,
IconXCircleOutline,
IconPlusCircleOutline,
IconCodeOutline,
} from '@taskany/icons';

import { usePreviewContext } from '../../contexts/previewContext';
import { useSectionUpdateMutation } from '../../modules/sectionHooks';
import { useSectionCodeSessionCreate, useSectionUpdateMutation } from '../../modules/sectionHooks';
import { SectionWithRelationsAndResults, UpdateSection } from '../../modules/sectionTypes';
import { generatePath, pageHrefs, Paths } from '../../utils/paths';
import { accessChecks } from '../../modules/accessChecks';
Expand Down Expand Up @@ -65,6 +66,7 @@ export const SectionFeedback = ({

const session = useSession();
const sectionUpdateMutation = useSectionUpdateMutation();
const createCodeSessionMutation = useSectionCodeSessionCreate();

const {
control,
Expand Down Expand Up @@ -156,6 +158,10 @@ export const SectionFeedback = ({
onAgree: onSubmit,
});

const createSessionHandler = useCallback(async () => {
await createCodeSessionMutation.mutateAsync({ sectionId: section.id });
}, [section, createCodeSessionMutation]);

const setHire = (value: boolean | null) => {
setValue('hire', value);
};
Expand Down Expand Up @@ -244,6 +250,27 @@ export const SectionFeedback = ({
/>
</Link>
))}
{nullable(
section.codeSessionLink,
(link) => (
<Link href={link} target="_blank">
<Button
iconRight={<IconCodeOutline size="s" />}
text={tr('Go to session')}
type="button"
/>
</Link>
),
nullable(editMode && section.codeSessionLink == null, () => (
<Button
iconRight={<IconCodeOutline size="s" />}
text={tr('Create session')}
type="button"
onClick={createSessionHandler}
/>
)),
)}

{nullable(isEditable && section.hire !== null, () => (
<Button
type="button"
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,10 @@ export default {
model: process.env.AI_ASSISTANT_MODEL,
cvParsePrompt: process.env.AI_ASSISTANT_CV_PARSE_PROMPT,
},
code: {
url: process.env.CODE_URL,
apiToken: process.env.CODE_API_TOKEN,
claimSessionLink: process.env.CODE_SESSION_CLAIM_LINK,
enabled: Boolean(process.env.CODE_URL && process.env.CODE_API_TOKEN && process.env.CODE_SESSION_CLAIM_LINK),
},
};
98 changes: 98 additions & 0 deletions src/modules/codeSessionMethods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { TRPCError } from '@trpc/server';

import config from '../config';

import { CreateSession } from './codeSessionTypes';

type CodeConfig =
| {
enabled: false;
url?: never;
apiToken?: never;
claimSessionLink?: never;
}
| {
enabled: true;
url: string;
apiToken: string;
claimSessionLink: string;
};

const checkConfig = (): CodeConfig => {
if (!config.code.enabled) {
return { enabled: false };
}

if (!config.code.url || !config.code.apiToken || !config.code.claimSessionLink) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Code integration is not configured' });
}

const re = /{sessionId}/g; // placeholder template

// check link template is contains placeholder
if (config.code.claimSessionLink.match(re) == null) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Code integration is not configured' });
}

return {
enabled: true,
url: config.code.url,
apiToken: config.code.apiToken,
claimSessionLink: config.code.claimSessionLink,
};
};

enum endpoint {
sessionCreate = 'session/create',
}

interface RequestPayload {
[endpoint.sessionCreate]: CreateSession['payload'];
}

interface EndpointResponse {
[endpoint.sessionCreate]: CreateSession['response'];
}

interface PostRequestGetter {
<T extends keyof EndpointResponse, B extends RequestPayload[T], R = EndpointResponse[T]>(path: T): (
body: B,
) => Promise<R>;
}

const post: PostRequestGetter = (path) => {
return async (body) => {
const codeCfg = checkConfig();

if (!codeCfg.enabled) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Code integration is not configured' });
}

return fetch(`${codeCfg.url}/api/rest/${path}`, {
method: 'POST',
headers: {
authorization: codeCfg.apiToken,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}).then(async (res) => {
if (res.ok) {
return res.json();
}

const message = await res.text();
throw new TRPCError({ code: 'BAD_REQUEST', message });
});
};
};

export const createSession = post(endpoint.sessionCreate);

export const codeSessionMethods = {
createSession: async (title: string) => {
return createSession({ title });
},
readConfig: async (): Promise<CodeConfig> => {
return checkConfig();
},
};
32 changes: 32 additions & 0 deletions src/modules/codeSessionTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from 'zod';

export const createSessionPayloadScheme = z.object({ title: z.string() });

export const createSessionResponseScheme = z.object({
createdAt: z.date(),
id: z.string(),
updatedAt: z.date(),
active: z.boolean().nullable(),
ownerId: z.string().nullable(),
roomId: z.string(),
document: z.object({
id: z.string(),
title: z.string(),
created_at: z.date(),
updated_at: z.date(),
editable: z.boolean(),
data: z.any().nullable(),
}),
room: z.object({
createdAt: z.date(),
id: z.string(),
updatedAt: z.date(),
ownerId: z.string().nullable(),
documentId: z.string(),
}),
});

export interface CreateSession {
payload: z.infer<typeof createSessionPayloadScheme>;
response: z.infer<typeof createSessionResponseScheme>;
}
3 changes: 2 additions & 1 deletion src/modules/modules.i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,6 @@
"Minimum value is {value}": "",
"Hire streams events can only be requested for 1 day or week": "",
"Valid characters: a-z, _, digits": "",
"Rejection following the results of {title} section": ""
"Rejection following the results of {title} section": "",
"Code session successfully create": ""
}
3 changes: 2 additions & 1 deletion src/modules/modules.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,6 @@
"Minimum value is {value}": "Минимальное значение {value}",
"Hire streams events can only be requested for 1 day or week": "События для потока найма могут быть получены только для интервала в 1 день или неделю",
"Valid characters: a-z, _, digits": "Разрешенные символы: a-z, _, цифры",
"Rejection following the results of {title} section": "Отказ по итогам секции {title}"
"Rejection following the results of {title} section": "Отказ по итогам секции {title}",
"Code session successfully create": "Сессия проведения секции успешно создана"
}
13 changes: 13 additions & 0 deletions src/modules/sectionHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ export const useSectionCancelMutation = () => {
onError: enqueueErrorNotification,
});
};

export const useSectionCodeSessionCreate = () => {
const { enqueueSuccessNotification, enqueueErrorNotification } = useNotifications();
const utils = trpc.useContext();

return trpc.sections.createCodeSession.useMutation({
onSuccess: () => {
enqueueSuccessNotification(tr('Code session successfully create'));
utils.sections.invalidate();
},
onError: enqueueErrorNotification,
});
};
34 changes: 33 additions & 1 deletion src/modules/sectionMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { commentMethods } from './commentMethods';
import { analyticsEventMethods } from './analyticsEventMethods';
import { userMethods } from './userMethods';
import { crewMethods } from './crewMethods';
import { codeSessionMethods } from './codeSessionMethods';

async function getCalendarSlotData(
params: SectionCalendarSlotBooking | undefined,
Expand Down Expand Up @@ -207,7 +208,15 @@ const getById = async (id: number, accessOptions: AccessOptions = {}): Promise<S

section.attaches = section.attaches.filter((attach: Attach) => !attach.deletedAt);

return { ...section, passedSections };
let codeSessionLink: string | null = null;
const codeCfg = await codeSessionMethods.readConfig();

if (codeCfg.enabled && section.codeSessionId) {
const { claimSessionLink } = codeCfg;
codeSessionLink = claimSessionLink.replace(/{(\w.*)}/g, section.codeSessionId);
}

return { ...section, passedSections, codeSessionLink };
};

const getInterviewSections = (data: { interviewId: number }) => {
Expand Down Expand Up @@ -384,6 +393,28 @@ const deleteSection = async ({ sectionId }: DeleteSection): Promise<Section> =>
return prisma.section.delete({ where: { id: sectionId } });
};

const createAndLinkCodeSession = async (sectionId: number) => {
const codeCfg = await codeSessionMethods.readConfig();
if (!codeCfg.enabled) {
return { success: false };
}

const currentSection = await getById(sectionId);

const title = `${currentSection.sectionType.title} at ${currentSection.createdAt.toDateString()}`;

const sessionData = await codeSessionMethods.createSession(title);

await prisma.section.update({
where: { id: currentSection.id },
data: {
codeSessionId: sessionData.id,
},
});

return { success: true };
};

export const sectionMethods = {
create,
getById,
Expand All @@ -392,4 +423,5 @@ export const sectionMethods = {
findAllInterviewerSections,
delete: deleteSection,
cancel: cancelSection,
createAndLinkCodeSession,
};
1 change: 1 addition & 0 deletions src/modules/sectionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export type SectionWithRelationsAndResults = Section & {
};
passedSections: SectionWithSectionType[];
attaches: Attach[];
codeSessionLink: string | null;
};

export interface SectionWithInterviewerRelation extends Section {
Expand Down
5 changes: 5 additions & 0 deletions src/trpc/routers/sectionsRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,9 @@ export const sectionsRouter = router({
.mutation(async ({ input }) => {
return sectionMethods.delete(input);
}),

createCodeSession: protectedProcedure
.input(getSectionSchema)
.use(accessMiddlewares.section.updateNoMetadata)
.mutation(async ({ input }) => sectionMethods.createAndLinkCodeSession(input.sectionId)),
});
Loading