diff --git a/.env.example b/.env.example deleted file mode 100644 index d9bb1b5b..00000000 --- a/.env.example +++ /dev/null @@ -1,28 +0,0 @@ -# Created by Vercel CLI -COPILOT_API_KEY="" -COPILOT_ENV="local" -NX_DAEMON="" -POSTGRES_DATABASE="" -POSTGRES_HOST="" -POSTGRES_PASSWORD="" -POSTGRES_PRISMA_URL="" -POSTGRES_URL="" -POSTGRES_URL_NON_POOLING="" -POSTGRES_USER="default" -TURBO_REMOTE_ONLY="" -TURBO_RUN_SUMMARY="" -VERCEL="1" -VERCEL_ENV="development" -VERCEL_GIT_COMMIT_AUTHOR_LOGIN="" -VERCEL_GIT_COMMIT_AUTHOR_NAME="" -VERCEL_GIT_COMMIT_MESSAGE="" -VERCEL_GIT_COMMIT_REF="" -VERCEL_GIT_COMMIT_SHA="" -VERCEL_GIT_PREVIOUS_SHA="" -VERCEL_GIT_PROVIDER="" -VERCEL_GIT_PULL_REQUEST_ID="" -VERCEL_GIT_REPO_ID="" -VERCEL_GIT_REPO_OWNER="" -VERCEL_GIT_REPO_SLUG="" -VERCEL_URL="" -WEBHOOK_SIGNING_SECRET="" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6818bcda..da69e3b3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 18.17.0 + node-version: 20 cache: yarn cache-dependency-path: './yarn.lock' diff --git a/.gitignore b/.gitignore index ed13faaa..1a485616 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ next-env.d.ts /migrations/migration_lock.toml .idea /.vscode/ + +# Sentry Config File +.sentryclirc diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..40c7f29b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 - 2025 Copilot Platforms Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/migrations/20231018163816_create_setting_table/migration.sql b/migrations/0_init/migration.sql similarity index 52% rename from migrations/20231018163816_create_setting_table/migration.sql rename to migrations/0_init/migration.sql index 5a92f71f..eaca5d7f 100644 --- a/migrations/20231018163816_create_setting_table/migration.sql +++ b/migrations/0_init/migration.sql @@ -10,10 +10,21 @@ CREATE TABLE "Setting" ( "createdById" UUID NOT NULL, "message" TEXT, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMPTZ NOT NULL, CONSTRAINT "Setting_pkey" PRIMARY KEY ("id") ); --- CreateIndex -CREATE UNIQUE INDEX "Setting_createdById_key" ON "Setting"("createdById"); +-- CreateTable +CREATE TABLE "Message" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "message" TEXT NOT NULL, + "clientId" UUID NOT NULL, + "channelId" UUID NOT NULL, + "senderId" UUID NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "Message_pkey" PRIMARY KEY ("id") +); + diff --git a/migrations/20231020073838_create_message_table/migration.sql b/migrations/20231020073838_create_message_table/migration.sql deleted file mode 100644 index 80c6c571..00000000 --- a/migrations/20231020073838_create_message_table/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ --- DropIndex -DROP INDEX "Setting_createdById_key"; - --- CreateTable -CREATE TABLE "Message" ( - "id" UUID NOT NULL DEFAULT gen_random_uuid(), - "message" TEXT NOT NULL, - "clientId" UUID NOT NULL, - "channelId" UUID NOT NULL, - "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Message_pkey" PRIMARY KEY ("id") -); diff --git a/migrations/20231020074502_alter_table_message_add_sender_id/migration.sql b/migrations/20231020074502_alter_table_message_add_sender_id/migration.sql deleted file mode 100644 index 47324339..00000000 --- a/migrations/20231020074502_alter_table_message_add_sender_id/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `senderId` to the `Message` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Message" ADD COLUMN "senderId" UUID NOT NULL; diff --git a/migrations/20231020101125_alter_tables_make_updated_at_timezone/migration.sql b/migrations/20231020101125_alter_tables_make_updated_at_timezone/migration.sql deleted file mode 100644 index 2d1d871f..00000000 --- a/migrations/20231020101125_alter_tables_make_updated_at_timezone/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- AlterTable -ALTER TABLE "Message" ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ; - --- AlterTable -ALTER TABLE "Setting" ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ; diff --git a/migrations/20240207093948_setup_sender_id_in_settings/migration.sql b/migrations/20240207093948_setup_sender_id_in_settings/migration.sql new file mode 100644 index 00000000..cd9973e8 --- /dev/null +++ b/migrations/20240207093948_setup_sender_id_in_settings/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "senderId" UUID; diff --git a/migrations/20240207093949_migrate_field_senderId_in_createrId/migration.sql b/migrations/20240207093949_migrate_field_senderId_in_createrId/migration.sql new file mode 100644 index 00000000..f2d04fd7 --- /dev/null +++ b/migrations/20240207093949_migrate_field_senderId_in_createrId/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +UPDATE "Setting" SET "senderId"="createdById"; diff --git a/migrations/20240216011532_make_sender_id_non_nullable/migration.sql b/migrations/20240216011532_make_sender_id_non_nullable/migration.sql new file mode 100644 index 00000000..c4f4cf17 --- /dev/null +++ b/migrations/20240216011532_make_sender_id_non_nullable/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `senderId` on table `Setting` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Setting" ALTER COLUMN "senderId" SET NOT NULL; diff --git a/migrations/20240216011746_add_workspace_id_in_settings/migration.sql b/migrations/20240216011746_add_workspace_id_in_settings/migration.sql new file mode 100644 index 00000000..839a3bec --- /dev/null +++ b/migrations/20240216011746_add_workspace_id_in_settings/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `workspaceId` to the `Setting` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "workspaceId" UUID NOT NULL; diff --git a/migrations/20240216014724_remove_uuid_constraint_in_settings/migration.sql b/migrations/20240216014724_remove_uuid_constraint_in_settings/migration.sql new file mode 100644 index 00000000..9642091f --- /dev/null +++ b/migrations/20240216014724_remove_uuid_constraint_in_settings/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Setting" ALTER COLUMN "workspaceId" SET DATA TYPE TEXT; diff --git a/migrations/20240320162855_alter_table_message_change_column_channel_id_to_string/migration.sql b/migrations/20240320162855_alter_table_message_change_column_channel_id_to_string/migration.sql new file mode 100644 index 00000000..4c6c0fda --- /dev/null +++ b/migrations/20240320162855_alter_table_message_change_column_channel_id_to_string/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Message" ALTER COLUMN "channelId" SET DATA TYPE TEXT; diff --git a/next.config.js b/next.config.js index 22478d5d..b071295d 100644 --- a/next.config.js +++ b/next.config.js @@ -4,3 +4,47 @@ const withSvgr = require('next-plugin-svgr'); const nextConfig = {}; module.exports = withSvgr(nextConfig); + +// Injected content via Sentry wizard below + +const { withSentryConfig } = require('@sentry/nextjs'); + +module.exports = withSentryConfig( + module.exports, + { + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options + + // Suppresses source map uploading logs during build + silent: true, + org: 'copilot-platforms', + project: 'auto-responder', + }, + { + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Transpiles SDK to be compatible with IE11 (increases bundle size) + transpileClientSDK: true, + + // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. (increases server load) + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: '/monitoring', + + // Hides source maps from generated client bundles + hideSourceMaps: true, + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + + // Enables automatic instrumentation of Vercel Cron Monitors. + // See the following for more information: + // https://docs.sentry.io/product/crons/ + // https://vercel.com/docs/cron-jobs + automaticVercelMonitors: true, + }, +); diff --git a/package.json b/package.json index b4c25997..306dfe05 100644 --- a/package.json +++ b/package.json @@ -15,19 +15,28 @@ "prettier:check": "prettier --check \"{src,test}/**/*.{ts,tsx}\"", "prettier:fix": "prettier --write .", "lint-staged": "npx lint-staged", - "postinstall": "prisma generate" + "postinstall": "prisma generate", + "db:migrate": "dotenv-run-script .env.development.local -- db:_migrate", + "db:_migrate": "prisma migrate dev", + "db:seed": "dotenv-run-script .env.development.local -- db:_seed", + "db:_seed": "prisma migrate resolve --applied 0_init" }, "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", "@hookform/resolvers": "^3.3.2", "@js-joda/core": "^5.6.1", "@js-joda/timezone": "^2.18.2", - "@prisma/client": "^5.4.2", + "@mui/material": "^5.15.7", + "@prisma/client": "^5.9.1", "@radix-ui/react-select": "^2.0.0", + "@sentry/nextjs": "^7.105.0", "@vercel/postgres": "^0.5.0", - "copilot-node-sdk": "^0.0.45", - "next": "^14.0.4", + "copilot-node-sdk": "^3.5.1", + "dotenv-run-script": "^0.4.1", + "next": "14.2.35", "next-plugin-svgr": "^1.1.8", - "prisma": "^5.4.2", + "prisma": "^5.9.1", "react": "latest", "react-dom": "latest", "react-error-boundary": "^4.0.11", @@ -49,6 +58,7 @@ "prettier": "^3.1.1", "prisma": "^5.4.2", "tailwindcss": "latest", + "ts-node": "^10.9.2", "typescript": "latest" }, "lint-staged": { @@ -56,5 +66,6 @@ "yarn lint:fix", "yarn prettier:fix" ] - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/schema.prisma b/schema.prisma index 3d80bae4..0246d0b2 100644 --- a/schema.prisma +++ b/schema.prisma @@ -20,6 +20,8 @@ model Setting { timezone String? workingHours Json? @db.JsonB createdById String @db.Uuid + senderId String @db.Uuid + workspaceId String message String? createdAt DateTime @default(now()) @db.Timestamptz() updatedAt DateTime @updatedAt @ignore @db.Timestamptz() @@ -29,7 +31,7 @@ model Message { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid message String clientId String @db.Uuid - channelId String @db.Uuid + channelId String senderId String @db.Uuid createdAt DateTime @default(now()) @db.Timestamptz() updatedAt DateTime @updatedAt @ignore @db.Timestamptz() diff --git a/sentry.client.config.ts b/sentry.client.config.ts new file mode 100644 index 00000000..e683dc21 --- /dev/null +++ b/sentry.client.config.ts @@ -0,0 +1,31 @@ +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import appConfig from '@/config/app'; +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: appConfig.sentry.DSN, + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + + replaysOnErrorSampleRate: 1.0, + + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + integrations: [ + Sentry.replayIntegration({ + // Additional Replay configuration goes in here, for example: + maskAllText: true, + blockAllMedia: true, + }), + ], +}); diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts new file mode 100644 index 00000000..f589352e --- /dev/null +++ b/sentry.edge.config.ts @@ -0,0 +1,16 @@ +// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). +// The config you add here will be used whenever one of the edge features is loaded. +// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import appConfig from '@/config/app'; +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: appConfig.sentry.DSN, + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, +}); diff --git a/sentry.server.config.ts b/sentry.server.config.ts new file mode 100644 index 00000000..dbd422ae --- /dev/null +++ b/sentry.server.config.ts @@ -0,0 +1,18 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import appConfig from '@/config/app'; +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: appConfig.sentry.DSN, + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + + // uncomment the line below to enable Spotlight (https://spotlightjs.com) + // spotlight: process.env.NODE_ENV === 'development', +}); diff --git a/src/app/api/internal-users/route.ts b/src/app/api/internal-users/route.ts new file mode 100644 index 00000000..0f8bdac2 --- /dev/null +++ b/src/app/api/internal-users/route.ts @@ -0,0 +1,22 @@ +import { errorHandler } from '@/utils/common'; +import { CopilotAPI } from '@/utils/copilotApiUtils'; +import { NextResponse, NextRequest } from 'next/server'; +import { z } from 'zod'; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const token = searchParams.get('token'); + if (!token) { + return errorHandler('Missing token', 422); + } + + const copilotClient = new CopilotAPI(z.string().parse(token)); + try { + const clients = await copilotClient.getInternalUsers(); + + return NextResponse.json(clients); + } catch (error) { + console.error('getInternalUsers', error); + return errorHandler('Clients not found.', 404); + } +} diff --git a/src/app/api/messages/services/message.service.ts b/src/app/api/messages/services/message.service.ts index 04a75a26..7699f840 100644 --- a/src/app/api/messages/services/message.service.ts +++ b/src/app/api/messages/services/message.service.ts @@ -6,6 +6,7 @@ import { SettingResponse } from '@/types/setting'; import { Message, SendMessageRequestSchema } from '@/types/message'; import DBClient from '@/lib/db'; import { z } from 'zod'; +import { CopilotAPIError, matchesCopilotApiError } from '@/exceptions/copilot'; export class MessageService { private prismaClient: PrismaClient = DBClient.getInstance(); @@ -13,8 +14,8 @@ export class MessageService { async handleSendMessageWebhook(message: Message, { apiToken }: { apiToken: string }) { const settingService = new SettingService(); const copilotClient = new CopilotAPI(apiToken); - const currentUser = await copilotClient.me(); - const setting = await settingService.findByUserId(currentUser.id); + const workspace = await copilotClient.getWorkspace(); + const setting = await settingService.findByWorkspaceId(workspace.id); if (setting?.type === SettingType.DISABLED) { return; } @@ -50,6 +51,12 @@ export class MessageService { await this.sendMessage(copilotClient, setting, message); } } catch (e: unknown) { + if (matchesCopilotApiError(e)) { + if (e.request?.errors && e.request.errors['404'] === '404') { + return; + } + } + console.error(e); return; @@ -65,7 +72,7 @@ export class MessageService { async sendMessage(copilotClient: CopilotAPI, setting: SettingResponse, message: Message): Promise { const messageData = SendMessageRequestSchema.parse({ text: setting.message, - senderId: setting.createdById, + senderId: setting.senderId, channelId: message.channelId, }); @@ -76,7 +83,7 @@ export class MessageService { message: z.string().parse(setting.message), clientId: message.senderId, channelId: messageData.channelId, - senderId: setting.createdById, + senderId: setting.senderId, }, }), ]); diff --git a/src/app/api/messages/webhook/route.ts b/src/app/api/messages/webhook/route.ts index a2e2986d..8db90100 100644 --- a/src/app/api/messages/webhook/route.ts +++ b/src/app/api/messages/webhook/route.ts @@ -4,6 +4,7 @@ import { MessageSchema } from '@/types/message'; import { MessageService } from '@/app/api/messages/services/message.service'; import appConfig from '@/config/app'; import { WebhookSchema } from '@/types/webhook'; +import { hasTimeExceeded } from '@/utils/hasTimeExceeded'; export async function POST(request: NextRequest) { const rawBody = await request.text(); @@ -48,6 +49,10 @@ export async function POST(request: NextRequest) { ); } } + if (hasTimeExceeded(payload.data.createdAt)) { + console.info('Autoresponse failed due to stale webhook timestamp. ', payload.data); + return NextResponse.json({}); + } const messageService = new MessageService(); await messageService.handleSendMessageWebhook(payload.data, { diff --git a/src/app/api/settings/services/setting.service.ts b/src/app/api/settings/services/setting.service.ts index df062157..8ba3f104 100644 --- a/src/app/api/settings/services/setting.service.ts +++ b/src/app/api/settings/services/setting.service.ts @@ -1,16 +1,14 @@ import { PrismaClient } from '@prisma/client'; import { SettingRequest, SettingResponse, SettingResponseSchema } from '@/types/setting'; -import { getCurrentUser } from '@/utils/common'; +import { getCurrentUser, getWorkspace } from '@/utils/common'; import DBClient from '@/lib/db'; export class SettingService { private prismaClient: PrismaClient = DBClient.getInstance(); - async findByUserId(createdById: string): Promise { + async findByWorkspaceId(workspaceId: string): Promise { const setting = await this.prismaClient.setting.findFirst({ - where: { - createdById: createdById, - }, + where: { workspaceId }, }); if (!setting) { @@ -22,14 +20,17 @@ export class SettingService { async save(requestData: SettingRequest, { apiToken }: { apiToken: string }): Promise { const currentUser = await getCurrentUser(apiToken); + if (!currentUser) throw new Error('No user associated with this session'); + + const currentWorkspace = await getWorkspace(apiToken); - const settingByUser = await this.prismaClient.setting.findFirst({ + const settingByWorkspace = await this.prismaClient.setting.findFirst({ where: { - createdById: currentUser.id, + workspaceId: currentWorkspace.id, }, }); - if (!settingByUser) { + if (!settingByWorkspace) { await this.prismaClient.setting.create({ data: { type: requestData.type, @@ -38,6 +39,8 @@ export class SettingService { workingHours: requestData.workingHours, message: requestData.message, createdById: currentUser.id, + senderId: requestData.senderId, + workspaceId: currentWorkspace?.id, }, }); @@ -46,7 +49,7 @@ export class SettingService { await this.prismaClient.setting.update({ where: { - id: settingByUser.id, + id: settingByWorkspace.id, }, data: { type: requestData.type, @@ -54,6 +57,8 @@ export class SettingService { // @ts-ignore workingHours: requestData.workingHours, message: requestData.message, + senderId: requestData.senderId, + workspaceId: currentWorkspace.id, }, }); } diff --git a/src/app/components/AutoResponder.tsx b/src/app/components/AutoResponder.tsx index 25906040..e420062e 100644 --- a/src/app/components/AutoResponder.tsx +++ b/src/app/components/AutoResponder.tsx @@ -1,4 +1,5 @@ 'use client'; + import { z } from 'zod'; import { useEffect, useRef, useState } from 'react'; import TimezoneSelect, { ITimezone } from 'react-timezone-select'; @@ -24,6 +25,8 @@ import { SelectedDay, SettingsData, } from '@/constants'; +import { InternalUser, InternalUsers, WorkspaceResponse } from '@/types/common'; +import { MenuItem, Select, SelectChangeEvent } from '@mui/material'; const defaultSelectedDays: SelectedDay[] = [ { @@ -56,7 +59,10 @@ const defaultSelectedDays: SelectedDay[] = [ interface Props { onSave(data: SettingsData): Promise; activeSettings: SettingsData; + internalUsers: InternalUsers; + workspace?: WorkspaceResponse; } + const DropdownIndicator = (props: DropdownIndicatorProps>) => { return ( @@ -145,10 +151,10 @@ const ValidationSchema = z.object({ .min(10, "Response can't be less than 10 characters long") .max(2000, "Response can't be more than 2000 characters long") .nullable(), - sender: z.string(), + senderId: z.string().uuid().nullable(), }); -const AutoResponder = ({ onSave, activeSettings }: Props) => { +const AutoResponder = ({ onSave, activeSettings, internalUsers, workspace }: Props) => { const defaultFormValues = useRef(activeSettings); const [saving, setSaving] = useState(false); const [workingHoursErrors, setWorkingHoursErrors] = useState>({}); @@ -261,15 +267,36 @@ const AutoResponder = ({ onSave, activeSettings }: Props) => { setWorkingHoursErrors({}); }; + const checkFormErrors = () => { + if (autoRespond === SettingType.ENABLED || autoRespond === SettingType.DISABLED) { + delete errors.selectedDays; + } + return !!Object.keys(errors).length; + }; + return ( Something went wrong}> -
+ { + e.preventDefault(); + handleSubmit(onSubmit)(); + // This is a hacky way of doing things, but isDirty is not being set to false even when there are no errors. + // If error exists and isDirty, it will already have been handled + // This is for when there are no errors, but for some reason isDirty refuses to be reset + if (Object.keys(errors).length === 0 && isDirty) { + onSubmit(getValues()); + } + }} + className="h-full flex flex-col" + >
{ {errors.response &&

{errors.response.message}

}
- ( + + )} /> )} @@ -355,7 +409,7 @@ const AutoResponder = ({ onSave, activeSettings }: Props) => {